MVVM Pattern in WPF

October 16, 2020—A survey of the MVVM pattern in WPF

MVVM stands for Model View View-Model. It is a pattern for creating user interfaces. It helps you separate your view and its behavior from the rest of your application. With some discipline that means your application will be easier to change over time.

Model—The model is basically everything except the view and its behavior.

View—The user interface.

View model—The view's behavior.

WPF stands for Windows Presentation Foundation. It was created by Microsoft in the late 2000s. It helps you create visual applications for Windows using the .Net Framework.

WPF popularized the MVVM pattern, and the MVVM pattern is very natural to use in WPF.

A little example

Here's an example:

<!-- View.xaml -->
... (some boilerplate code goes here)
<TextBlock Text="{Binding Message}"/>
// ViewModel.cs
public class ViewModel : INotifyPropertyChanged
{
  string _message;

  public event PropertyChangedEventHandler PropertyChanged;

  public string Message
  {
    get => _message;
    set
    {
      if (value == _message)
        return;
      _message = value;
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Message)));
    }
  }
}
// Model.cs
public class Model
{
  public async Task<string> GetMessageFromDatabaseAsync() { ... }
}
// Program.cs
class Program
{
  static void Main()
  {
    var model = new Model();
    var viewModel = new ViewModel();

    Task.Run(async () =>
    {
      viewModel.Message = await model.GetMessageFromDatabaseAsync();
    });

    var view = new View();
    view.DataContext = viewModel;
    view.ShowDialog();
  }
}

Let's step through Program.cs

  1. The model is instantiated
  2. The view model is instantiated
  3. The view is instantiated, given the view model, and displayed to the user
  4. Behind the scenes, the {Binding Message} has instructed WPF to pay attention to the view model's PropertyChanged event, especially when it raises an event with the name "Message"
  5. In the background, some message is asynchronously acquired from the database and assigned to the view model's Message property
  6. When the view model's Message property is assigned it will raise its PropertyChanged event
  7. WPF notices the raised event and reads the Message property from the view model and assigns it to the Text property of the TextBlock in the view

The end result is the message is shown on the screen.

That might seem kind of convoluted. Especially if you compare with this:

// Program.cs
class Program
{
  static void Main()
  {
    var textBlock = new TextBlock();
    var window = new Window
    {
      Content = textBlock
    };
    Task.Run(async () =>
    {
      var message = await new Model().GetMessageFromDatabaseAsync();
      textBlock.Dispatcher.InvokeAsync(() =>
      {
        textBlock.Text = message;
      });
    });
    window.ShowDialog();
  }
}

But I think with the next example you'll begin to see why the convolution is worth it.

A bigger example

Here's a more complicated example:

// BaseViewModel.cs
public abstract class BaseViewModel : INotifyPropertyChanged
{
  readonly Dictionary<string, object> _properties = new Dictionary<string, object>();

  public event PropertyChangedHandler PropertyChanged;

  protected T Get<T>([CallerMemberName] string propertyName = null)
  {
    // Return the indicated property
  }

  protected bool Set(object value, [CallerMemberName] string propertyName = null)
  {
    // Set the indicated property. Return true and raise the PropertyChanged
    // event if the property has changed
  }
}
// ButtonViewModel
public class ButtonViewModel : BaseViewModel
{
  public ICommand Command
  {
    get => Get<ICommand>();
    set => Set(value);
  }

  public string Label
  {
    get => Get<string>();
    set => Set(value);
  }
}
// ViewModel.cs
public class ViewModel : BaseViewModel
{
  public ViewModel(
    IEnumerable<string> people,
    Action<int> handleRatingFeedback,
    Action<string> handleFavoritePerson)
  {
    RatingScale = new ObservableCollection(
      Enumerable
      .Range(1, 5)
      .Select(rating => new ButtonViewModel
      {
        Label = rating.ToString(),
        Command = new ActionCommand(() => handleRatingFeedback(rating))
      })
    );
    People = new ObservableCollection(
      people
      .Select(person => new ButtonViewModel
      {
        Label = person,
        Command = new ActionCommand(() => handleFavoritePerson(person))
      })
    );
  }

  public ObservableCollection<ButtonViewModel> RatingScale { get; }
  public ObservableCollection<ButtonViewModel> People { get; }
}
<!-- RowOfButtons.xaml -->
... (some boilerplate code goes here)
<ItemsControl ItemsSource="{Binding ButtonViewModels}">
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <StackPanel
        IsItemsHost = "True"
        Orientation = "Horizontal"/>
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>
  <ItemsControl.ItemTemplate>
    <DataTemplate DataType="{x:Type ButtonViewModel}">
      <Button
        Command="{Binding Command}"
        Content="{Binding Label}"/>
    </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>
<!-- View.xaml -->
... (some boilerplate here)
<StackPanel>
  <TextBlock>How are we doing?</TextBlock>
  <RowOfButtons DataContext="{Binding RatingScale}"/>
  <TextBlock>Pick your favorite person:</TextBlock>
  <RowOfButtons DataContext="{Binding People}"/>
</StackPanel>
// Model.cs
public class Model
{
  public async Task StoreRatingAsync(int rating);
  public string[] GetPeople();
  public async Task StoreFavoritePersonAsync(string person);
}
// Program.cs
class Program
{
  static void Main()
  {
    var model = new Model();
    var people = model.GetPeople();
    var viewModel = new ViewModel(
      people,
      async rating => await model.StoreRatingAsync(rating),
      async person => await model.StoreFavoritePersonAsync(person)
    );
    var view = new View
    {
      DataContext = viewModel
    };
    view.ShowDialog();
  }
}

Let's compare it to WPF without MVVM:

// Program.cs
class Program
{
  static void Main()
  {
    var model = new Model();
    var ratingsStack = new StackPanel
    {
      Orientation = Horizontal
    };
    ratingsStack.Children.AddRange(
      Enumerable
      .Range(1, 5)
      .Select(rating => new Button
      {
        Command = new ActionCommand(async () => await model.StoreRatingAsync(rating)),
        Content = rating.ToString()
      })
    );
    var peopleStack = new StackPanel
    {
      Orientation = Horizontal
    };
    peopleStack.Children.AddRange(
      model
      .GetPeople()
      .Select(person => new Button
      {
        Command = new ActionCommand(async () => await model.StoreFavoritePersonAsync(person)),
        Content = person
      })
    );
    var view = new Window
    {
      Content = new StackPanel
      {
        Children =
        {
          new TextBlock
          {
            Text = "How are we doing?"
          },
          ratingsStack,
          new TextBlock
          {
            Text = "Pick your favorite person:"
          },
          peopleStack
        }
      }
    }
    view.ShowDialog();
  }
}

Again, I think the non-MVVM way is simpler in this case. But that's not all...

Add some styling

Now let's change the label colors. In the MVVM version we'll change our views:

<!-- RowOfButtons.xaml -->
...
<DataTemplate DataType="{x:Type ButtonViewModel}">
  <Button Command="{Binding Command}">
    <TextBlock
      Foreground="Green"
      Text="{Binding Label}"/>
  </Button>
</DataTemplate>
...
<!-- View.xaml -->
...
<TextBlock Foreground="Red">How are we doing?</TextBlock>
...
<TextBlock Foreground="Blue">Pick your favorite person:</TextBlock>
...

And in the non-MVVM version we'll change the code that generates the UI components:

// Program.cs
...
ratingsStack.Children.AddRange(
  Enumerable
  .Range(1, 5)
  .Select(rating => new Button
  {
    Command = new ActionCommand(async () => await model.StoreRatingAsync(rating)),
    Content = new TextBlock
    {
      Foreground = Brushes.Green,
      Text = rating.ToString()
    }
  })
);
...
peopleStack.Children.AddRange(
  model
  .GetPeople()
  .Select(person => new Button
  {
    Command = new ActionCommand(async () => await model.StoreFavoritePersonAsync(person)),
    Content = new TextBlock
    {
      Foreground = Brushes.Green,
      Text = rating.ToString()
    }
  })
);
...
new TextBlock
{
  Foreground = Brushes.Red,
  Text = "How are we doing?"
},
...
new TextBlock
{
  Brushes.Blue,
  Text = "Pick your favorite person:"
},
...

Did you catch that? In the MVVM version our changes were quite isolated. We only changed views. We didn't change view models, nor did we alter the model, nor did we alter how the view, view model, and model are wired together. The chances are quite small that we introduced a bug in how data is read from or stored to the model. We also probably didn't accidentally mess up what happens when you click on your favorite person. The advantage is our changes were very isolated.

But in the non-MVVM version we had to change code in close proximity to completely unrelated things. Simple changes like changing the labels' colors were introduced immediately next to the code that manipulates the model.

Have you ever experienced an application that's stuck with a decades-old UI because its maintainers don't have the time to fish through the tangled mess of UI styling and business logic? Have you ever experienced an application that broke after its maintainers changed colors or layout? Those kinds of things happen less often with MVVM.

MVVM is not the only way

Now that we recognize the dangers of mixing UI concerns with other responsibilities you can probably imagine various ways to separate those things. And you can probably imagine ways to separate those things without using MVVM. And that's fine.

The point

The point is MVVM is a disciplined way (not the disciplined way) to isolate UI concerns.

The downsides of the MVVM pattern in WPF include:

  • Lots of boilerplate code
  • Sometimes you have to take a pretty indirect path
  • WPF's binding mechanism isn't the fastest thing out there

But it brings all the benefits of separating concerns.

Disclaimer—the code on this page will not compile. It's inspirational pseudocode that I wrote off the cuff.