Skip to main content

.NET MVVM Pattern

Introduction

The Model-View-ViewModel (MVVM) pattern is a design pattern specifically created for modern UI development. It was introduced by Microsoft architects to simplify the event-driven programming of user interfaces, particularly for Windows Presentation Foundation (WPF) and Silverlight applications. Today, MVVM is a cornerstone of .NET desktop application development, especially for WPF, Universal Windows Platform (UWP), and Xamarin applications.

MVVM separates application logic from the user interface, making your applications more maintainable, testable, and scalable. This separation allows developers and designers to work more independently and makes code reuse easier.

MVVM Core Components

The MVVM pattern consists of three core components:

  1. Model: Represents the application data and business logic
  2. View: Defines the UI structure and appearance
  3. ViewModel: Acts as a bridge between the Model and View, handling UI logic

Let's explore each component in detail.

Model

The Model represents the application's data and business rules. It's responsible for:

  • Data access
  • Business logic
  • Data validation

Models are completely independent of the user interface and aren't aware of the View or ViewModel.

csharp
// Sample Model class
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }

public string FullName => $"{FirstName} {LastName}";

public bool ValidateName()
{
return !string.IsNullOrEmpty(FirstName) && !string.IsNullOrEmpty(LastName);
}
}

View

The View defines the structure and appearance of what the user sees on the screen. In WPF applications, Views are typically defined in XAML. The View should contain minimal code-behind and should interact with the ViewModel through data binding, commands, and other mechanisms.

xml
<!-- Sample View (XAML) -->
<Window x:Class="MVVMSample.Views.PersonView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Person Details" Height="300" Width="400">
<Grid>
<StackPanel Margin="10">
<TextBlock Text="First Name:" />
<TextBox Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="Last Name:" />
<TextBox Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="Save" Command="{Binding SaveCommand}" Margin="0,10,0,0" />
</StackPanel>
</Grid>
</Window>

ViewModel

The ViewModel exposes data and commands from the Model to the View. It's responsible for:

  • Exposing data from the Model to the View
  • Handling user interactions via commands
  • Managing View state
  • Implementing input validation logic

ViewModels use property change notification to update the UI automatically when data changes.

csharp
public class PersonViewModel : INotifyPropertyChanged
{
private Person _person;

public PersonViewModel()
{
_person = new Person();
SaveCommand = new RelayCommand(Save, CanSave);
}

private string _firstName;
public string FirstName
{
get => _firstName;
set
{
if (_firstName != value)
{
_firstName = value;
_person.FirstName = value;
OnPropertyChanged();
SaveCommand.RaiseCanExecuteChanged();
}
}
}

private string _lastName;
public string LastName
{
get => _lastName;
set
{
if (_lastName != value)
{
_lastName = value;
_person.LastName = value;
OnPropertyChanged();
SaveCommand.RaiseCanExecuteChanged();
}
}
}

public RelayCommand SaveCommand { get; private set; }

private bool CanSave()
{
return _person.ValidateName();
}

private void Save()
{
// Save logic here
MessageBox.Show($"Person {_person.FullName} saved!");
}

public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

Key MVVM Concepts

Data Binding

Data binding is a core concept in MVVM that connects properties of the ViewModel to UI elements in the View. When a ViewModel property changes, the UI automatically updates, and vice versa.

xml
<TextBox Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" />

In this example:

  • Text is the target property on the TextBox
  • FirstName is the source property on the ViewModel
  • UpdateSourceTrigger=PropertyChanged specifies when changes in the UI should update the ViewModel

Commands

Commands allow user actions in the View (like button clicks) to execute methods in the ViewModel without direct event handlers in code-behind. The ICommand interface enables this functionality.

csharp
// A simple implementation of ICommand
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;

public RelayCommand(Action execute, Func<bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}

public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute();
}

public void Execute(object parameter)
{
_execute();
}

public event EventHandler CanExecuteChanged;

public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}

Then use the command in your ViewModel:

csharp
public RelayCommand SaveCommand { get; private set; }

public PersonViewModel()
{
SaveCommand = new RelayCommand(Save, CanSave);
}

And bind it in the View:

xml
<Button Content="Save" Command="{Binding SaveCommand}" />

Property Change Notification

For data binding to work properly, the ViewModel must notify the View when property values change. This is done by implementing the INotifyPropertyChanged interface.

csharp
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;

field = value;
OnPropertyChanged(propertyName);
return true;
}
}

Implementing MVVM: Step by Step

Let's build a simple contact management application using MVVM:

1. Create the Model

csharp
public class Contact
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Phone { get; set; }

public bool IsValid()
{
return !string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Email);
}
}

public class ContactRepository
{
private List<Contact> _contacts = new List<Contact>();

public IEnumerable<Contact> GetAll()
{
return _contacts;
}

public void Add(Contact contact)
{
contact.Id = _contacts.Count > 0 ? _contacts.Max(c => c.Id) + 1 : 1;
_contacts.Add(contact);
}

public void Update(Contact contact)
{
var existingContact = _contacts.FirstOrDefault(c => c.Id == contact.Id);
if (existingContact != null)
{
existingContact.Name = contact.Name;
existingContact.Email = contact.Email;
existingContact.Phone = contact.Phone;
}
}

public void Delete(int id)
{
var contact = _contacts.FirstOrDefault(c => c.Id == id);
if (contact != null)
{
_contacts.Remove(contact);
}
}
}

2. Create the ViewModel

csharp
public class ContactViewModel : ViewModelBase
{
private readonly ContactRepository _repository;
private Contact _selectedContact;

public ContactViewModel()
{
_repository = new ContactRepository();
LoadContacts();

AddCommand = new RelayCommand(AddContact);
UpdateCommand = new RelayCommand(UpdateContact, CanUpdateContact);
DeleteCommand = new RelayCommand(DeleteContact, CanDeleteContact);
}

public ObservableCollection<Contact> Contacts { get; } = new ObservableCollection<Contact>();

public Contact SelectedContact
{
get => _selectedContact;
set
{
if (SetProperty(ref _selectedContact, value))
{
// When selection changes, update properties and command states
Name = value?.Name;
Email = value?.Email;
Phone = value?.Phone;
UpdateCommand.RaiseCanExecuteChanged();
DeleteCommand.RaiseCanExecuteChanged();
}
}
}

private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}

private string _email;
public string Email
{
get => _email;
set => SetProperty(ref _email, value);
}

private string _phone;
public string Phone
{
get => _phone;
set => SetProperty(ref _phone, value);
}

public RelayCommand AddCommand { get; }
public RelayCommand UpdateCommand { get; }
public RelayCommand DeleteCommand { get; }

private void LoadContacts()
{
Contacts.Clear();
foreach (var contact in _repository.GetAll())
{
Contacts.Add(contact);
}
}

private void AddContact()
{
var contact = new Contact
{
Name = Name,
Email = Email,
Phone = Phone
};

_repository.Add(contact);
Contacts.Add(contact);

// Clear form
Name = string.Empty;
Email = string.Empty;
Phone = string.Empty;
}

private bool CanUpdateContact()
{
return SelectedContact != null && !string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Email);
}

private void UpdateContact()
{
if (SelectedContact != null)
{
SelectedContact.Name = Name;
SelectedContact.Email = Email;
SelectedContact.Phone = Phone;

_repository.Update(SelectedContact);

// Refresh the list to reflect changes
int selectedIndex = Contacts.IndexOf(SelectedContact);
LoadContacts();
SelectedContact = Contacts.ElementAtOrDefault(selectedIndex);
}
}

private bool CanDeleteContact()
{
return SelectedContact != null;
}

private void DeleteContact()
{
if (SelectedContact != null)
{
_repository.Delete(SelectedContact.Id);
Contacts.Remove(SelectedContact);

// Clear form
Name = string.Empty;
Email = string.Empty;
Phone = string.Empty;
SelectedContact = null;
}
}
}

3. Create the View

xml
<Window x:Class="MVVMSample.Views.ContactsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewmodels="clr-namespace:MVVMSample.ViewModels"
Title="Contact Manager" Height="450" Width="800">

<Window.DataContext>
<viewmodels:ContactViewModel />
</Window.DataContext>

<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>

<!-- Contact List -->
<ListBox Grid.Column="0"
ItemsSource="{Binding Contacts}"
SelectedItem="{Binding SelectedContact}"
DisplayMemberPath="Name"
Margin="10" />

<!-- Contact Details -->
<StackPanel Grid.Column="1" Margin="10">
<TextBlock Text="Name:" />
<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10" />

<TextBlock Text="Email:" />
<TextBox Text="{Binding Email, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10" />

<TextBlock Text="Phone:" />
<TextBox Text="{Binding Phone, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,20" />

<StackPanel Orientation="Horizontal">
<Button Content="Add" Command="{Binding AddCommand}" Width="80" Margin="0,0,10,0" />
<Button Content="Update" Command="{Binding UpdateCommand}" Width="80" Margin="0,0,10,0" />
<Button Content="Delete" Command="{Binding DeleteCommand}" Width="80" />
</StackPanel>
</StackPanel>
</Grid>
</Window>

4. Connect Everything in the Application

csharp
// App.xaml.cs
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);

var mainWindow = new ContactsView();
mainWindow.Show();
}
}

MVVM Frameworks and Libraries

While you can implement MVVM manually as shown above, there are several frameworks that simplify the process:

  1. Microsoft MVVM Toolkit: A lightweight, modular MVVM library from Microsoft

    bash
    dotnet add package Microsoft.Toolkit.Mvvm
  2. Prism: A comprehensive framework for building loosely coupled applications

    bash
    dotnet add package Prism.WPF
  3. Caliburn.Micro: A small but powerful framework with convention-based binding

    bash
    dotnet add package Caliburn.Micro
  4. ReactiveUI: An MVVM framework that integrates with reactive programming

    bash
    dotnet add package ReactiveUI.WPF

Using the Microsoft MVVM Toolkit simplifies our earlier examples:

csharp
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;

public class ContactViewModel : ObservableObject
{
private readonly ContactRepository _repository;

public ContactViewModel()
{
_repository = new ContactRepository();
LoadContacts();

AddCommand = new RelayCommand(AddContact);
UpdateCommand = new RelayCommand(UpdateContact, CanUpdateContact);
DeleteCommand = new RelayCommand(DeleteContact, CanDeleteContact);
}

// Properties and commands implementation...
}

Best Practices for MVVM

To get the most out of MVVM, follow these best practices:

  1. Keep the View code-behind minimal: Avoid business logic in the View's code-behind. Use data binding and commands instead.

  2. Use ViewModelLocator: Implement a ViewModelLocator to manage ViewModel instances and their lifecycle.

  3. Implement INotifyPropertyChanged properly: Ensure all properties that the UI binds to implement property change notification.

  4. Use commands for user interactions: Avoid event handlers in code-behind; use commands instead.

  5. Avoid direct references from ViewModel to View: The ViewModel shouldn't know about the specific View that's using it.

  6. Use data templates to style data: Define look and feel in XAML, not in code.

  7. Unit test your ViewModels: One major benefit of MVVM is testability – take advantage of it.

Common Challenges and Solutions

Challenge 1: Dialog Services

ViewModels shouldn't directly open dialogs, as that's a UI concern. Instead, implement a dialog service:

csharp
// Interface
public interface IDialogService
{
bool ShowConfirmation(string message, string title);
void ShowMessage(string message, string title);
}

// Implementation
public class DialogService : IDialogService
{
public bool ShowConfirmation(string message, string title)
{
return MessageBox.Show(message, title, MessageBoxButton.YesNo) == MessageBoxResult.Yes;
}

public void ShowMessage(string message, string title)
{
MessageBox.Show(message, title);
}
}

// In ViewModel constructor
public ContactViewModel(IDialogService dialogService)
{
_dialogService = dialogService;
}

// Using the service
private void DeleteContact()
{
if (_dialogService.ShowConfirmation("Are you sure?", "Delete Contact"))
{
// Delete logic
}
}

Challenge 2: Navigation Between Views

For navigation, implement a navigation service:

csharp
public interface INavigationService
{
void NavigateTo<T>() where T : ViewModel;
void GoBack();
}

// Usage in ViewModel
public RelayCommand NavigateToDetailsCommand => new RelayCommand(() =>
{
_navigationService.NavigateTo<ContactDetailsViewModel>();
});

Summary

The MVVM pattern is a powerful approach to building .NET desktop applications, particularly for WPF. It separates concerns, making your code more maintainable and testable. The key components are:

  • Model: Represents your data and business logic
  • View: Defines the UI appearance
  • ViewModel: Acts as a bridge between Model and View

MVVM leverages data binding, commands, and property change notification to create a loosely coupled architecture. While it has a steeper learning curve than some patterns, the benefits in code organization, testability, and maintainability make it worthwhile for complex applications.

Additional Resources

Exercises

  1. Create a simple To-Do list application using MVVM with the following features:

    • Add a new task with title and priority
    • Mark tasks as complete
    • Delete tasks
    • Filter tasks by completion status
  2. Extend the contacts application from the examples to include:

    • Contact categories (friend, family, work)
    • Contact search functionality
    • Contact import/export to CSV
  3. Refactor an existing WPF application to follow MVVM principles, identifying where View, ViewModel, and Model responsibilities should be separated.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)