Linking button controls to data (WPF) - c#

I am working on an application showing 20 graphic buttons controls in a MainWindow (Button1 to Button20).
Each button control can display a Content string, and has a tooltip designed as follow :
<Button x:Name="button1" FontWeight="Bold" FontSize="15" Content="" HorizontalAlignment="Left" Margin="20,69,0,0" VerticalAlignment="Top" Width="92" Height="29" Click="Button_Click" Background="#FFFFFFFF" MouseEnter="button_MouseEnter">
<Button.ToolTip>
<Border Margin="-4,0,-4,-3" Padding="10" Background="Yellow">
<Border.BitmapEffect>
<OuterGlowBitmapEffect></OuterGlowBitmapEffect>
</Border.BitmapEffect>
<Label x:Name ="lbl1" FontSize="20" Content="{Binding Path=ToolTip}">
</Label>
</Border>
</Button.ToolTip>
<Button.Effect>
<DropShadowEffect/>
</Button.Effect>
</Button>
I would like to define the string content and the tooltip string for each button in an XML file so this information can be changed by modifying the XML file.
For this, I created a ViewModel defining an object called Bouton (in french) :
public class Bouton : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
void Notify(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
int boutonNumber;
public int BoutonNumber{ get { return boutonNumber; } set { boutonNumber= value; Notify("BoutonNumber"); } }
string texteBouton;
public string TexteBouton { get { return texteBouton; } set { texteBouton = value; Notify("TexteBouton"); } }
string tooltip;
public string Tooltip { get { return tooltip; } set { tooltip = value; Notify("ToolTip"); } }
public Bouton(int nb, string tb, string tt)
{
BoutonNumber = nb;
TexteBouton = tb;
Tooltip = tt;
}
}
When reading the XML file, I create 20 objects of Bouton type with the information about boutonNumber, Content and Tooltip. Then all these Bouton objects are stored into a List collection.
Now I want to use DataBinding between my Bouton list and the graphic controls on my MainWindow to be able to display the content string and the tooltip string on each button.
In the MainWindow, I used the following code :
public MainWindow()
{
InitializeComponent();
List<Bouton> lst = Utilities.CreateList();
this.DataContext = lst;
}
where lst is a List collection correctly initialized.
But I do not know how to make the databinding work on the Button controls. How can I make each of the 20 Button controls correctly link to the corresponding Bouton object (contained in the Boutons collection) ? I mean how can Button1 control get its strings from my Bouton1 object, the Button2 control get its string from Bouton2 object and so on until Button20 control and Bouton20 object ?
Thank you for your help. Please note I am a beginner with WPF and this is my first WPF project with Visual Studio.

I think the simplest option would be to wrap your button in a UserControl.
This UserControl then contains a property of type Bouton (not great naming btw. - even if it is a different language, the term "button" already exists, so this is rather ambiguous. As a non-native english speaker myself, I also recommend getting used to naming in English, it will save you lots of headaches in the long run, but that might be subjective )
You can then bind directly to that property in the UserControl's template. All you have to do is assign each UserControl the correct button data.
eg.
public class CustomButton : UserControl
{
public Bouton ParsedButtonData { get; set; }
public CustomButton()
{
DataContext = this;
InitializeComponent();
}
}
and in your UserControl's template:
<Button ...>
<Button.ToolTip>
<Border ...>
<Label Content="{Binding ParsedButtonData.Tooltip}" ...>
</Label>
</Border>
</Button.ToolTip>
</Button>
You can then place the UserControl in your XAML like this:
xmlns:ctrls="clr-namespace:MyProject.MyNamespace"
<ctrls:CustomButton name="myFirstButton"/>
Now all you have to do is make sure each CustomButton has their ParsedButtonData set to the corresponding piece of data. You can either set this manually for each button created in your XAML, or you can create the CustomButtons through C# in the first place.
If you create your UserControls in XAML, for example:
public void SomeMethod()
{
myFirstButton.ParsedButtonData = lst[0];
}
Alternatively, you might want to look into extending ItemsControl. It's basically made for this sort of application. An ItemsControl has an ItemsSource, which can be any collection type such as List<> or ObservableCollection<>. It then creates its children from that collection, setting the DataContext automatically to the corresponding element in said list.
Examples of that would be DataGrid or ListView. I find that to be a little more involved though, if you really just want to place a bunch of a Buttons on a single View.

Related

WPF (Avalonia) - Draw control (button) outside TabItem?

Perhaps I'm going about this the wrong way, but my layout is in a way where I have multiple Expanders in a TabControl and I want to add an "expand all" button.
Now logically this button should be inside the tab as it would control the elements in the tab so they ought to be grouped together. Visually however this would be a waste of space as I got a lot of empty space on the Tab Header bar (not sure what the terminology is, the row with the tabheaders).
So what I'm trying to achieve is adding a button outside the content of the tab. The canvas element seems to be what I need to use and it's working as far as its repositioning the element but it gets cut off. This is much easier to explain with a picture so
(if you look hard you can see where the button is as the header covering it is slightly translucent)
Now I can position it where I'd like it to be by moving it outside the TabItem but then I would have to write code to see which tab is focussed and hide it when it's not "Current" that is focussed. That to me sounds like the wrong way to do it as the only thing I want to do is move a button which is a 'view' type of thing.
My MainWindow.axaml:
<TabControl Grid.Row="1" Grid.Column="0" VerticalAlignment="Stretch">
<TabItem Header="Current" ZIndex="1">
<ScrollViewer Classes="CrawlersInAction">
<StackPanel>
<Canvas>
<Button Canvas.Right="10" Canvas.Top="-20" ZIndex="5">Expand All</Button>
</Canvas>
<!-- My very long template code for rendering the expanders -->
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
I do have a background in HTML/CSS so I thought Zindex would the trick and tried applying it in various places without any luck.
PS: I'm using Avalonia instead of WPF but it's pretty much a cross-platform clone, so any WPF know-how probably carries over 1:1.
If you think about it, this functionality lives in the ViewModel at the same "level" as the Tab Control.
<Grid>
<TabControl Items="{Binding MyTabViewModels}" SelectedItem={Binding SelectedTab} />
</Grid>
An Instance of MyTabViewModel has a collection on it:
public ObservableCollection<MyCollectionType> Items
The item class MyCollectionType has an IsExpanded property ...
public bool IsExpanded {get;set;}
Bound to your Expander control IsExpanded property.
Shove your button into the XAML
<Grid>
<TabControl Items="{Binding MyTabViewModels}" />
<Button Commmand={Binding ExpandAllCommand} />
</Grid>
Now on your base ViewModel your ICommand can do something like:
public void ExpandAllCommandExecuted()
{
foreach(var vm in SelectedTab.Items)
{
vm.IsExpanded = true;
}
}
Good luck, this is all pseudocode but illustrates a potential pattern.
The problem seems to have originated from placing my <canvas> control inside the <scrollviewer> control. I've placed it outside it whilst trying many things it seems and it works as I wanted it to. The buttons are visible rendering on top of the tabbar (TabStrip).
My XAML is now:
<TabControl Grid.Row="1" Grid.Column="0" VerticalAlignment="Stretch">
<TabItem Header="Current">
<StackPanel>
<Canvas>
<StackPanel Orientation="Horizontal" Canvas.Right="0" Canvas.Bottom="10" Spacing="5">
<Button Command="{Binding CollapseAll}" IsEnabled="{Binding !AllAreCollapsed}">Collapse All</Button>
<Button Command="{Binding ExpandAll}" IsEnabled="{Binding !AllAreExpanded}">Expand All</Button>
</StackPanel>
</Canvas>
<ScrollViewer Classes="CrawlersInAction">
<StackPanel>
<ItemsControl Name="itemscontrol" Items="{Binding SiteInfos}" VerticalAlignment="Stretch">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander ExpandDirection="Down" IsExpanded="{Binding IsExpanded, Mode=TwoWay}" VerticalAlignment="Stretch">
<!-- Ommited my very long template code -->
</Expander>
<DataTemplate>
<ItemsControl.ItemTemplate>
<ItemsControl>
</StackPanel>
</ScrollViewer>
</StackPanel>
</TabItem>
</TabControl>
Codewise I ended up adding a "IsExpanded" property to my SiteInfo class that is used as the base for the expanders IsExpanded property and kept in sync by making it a two way binding as per the XAML above. The code on SiteInfo is:
public class SiteInfo : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public static readonly bool StartIsExpanded = true;
private bool _isExpanded = StartIsExpanded;
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (value != IsExpanded)
{
_isExpanded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsExpanded)));
}
}
}
When I create my SiteInfo objects in MainWindowViewModel I subscribe to the events (siteInfo.PropertyChanged += SiteInfo_PropertyChanged;). When the event is received and it would change if my collapse or expand all button should be disabled it sends it own PropertyChangedEvent which then enables/disabled the control.
public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
{
public new event PropertyChangedEventHandler? PropertyChanged;
public ObservableCollection<SiteInfo> SiteInfos { get; }
= new ObservableCollection<SiteInfo>();
//Change SiteInfo.StartExpanded if you want this changed.
private bool _allAreExpanded = SiteInfo.StartIsExpanded;
public bool AllAreExpanded
{
get => _allAreExpanded;
set
{
if (_allAreExpanded != value)
{
_allAreExpanded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AllAreExpanded)));
}
}
}
//Change SiteInfo.StartExpanded if you want this changed.
private bool _allAreCollapsed = !SiteInfo.StartIsExpanded;
public bool AllAreCollapsed {
get { return _allAreCollapsed; }
set {
if (_allAreCollapsed != value)
{
_allAreCollapsed = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AllAreCollapsed)));
}
}
}
private void SiteInfo_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if(e.PropertyName == nameof(siteInfo.IsExpanded))
{
AllAreCollapsed = AreAllCollapsed();
AllAreExpanded = AreAllExpanded();
}
}
public bool AreAllCollapsed()
{
return !SiteInfos.Any<SiteInfo>( siteInfo => siteInfo.IsExpanded );
}
public bool AreAllExpanded()
{
return !SiteInfos.Any<SiteInfo>( siteInfo => siteInfo.IsCollapsed);
}
public void CollapseAll()
{
foreach(SiteInfo siteInfo in SiteInfos)
{
siteInfo.IsExpanded = false;
}
}
public void ExpandAll()
{
foreach (SiteInfo siteInfo in SiteInfos)
{
siteInfo.IsExpanded = true;
}
}
}
Figured I'd add the rest of my code in case anyone Googles this up and wants to do something similar.
So now when my program loads and everything is set to the default expanded true Expand All is disabled, Collapse all is enabled. Changing one expander to collapsed status will have both buttons enabled and collapsing all expanders will disable the Collapse All button.

WPF Bind User Control property not working

I have a user control and im trying to bind one of its properties
User Control Xaml
<UserControl x:Class="pi_browser.Testing.Example"
...
x: Name="LabelControl">
<StackPanel x:Name="RootStackPanel">
<Label Content="{Binding Text, ElementName=LabelControl}"/>
</StackPanel>
</UserControl>
User Control Codebehind
public partial class Example : UserControl
{
public Example()
{
InitializeComponent();
ExampleViewModel vm = new ExampleViewModel(State);
DataContext = vm;
}
public Boolean State
{
get { return (Boolean)this.GetValue(StateProperty); }
set { this.SetValue(StateProperty, value); }
}
public static readonly DependencyProperty StateProperty =
DependencyProperty.Register("State",
typeof(Boolean), typeof(Example), new PropertyMetadata(false));
}
Main Page View Model
class ExampleViewModel
{
public ExampleViewModel(bool v)
{
val = v;
}
bool val;
public string Text { get => val ? "This worked" : "This didnt work"; }
}
Main Window Xaml
<Window x:Class="pi_browser.Testing.Tester" ... >
<Grid>
<local:Example State="True"/>
</Grid>
</Window>
In this example I didn't bind the State variable, I only passed a literal, but ideally I would like to bind to actual values.
State is a boolean, yet you bind to Text. Let us fix one issue by creating a dependency property Text on your User Control. We shall fix the Text issue and not the boolean State issue. Once you fix that, do the same for State.
So to fix Text we need to fix why this fails:
<Label Content="{Binding Text, ElementName=LabelControl}"/>
You set the ElementName to be the UserControl itself, which is what one wants. But then you tell the binding to look for (remember binding is just reflection of an object under the covers) the property Text. The property Text does not exist on that instance/class...but State does. Its obvious to bind to a newly created Text dependency property on the user control to fix the first issue.
Then when you instantiate the control on your main page, you need to then, and only then bind to Text because that property also resides on your viewmodel.
So three things, along with the change mentioned on the UserControl:
Make your ViewModel adhere to INotifyPropertyChanged and make the Text property use the notification mechanism you install.
Make sure that your main page has its DataContext set to a vailid instance of your ViewModel class.
Bind to Text such as <local:Example State="{Binding Text}"/>
Once that is done, the Text value will properly flow towards the UserControl.

How do access the property of the child control?

My English skill is poor because I'm not a native English speaker.
I have created two custom control.
First control has a DP named CaretIndexFromLine and the control name is TextArea.
The following is the summarized cs code of the first control.
public class TextArea : TextBox
{
public int CaretIndexFromLine
{
get { return (int)GetValue(CaretIndexFromLineProperty ); }
set { SetValue(CaretIndexFromLineProperty , value); }
}
public static readonly DependencyProperty CaretIndexFromLineProperty =
DependencyProperty.Register("CaretIndexFromLine", typeof(int), typeof(TextArea), new P
ropertyMetadata(0));
...
}
The second control named Editor has child control TextArea.
The following is the summarized XAML code of the second control.
<Editor> <TextArea x:Name="PART_TextArea"/> </Editor>
And the following is summarized cs code of the second control.
public class Editor : Control
{
public TextArea TextArea { get; private set; }
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.TextArea = (TextArea)Template.FindName("PART_TextArea", this);
}
...
}
And I was going to use the above control in the MainWindow.
<MainWindow> <Editor/> </MainWindow>
There is a problem here.
I want to access the property (CaretIndex) of the TextArea in the XAML code.
I tried like this but It was not operated.
<MainWindow>
<StackPanel>
<Editor x:Name="editor"/>
<TextBlock Text="{Binding ElementName=editor.TextArea, Path=CaretIndexFromLine}/>
</StackPanel>
</MainWindow>
What should I do to achieve the above goal?
Thank you for reading this and please teach me the solution.
Your Editor control has an TextArea property so just set the ElementName to "editor" and the Path to TextArea.CaretIndexFromLine:
<TextBlock Text="{Binding ElementName=editor, Path=TextArea.CaretIndexFromLine}/>
TextArea should be a read-only dependency property though in order for a notification to be raised when it's set in the OnApplyTemplate() method.

Populate control after selectedindexchange event has fired

I have a MVVM WPF project where I have an devexpress accordian control which is populated with xml template items from a ViewModel. That works great, but my problem is when I click on one of the items in the accordian control and the selectedIndexChanged event is fired. I want to handle that in the MVVM manner and get the selected items value(which is a path to an xml file) from the accordian control, fetch the content of the xml file and databind a textbox control with the content of the xml file. The following is what I have tried so far.
Here is my xaml user control
<dxa:AccordionControl Grid.Column="0" x:Name="accordianTemplateMenu"
SelectionMode="Single" SelectionUnit="SubItemOrRootItem" ItemsSource="
{Binding TemplateItems}"
ChildrenPath="TemplateItems" DisplayMemberPath="Header >
<dxmvvm:Interaction.Behaviors>
<dxmvvm:EventToCommand EventName="SelectedItemChanged" Command="
{Binding EditCommand}">
<dxmvvm:EventToCommand.EventArgsConverter>
<Common:AccordionEventArgsConverter/>
</dxmvvm:EventToCommand.EventArgsConverter>
</dxmvvm:EventToCommand>
</dxmvvm:Interaction.Behaviors>
</dxa:AccordionControl>
<GridSplitter Grid.Column="1" />
<TextBlock Grid.Column="2" x:Name="templateItemContainer">
<Run Name="run" Text="{Binding XML}" ></Run>
</TextBlock>
This boils down to the AccordionEventArgsConverter which gets me the event arguments from the selecteditem in the accordian control:
public class AccordionEventArgsConverter :
EventArgsConverterBase<AccordionSelectedItemChangedEventArgs>
{
protected override object Convert(object sender,
AccordionSelectedItemChangedEventArgs args)
{
if (args != null)
{
return args;
}
return null;
}
}
And finally my viewmodel:
class TemplateMenuViewModel
{
private List<TemplateItem> _templateItems;
public TemplateMenuViewModel()
{
EditCommand = new DelegateCommand<object>(Edit, CanEdit);
}
public List<TemplateItem> TemplateItems
{
get
{
TemplateProvider provider = new TemplateProvider();
return provider.GetTemplateMenuItems("pathToMenuItems");
}
set { _templateItems = value; }
}
public ICommand<object> EditCommand { get; private set; }
public void Edit(object accordianItemArgs)
{
}
public bool CanEdit(object accordianItemArgs)
{
return accordianItemArgs != null;
}
}
I am able to get into the public void Edit method, which is great because from there I can use the accordianItemArgs to get the xml content, but how do I "return"/databind the xml content to the textblock element in the xaml file?
There are a couple of things:
You need the TemplateMenuViewModel to define an XML property. It looks like your TextBlock is already binding to it.
Then you need your ViewModel to implement the INotifyPropertyChanged interface. It doesn't look like you're doing that, then raise a property changed event when the XML text is set.
You should set your Text="{Binding XML}" with a Mode of OneWay:
Text="{Binding XML, Mode=OneWay}"
If you need more information on how to implement INotifyPropertyChanged, check out this tutorial: https://www.tutorialspoint.com/mvvm/mvvm_first_application.htm.

UserControl using parent elements in wpf?

When you have a usercontrol in wpf can it reach outside to its parent elements? For instance my user control just lists some generic things which are held inside the control which is encapsulated within a dockpanel on the main window, but I have a textbox and button in the main window that I would like to access from the control... is this possible?
It would save me alot of time rather than changing the content of the entire window and displaying the same textbox/button in every usercontrol. If anyone has an example of this it would be much appreciated.
Yes it is possible and here is some code I have used to compose presentations out of UserControls that have DPs.
I don't love it even a little, but it works. I also think this is a great topic and maybe some code will help get some better answers!
Cheers,
Berry
UserControl XAML
<Button x:Name="btnAddNewItem" Style="{StaticResource blueButtonStyle}" >
<StackPanel Orientation="Horizontal">
<Image Source="{resx:Resx ResxName=Core.Presentation.Resources.MasterDetail, Key=bullet_add}" Stretch="Uniform" />
<Label x:Name="tbItemName" Margin="5" Foreground="White" Padding="10, 0">_Add New [item]</Label>
</StackPanel>
</Button>
UserControl Code Behind
public partial class AddNewItemButton : UserControl
{
...
#region Item Name
public static readonly DependencyProperty ItemNameProperty = DependencyProperty.Register(
"ItemName", typeof(string), typeof(AddNewItemButton),
new FrameworkPropertyMetadata(OnItemNameChanged));
public string ItemName
{
get { return (string)GetValue(ItemNameProperty); }
set { SetValue(ItemNameProperty, value); }
}
public string ButtonText { get { return (string) tbItemName.Content; } }
private static void OnItemNameChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
// When the item name changes, set the text of the item name
var control = (AddNewItemButton)obj;
control.tbItemName.Content = string.Format(GlobalCommandStrings.Subject_Add, control.ItemName.Capitalize());
control.ToolTip = string.Format(GlobalCommandStrings.Subject_Add_ToolTip, control.ItemName);
}
#endregion
#region Command
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
"Command", typeof(ICommand), typeof(AddNewItemButton),
new FrameworkPropertyMetadata(OnCommandChanged));
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
private static void OnCommandChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
// When the item name changes, set the text of the item name
var control = (AddNewItemButton)obj;
control.btnAddNewItem.Command = control.Command;
}
#endregion
}
Another UserControl showing Composition
<UserControl ...
xmlns:uc="clr-namespace:Smack.Core.Presentation.Wpf.Controls.UserControls"
>
<DockPanel LastChildFill="True">
...
<uc:AddNewItemButton x:Name="_addNewItemButton" Margin="0,0,10 0" DockPanel.Dock="Right" />
...
</DockPanel>
</UserControl>
A better design pattern would be to have the usercontrol notify (via event) the main window when something needs to be changed, and to ask the window (via method) when it needs some information. You would, for example, have a GetText() method on the window that the usercontrol could call, and a ChangeText event on the usercontrol that the window would subscribe to.
The idea is to keep the window in control at all times. Using this mentality will make it easier for you to develop applications in the future.
To answer your question: yes, you can either access parent controls either through a RelativeSource binding or through the Parent member in the back code. But a better answer is similar to #KendallFrey answer. Adopt a framework like Light MVVM and use its messenger class or use events the way Kendall described.

Categories

Resources