Rno
Rno

Reputation: 897

WPF beginner: How to bind to an instance of a class and reference it in code-behind event handlers

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

Answers (1)

BionicCode
BionicCode

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

Related Questions