.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:
- Model: Represents the application data and business logic
- View: Defines the UI structure and appearance
- 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.
// 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.
<!-- 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.
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.
<TextBox Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" />
In this example:
Text
is the target property on the TextBoxFirstName
is the source property on the ViewModelUpdateSourceTrigger=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.
// 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:
public RelayCommand SaveCommand { get; private set; }
public PersonViewModel()
{
SaveCommand = new RelayCommand(Save, CanSave);
}
And bind it in the View:
<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.
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
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
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
<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
// 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:
-
Microsoft MVVM Toolkit: A lightweight, modular MVVM library from Microsoft
bashdotnet add package Microsoft.Toolkit.Mvvm
-
Prism: A comprehensive framework for building loosely coupled applications
bashdotnet add package Prism.WPF
-
Caliburn.Micro: A small but powerful framework with convention-based binding
bashdotnet add package Caliburn.Micro
-
ReactiveUI: An MVVM framework that integrates with reactive programming
bashdotnet add package ReactiveUI.WPF
Using the Microsoft MVVM Toolkit simplifies our earlier examples:
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:
-
Keep the View code-behind minimal: Avoid business logic in the View's code-behind. Use data binding and commands instead.
-
Use ViewModelLocator: Implement a ViewModelLocator to manage ViewModel instances and their lifecycle.
-
Implement INotifyPropertyChanged properly: Ensure all properties that the UI binds to implement property change notification.
-
Use commands for user interactions: Avoid event handlers in code-behind; use commands instead.
-
Avoid direct references from ViewModel to View: The ViewModel shouldn't know about the specific View that's using it.
-
Use data templates to style data: Define look and feel in XAML, not in code.
-
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:
// 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:
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
- Microsoft MVVM Toolkit Documentation
- WPF Data Binding Documentation
- Prism Library Documentation
- MVVM Pattern on Microsoft Docs
Exercises
-
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
-
Extend the contacts application from the examples to include:
- Contact categories (friend, family, work)
- Contact search functionality
- Contact import/export to CSV
-
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! :)