Combine Multiple ViewModels in WPF

I just helped a colleague of mine with a tricky task:

  • The user chooses from a drop down on View 1
  • Based on that choice, data and controls in View 3 need to be adjusted

The task description is straight forward and basically resembles any slightly more complicated wizard. Unfortunately, View 1 and View 3 didn't really have much in common, except the window where they were displayed, and rewriting that part of the application to accommodate for the change in specification didn't seem reasonable. Instead we did something else.

Given (Existing Code)

As a given, there was View 1 with a corresponding ViewModel 1 and View 3 with a corresponding ViewModel 3.

View 3 was already completely prepared. It's ViewModel had 2 flags (Flag 1 and Flag 2) and setting the flags to different values, triggered the correct changes on the View. Flag 1 and Flag 2 even had a correct initial value set, based on common data source between all the views, that's how we know, that it already technically worked. What was missing, was the dynamic update based on a user's actions in View 1.

View 1 was also prepared. It had a drop down list to choose from, which was correctly bound to a specific property in ViewModel 1.

The Changes (New Code)

The necessary changes were actually very simple and straight forward.

  1. View introduced a dependency between ViewModel 3 and ViewModel 1
public class ViewModel3 : INotifyPropertyChanged
{
    private readonly ViewModel1 _viewModel1; // this part is new

    public ViewModel3(CommonDataSource data,
        ViewModel1 viewModel1 // this part is new
    )
    {
        _viewModel1 = viewModel1; // this part is new
        
        // ...
    }
    
    // ...
}
  1. Now we updated the Flag 1 and Flag 2 properties on ViewModel 3 to use viewModel1 directly instead of data from CommonDataSource
public bool Flag1 => _viewModel.BoundProperty == "Some value";
public bool Flag2 => _viewModel.BoundProperty == "Some other value";

This worked exactly the same as before, because ViewModel 3 wouldn't notify it's View about the changes for Flag 1 and Flag 2. However, adding that notification was super simple.

  1. We subscribed to PropertyChanged on ViewModel 1 and raised our own event, when BoundProperty was changed
public class ViewModel3 : INotifyPropertyChanged
{
    private readonly ViewModel1 _viewModel1;

    public ViewModel3(/* ... */)
    {
        // ...
        
        _viewModel1.PropertyChanged += viewModel1_PropertyChanged
        
        // ...
    }
    
    // ...
    
    private void viewModel1_propertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(_viewModel1.BoundProperty)) 
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Flag1)));
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Flag2)));
        }
    }
}

Recap (Summary)

To recap, by introducing a proper dependency between ViewModel 3 and ViewModel 1, we could create simple getter properties which calculated a new value based on changes in ViewModel 1. Additionally by subscribing to the PropertyChanged event in ViewModel 1 we could get View 3 to be notified about changes in ViewModel 3, even though those changes were just indirect. Here the combined modifications.

public class ViewModel3 : INotifyPropertyChanged
{
    private readonly ViewModel1 _viewModel1; // this part is new

    public ViewModel3(CommonDataSource data,
        ViewModel1 viewModel1 // this part is new
    )
    {
        _viewModel1 = viewModel1; // this part is new
        _viewModel1.PropertyChanged += viewModel1_PropertyChanged

        // ...
    }
    
    // ...

    public bool Flag1 => _viewModel.BoundProperty == "Some value";
    public bool Flag2 => _viewModel.BoundProperty == "Some other value";

    private void viewModel1_propertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(_viewModel1.BoundProperty)) 
        {
            NotifyPropertyChanged(nameof(Flag1));
            NotifyPropertyChanged(nameof(Flag2));
        }
    }
}

In case, flag 1 or 2 would depend on additional properties as well, we could just update the calculation (right now a very simple if condition) and update the PropertyChanged-event handler.