Reputation: 897
Trying to learn WPF. Struggling with data-binding setup. On my main UI, I have two comboboxes linked to properties of the instance class. I want to 'reuse' that class instance in event handlers.
My struggle isn't with getting the code working, but of a design best-practices nature.
My class (simplified)
public class SomeClass : INotifyPropertyChanged
{
public SomeClass() {}
public IList<string> Categories
{
get
{
return _categories;
}
set
{
_categories = value;
OnPropertyChanged(nameof(Categories));
}
}
public IList<string> Forms
{
get
{
return _forms;
}
set
{
_forms = value;
OnPropertyChanged(nameof(Forms));
}
}
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
My first instinct was to declare all the bindings in the xaml. Everything in one place. Something like
<Window.Resources>
<local:SomeClass x:key="MyClass"/>
</Window.Resources>
Then in a toolbar:
<ToolBarPanel x:Name="myToolStripName" DataContext="{StaticResource MyClass}">
<ToolBarTray>
<ToolBar Height="25px">
<ComboBox x:Name="cboCategories"
ItemsSource="{Binding Path=Categories}"
SelectionChanged="CboCategories_SelectionChanged" />
<ComboBox x:Name="cboForms"
ItemsSource="{Binding Path=Forms}" />
</ToolBar>
</ToolBarTray>
</ToolBarPanel>
In the code-behind of MainWindow
there is the event handler
private void CboCategories_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
string categoryName = e.AddedItems[0].ToString();
if (!string.IsNullOrEmpty(categoryName))
{
[problem: MyClass/SomeClass].GetFormNames(categoryName);
}
}
I run into trouble here. I need to call a method in my instance class, but I do not think I can get to it from the event handler. Can I? I could simply new it up, but I would loose the binding.
So this approach isn't going to work. I could change my SomeClass
to be a Singleton, but I read those are considered an anti-pattern. Another approach would be to make it static
, but then I cannot (easily) implement INotifyPropertyChanged
.
In the end, I made SomeClass
a private readonly
field of MainWindow.xaml.cs
and set the DataContext
from there. Which works fine, but now the application relies on bindings being defined in both the xaml and code-behind, which feels counter-intuitive. I could of course move all the databinding stuff to code-behind. Again, counter-intuitive.
Given the setup described, which I feel must be really common, can you please give me some guidance on what is considered best-practice and suggestions for improvement?
Upvotes: 1
Views: 1487
Reputation: 29028
A better approach would be to assign the view model SomeClass
to the Window.DataContext
instead of Window.Resources
and use the property inheritance feature of DataContext
:
<Window.DataContext>
<local:SomeClass x:Name="ViewModel"/>
</Window.DataContext>
Remove the DataContext
property declaration from the ToolBarPanel
since the DataContext
is inherited from the parent element (the Window
). All child elements of a FrameworkElement
in the visual tree will inherit their parent's DataContext
:
<ToolBarPanel x:Name="MyToolStripName">
<ToolBarTray>
<ToolBar Height="25px">
<ComboBox x:Name="CboCategories"
ItemsSource="{Binding Categories}"
SelectionChanged="CboCategories_SelectionChanged" />
<ComboBox x:Name="CboForms"
ItemsSource="{Binding Forms}" />
</ToolBar>
</ToolBarTray>
</ToolBarPanel>
In your event handler just cast the DataContext
from Object
to your view model SomeClass
:
private void CboCategories_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
var categoryName = e.AddedItems[0].ToString();
if (!string.IsNullOrEmpty(categoryName)
&& this.DataContext is SomeClass viewModel)
{
viewModel.GetFormNames(categoryName);
}
}
If want to get rid of the event handler in the code-behind file, given that it doesn't do UI related operations, you can add a property as binding target for SelectedItem
to the view model:
public class SomeClass : INotifyPropertyChanged
{
private string selectedCategory;
public string SelectedCategory
{
get => this.selectedCategory;
set
{
this.selectedCategory = value;
OnPropertyChanged();
GetFormNames(this.SelectedCategory);
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
...
}
Replace the event handler with the binding of SelectedItem
to the view model. The Binding.Mode
has to be set to either OneWayToSource
or TwoWay
in order to send the data to the binding source:
<ToolBarPanel x:Name="MyToolStripName">
<ToolBarTray>
<ToolBar Height="25px">
<ComboBox x:Name="CboCategories"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory, Mode=TwoWay}" />
<ComboBox x:Name="CboForms"
ItemsSource="{Binding Forms}" />
</ToolBar>
</ToolBarTray>
</ToolBarPanel>
Applying the attribute [CallerMemberName
] to your PropertyChanged invocator method's parameter, like in the example, makes invocations more convenient since you do not have to pass the property names anymore.
Upvotes: 2