WPF: MVVM pattern and toolkit
The MVVM pattern [1] helps cleanly separate an application’s business and presentation logic from its user interface (UI).
-
Maintaining a clean separation between application logic and the UI helps address numerous development issues and makes an application easier to test, maintain, and evolve.
-
It can also significantly improve code re-use opportunities and allows developers and UI designers to collaborate more easily when developing their respective parts of an app.
1. MVVM pattern
There are three core components in the MVVM pattern: the model, the view, and the view model.
Using the MVVM pattern, the UI of the app and the underlying presentation and business logic are separated into three separate classes: the view, which encapsulates the UI and UI logic; the view model, which encapsulates presentation logic and state; and the model, which encapsulates the app’s business logic and data.
1.1. View
The view is responsible for defining the structure, layout, and appearance of what the user sees on screen.
-
Ideally, each view is defined in XAML, with a limited code-behind that does not contain business logic.
However, in some cases, the code-behind might contain UI logic that implements visual behavior that is difficult to express in XAML, such as animations.
-
Enable and disable UI elements by binding to view model properties, rather than enabling and disabling them in code-behind.
Ensure that the view models are responsible for defining logical state changes that affect some aspects of the view’s display, such as whether a command is available, or an indication that an operation is pending.
-
There are several options for executing code on the view model in response to interactions on the view, such as a button click or item selection.
-
If a control supports commands, the control’s
Command
property can be data-bound to anICommand
property on the view model.When the control’s command is invoked, the code in the view model will be executed.
-
In addition to commands, behaviors can be attached to an object in the view and can listen for either a command to be invoked or the event to be raised.
In response, the behavior can then invoke an ICommand on the view model or a method on the view model.
-
1.2. ViewModel
The view model implements properties and commands to which the view can data bind to, and notifies the view of any state changes through change notification events. The properties and commands that the view model provides define the functionality to be offered by the UI, but the view determines how that functionality is to be displayed.
-
Multi-platform apps should keep the UI thread unblocked to improve the user’s perception of performance.
Therefore, in the view model, use asynchronous methods for I/O operations and raise events to asynchronously notify views of property changes.
-
The view model is also responsible for coordinating the view’s interactions with one or many model classes that are required.
The view model might choose to expose model classes directly to the view so that controls in the view can data bind directly to them. In this case, the model classes will need to be designed to support data binding and change notification events.
-
Each view model provides data from a model in a form that the view can easily consume.
-
Placing the data conversion in the view model is a good idea because it provides properties that the view can bind to. For example, the view model might combine the values of two properties to make it easier to display by the view.
-
It’s also possible to use converters as a separate data conversion layer that sits between the view model and the view. This can be necessary, for example, when data requires special formatting that the view model doesn’t provide.
-
-
In order for the view model to participate in two-way data binding with the view, its properties must raise the
PropertyChanged
event.-
View models satisfy this requirement by implementing the
INotifyPropertyChanged
interface, and raising thePropertyChanged
event when a property is changed. -
For collections, the view-friendly
ObservableCollection<T>
is provided.This collection implements collection changed notification, relieving the developer from having to implement the
INotifyCollectionChanged
interface on collections.
-
1.3. Model
Model classes are non-visual classes that encapsulate the app’s data.
-
Model classes can be thought of as representing the app’s domain model that includes a data model along with business and validation logic.
-
Examples of model objects include data transfer objects (DTOs), Plain Old CLR Objects (POCOs), and generated entity and proxy objects.
-
Model classes are typically used in conjunction with services or repositories that encapsulate data access and caching.
1.4. Connecting view models to views
View models can be connected to views by using the data-binding capabilities.
1.4.1. Creating a view model declaratively
The simplest approach is for the view to declaratively instantiate its corresponding view model in XAML. When the view is constructed, the corresponding view model object will also be constructed.
<Grid.DataContext>
<vm:MainViewModel />
</Grid.DataContext>
Though the declarative construction and assignment of the view model by the view has the advantage that it’s simple, but has the disadvantage that it requires a default (parameter-less) constructor in the view model.
1.4.2. Creating a view model programmatically
A view can have code in the code-behind file, resulting in the view-model being assigned to its DataContext
property.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}
The programmatic construction and assignment of the view model within the view’s code-behind has the advantage that it’s simple.
-
However, the main disadvantage of this approach is that the view needs to provide the view model with any required dependencies.
-
Using a dependency injection container can help to maintain loose coupling between the view and view model.
1.5. Updating views in response to changes in the underlying view model or model
All view model and model classes that are accessible to a view should implement the INotifyPropertyChanged
interface. Implementing this interface in a view model or model class allows the class to provide change notifications to any data-bound controls in the view when the underlying property value changes.
-
Always raising a
PropertyChanged
event if a public property’s value changes. -
Always raising a
PropertyChanged
event for any calculated properties whose values are used by other properties in the view model or model. -
Always raising the
PropertyChanged
event at the end of the method that makes a property change, or when the object is known to be in a safe state. -
Never raising a
PropertyChanged
event if the property does not change. -
Never raising the
PropertyChanged
event during a view model’s constructor if you are initializing a property. -
Never raising more than one
PropertyChanged
event with the same property name argument within a single synchronous invocation of a public method of a class.
public sealed class MainViewModel : INotifyPropertyChanged
{
private string? _title;
public string? Title
{
get { return _title; }
set
{
if (_title != value)
{
_title = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
2. MVVM Toolkit
The CommunityToolkit.Mvvm
package (aka MVVM Toolkit, formerly named Microsoft.Toolkit.Mvvm
) is a modern, fast, and modular MVVM library. [2]
dotnet add package CommunityToolkit.Mvvm # --version 8.3.1
-
CommunityToolkit.Mvvm.ComponentModel
-
public abstract class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging
public sealed class MainViewModel : ObservableObject { private string? _title; public string? Title { get => _title; set => SetProperty(ref _title, value); } private TaskNotifier? myTask; public Task? MyTask { get => myTask; set => SetPropertyAndNotifyOnCompletion(ref myTask, value); } }
<StackPanel> <Label Content="{Binding MyTask.Status}" /> <TextBox Text="{Binding Title}" /> </StackPanel>
-
public abstract class ObservableRecipient : ObservableObject
public sealed class MainViewModel : ObservableRecipient, IRecipient<RequestMessage<int>> { public void Receive(RequestMessage<int> message) => throw new NotImplementedException(); }
public sealed class MainViewModel : ObservableRecipient { // For best results and to avoid memory leaks, it's recommended to use OnActivated to register // to messages, and to use OnDeactivated to do cleanup operations. protected override void OnActivated() => Messenger.Register<MainViewModel, RequestMessage<int>>(this, (r, m) => r.Receive(m)); // By default, OnDeactivated automatically unregisters the current instance from all registered messages. protected override void OnDeactivated() => base.OnDeactivated(); public void Receive(RequestMessage<int> message) => throw new NotImplementedException(); }
-
public abstract class ObservableValidator : ObservableObject, INotifyDataErrorInfo
<TextBox Text="{Binding Name, ValidatesOnDataErrors=True}" />
-
[ObservableProperty] [NotifyPropertyChangedFor(nameof(FullName))] // Notifying dependent properties [NotifyCanExecuteChangedFor(nameof(MyCommand))] // Notifying dependent commands [NotifyDataErrorInfo] // Requesting property validation [Required] [MinLength(2)] // Any other validation attributes too... [NotifyPropertyChangedRecipients] // Sending notification messages [PropertyChangedMessage<T>] [property: JsonPropertyName("name")] // Adding custom attributes private string? _name;
-
// only use in cases where the target types cannot just inherit from the equivalent // types (eg. from ObservableObject). [INotifyPropertyChanged] public partial class MyViewModel : SomeOtherType
-
-
CommunityToolkit.Mvvm.DependencyInjection
-
dotnet add package Microsoft.Extensions.DependencyInjection # --version 8.0.0
public partial class App : Application { public IServiceProvider Services { get; set; } public App() { Services = ConfigureServices(); } protected override void OnStartup(StartupEventArgs e) { var mainWindow = Services.GetRequiredService<MainWindow>(); mainWindow.Show(); } private ServiceProvider ConfigureServices() { var services = new ServiceCollection(); services.AddTransient<MainWindow>(); services.AddTransient<MainViewModel>(); return services.BuildServiceProvider(); } }
-
-
-
public sealed class MainViewModel : ObservableObject { public IRelayCommand OKCommand { get; } = new RelayCommand(() => { }, () => true); public IAsyncRelayCommand CancelCommand { get; } = new AsyncRelayCommand(() => Task.CompletedTask); }
-
[RelayCommand(CanExecute = nameof(CanGreetUser))] private void GreetUser(User? user) { Console.WriteLine($"Hello {user!.Name}!"); } private bool CanGreetUser(User? user) { return user is not null; } [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(GreetUserCommand))] private User? selectedUser;
// Call IAsyncRelayCommand.Cancel to signal that token. [RelayCommand(IncludeCancelCommand = true)] private async Task DoWorkAsync(CancellationToken token) { // Do some long running work... }