The MVVM Pattern

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Model-View-ViewModel (MVVM) is the architectural pattern that XAML platforms were designed around. It splits a screen into three roles: a view that draws the user interface, a view model that holds the state and the actions of that interface, and a model that holds the application’s data and rules. The roles are connected by data binding, commands and change notifications instead of by code that reaches into controls. This page explains the pattern, the decisions you take when you apply it in a desktop app (how views get their view models, where event handlers go, what stays in code-behind) and when you should not use it. It was written against .NET 10 LTS (latest patch 10.0.13) with C# 14, Visual Studio 2026 18.11.1, Windows App SDK 2.5.1 and CommunityToolkit.Mvvm 8.4.2 on Windows 10 and Windows 11. .NET 11 is at release candidate 2 (11.0.0-rc.2) and general availability is expected at .NET Conf, 10 to 13 November 2026; MVVM is a design pattern and nothing in it depends on the runtime version. WPF is the stack in focus, and a comparison table with WinUI 3 and Windows Forms follows later.

The running scenario is Bookshelf Desktop, with a BooksViewModel, a BookViewModel, a BookEditorViewModel, an IBookRepository, an IDialogService and an INavigationService. The pattern is shown here with the toolkit’s source generators in the examples; the hand-written plumbing they replace is on MVVM from Scratch, the toolkit itself on MVVM Toolkit (CommunityToolkit.Mvvm), messages between view models on Messaging Between View Models, the container on Dependency Injection and the Generic Host, navigation and dialog services on Navigation, Dialogs & Services in MVVM, and testing on Testing View Models & UI. The binding engine that makes the pattern work is covered on Data Binding, and commands on Commands.

Why a pattern at all

A new WPF or WinUI project starts with a window and a code-behind file. Typing a handler for each button’s Click event is the quickest way to get a result, and it is how Windows Forms works. The cost arrives as the app grows. Microsoft’s data binding guidance for Windows apps describes the symptom: handlers in files such as MainWindow.xaml.cs are tightly coupled to the controls, they manipulate the UI directly, replacing a control means rewriting the handler, and the code-behind files accumulate code that is not related to the UI, for example database access that is then duplicated for other windows.

MVVM addresses this by moving everything that is not drawing into classes that do not know about controls. The benefits that Microsoft’s sources list are the same in the MAUI enterprise book and in the Windows data binding article:

Benefit What it means in a desktop app

Testability

A view model can be created in a unit test, its commands executed and its properties asserted, without starting a UI thread or showing a window. Both Microsoft sources name this as a primary benefit.

Separation of concerns

Data access, presentation logic and layout live in different classes, so a change to one is less likely to cause regressions in another.

Replaceable UI

The app can be redesigned in XAML without touching the view model, provided the view is implemented in XAML or in code that only reads the view model. A new version of the view works with the existing view model.

Parallel work

Designers can work on views and developers on view models and models at the same time, as long as they agree on the members the view model exposes.

Reuse

A view model that does not reference controls can be hosted by another UI (for example the WPF and the WinUI 3 versions of Bookshelf Desktop can share one view-model library).

None of this is free. MVVM adds classes, indirection and a few rules, and the Windows data binding article says so plainly: it recommends MVVM for apps with complex data flows, multiple screens or team-developed code bases, and it says that code-behind is acceptable for simple single-page tools or prototypes, adopting MVVM incrementally as complexity grows. The section on when MVVM is overkill returns to this.

The three roles

The pattern has three components and one rule about who may know whom. In the words of the MAUI enterprise book, the view knows about the view model and the view model knows about the model, but the model is unaware of the view model and the view model is unaware of the view. The view model therefore isolates the view from the model, and the model can evolve independently of the view.

MVVM layers in Bookshelf Desktop: the view depends on the view model through a DataContext, the view model depends on the model and on service interfaces, and binding, commands and change notifications are the glue

Model

The model is the non-visual part of the application: the domain types, their validation and business rules, and the services or repositories that load and store them. The MAUI book describes model classes as non-visual classes that encapsulate the app’s data: DTOs, POCOs and generated entity and proxy objects, usually used together with services or repositories that encapsulate data access and caching. The Windows data binding article adds that the model layer is completely independent of the view and view-model layers and often resides partially in the cloud, which lets several client apps (a Windows App SDK app and a web app, say) work with the same underlying data.

In Bookshelf Desktop the model is the Book record and the IBookRepository abstraction, in a Bookshelf.Core library that references neither WPF nor WinUI:

namespace Bookshelf.Core;

public sealed record Book(
    int Id,
    string Title,
    string Author,
    int Year = 0,
    decimal Price = 0m,
    string? Isbn = null);

public interface IBookRepository
{
    Task<IReadOnlyList<Book>> GetAllAsync(CancellationToken cancellationToken);

    Task<Book?> GetByIdAsync(int id, CancellationToken cancellationToken);

    Task<Book> AddAsync(Book book, CancellationToken cancellationToken);

    Task UpdateAsync(Book book, CancellationToken cancellationToken);

    Task DeleteAsync(int id, CancellationToken cancellationToken);
}

The Book record is immutable, which has a consequence for the view model: the UI cannot bind two-way to a record’s properties. Editing therefore happens on a separate BookEditorViewModel that is created from a Book and produces a new one; this design is used on the other MVVM pages as well. It is a modelling choice of this site’s sample, not a requirement of MVVM, and the MAUI book explicitly allows the opposite: a view model may expose model classes directly if they are designed to support data binding and change notification.

ViewModel

The view model is the model of the view. It exposes the data the screen needs in a form the screen can bind to, it exposes the actions the screen offers as commands, and it tells the view about changes. The MAUI book summarizes the split: 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 displayed. Four duties follow from that:

  • Expose state as properties. The view model also holds state that is relevant to the UI but not to the model, such as the selected item, the search text or whether an operation is pending. The Windows data binding article calls this out explicitly.

  • Expose actions as ICommand members, and decide when each is available (CanExecute), so that the view never enables or disables controls in code.

  • Adapt the model for display: combine two values into one, format a value, wrap an immutable record in an observable object. The book recommends putting this conversion in the view model, and using value converters in the view only for special formatting the view model does not provide.

  • Coordinate services: call the repository, ask the dialog service for a confirmation, request navigation. The view model is also the integration point with data access code.

A view model must also stay free of view types. The MAUI book states the rule as a tip: do not reference view types such as Button and ListView from view models, so that they can be tested in isolation. In WPF this includes Window, MessageBox, Dispatcher (use a service or await), Application.Current and FrameworkElement. The Bookshelf BooksViewModel is built from the canonical shapes used across the section:

using System.Collections.ObjectModel;
using Bookshelf.Core;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace Bookshelf.Desktop.ViewModels;

public sealed partial class BookViewModel(Book book) : ObservableObject
{
    public Book Model => book;

    public int Id => book.Id;

    public string Title => book.Title;

    public string Author => book.Author;

    public int Year => book.Year;

    public decimal Price => book.Price;

    [ObservableProperty]
    public partial bool IsFavorite { get; set; }
}

public sealed partial class BooksViewModel(IBookRepository repository, IDialogService dialogs)
    : ObservableObject
{
    private IReadOnlyList<Book> _all = [];

    public ObservableCollection<BookViewModel> Books { get; } = [];

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(EditSelectedCommand))]
    [NotifyCanExecuteChangedFor(nameof(DeleteSelectedCommand))]
    public partial BookViewModel? SelectedBook { get; set; }

    [ObservableProperty]
    public partial string? SearchText { get; set; }

    [ObservableProperty]
    public partial string? StatusMessage { get; set; }

    partial void OnSearchTextChanged(string? value) => ApplyFilter();

    [RelayCommand(IncludeCancelCommand = true)]
    private async Task LoadAsync(CancellationToken cancellationToken)
    {
        StatusMessage = "Loading books...";
        _all = await repository.GetAllAsync(cancellationToken);
        ApplyFilter();
        StatusMessage = $"{_all.Count} books";
    }

    [RelayCommand]
    private async Task AddBookAsync()
    {
        Book? created = await dialogs.EditBookAsync(null);
        if (created is null)
        {
            return;
        }

        Book saved = await repository.AddAsync(created, CancellationToken.None);
        _all = [.. _all, saved];
        ApplyFilter();
    }

    [RelayCommand(CanExecute = nameof(HasSelection))]
    private async Task EditSelectedAsync()
    {
        Book? edited = await dialogs.EditBookAsync(SelectedBook?.Model);
        if (edited is null)
        {
            return;
        }

        await repository.UpdateAsync(edited, CancellationToken.None);
        _all = [.. _all.Select(b => b.Id == edited.Id ? edited : b)];
        ApplyFilter();
    }

    [RelayCommand(CanExecute = nameof(HasSelection))]
    private async Task DeleteSelectedAsync()
    {
        if (SelectedBook is not { } selected
            || !await dialogs.ConfirmAsync("Delete book", $"Delete \"{selected.Title}\"?"))
        {
            return;
        }

        await repository.DeleteAsync(selected.Id, CancellationToken.None);
        _all = [.. _all.Where(b => b.Id != selected.Id)];
        ApplyFilter();
    }

    [RelayCommand]
    private void ToggleFavorite(BookViewModel? book)
    {
        if (book is not null)
        {
            book.IsFavorite = !book.IsFavorite;
        }
    }

    private bool HasSelection() => SelectedBook is not null;

    private void ApplyFilter()
    {
        string text = SearchText?.Trim() ?? string.Empty;
        IEnumerable<Book> matches = text.Length == 0
            ? _all
            : _all.Where(b => b.Title.Contains(text, StringComparison.CurrentCultureIgnoreCase)
                || b.Author.Contains(text, StringComparison.CurrentCultureIgnoreCase));

        Books.Clear();
        foreach (Book book in matches)
        {
            Books.Add(new BookViewModel(book));
        }
    }
}

The class has no reference to a control. IDialogService is an interface defined in Bookshelf.Core (its implementation shows a Window and lives in the UI project), which is why the view model can ask for a confirmation and still be tested with a fake. The toolkit generates the INotifyPropertyChanged plumbing and the LoadCommand, LoadCancelCommand, AddBookCommand, EditSelectedCommand, DeleteSelectedCommand and ToggleFavoriteCommand members; the generator behaviour is explained on MVVM Toolkit (CommunityToolkit.Mvvm). The partial-property form of [ObservableProperty] was added in CommunityToolkit.Mvvm 8.4.0 and needs the C# 14 compiler from Visual Studio 2026 or the .NET 10 SDK. The Learn page for the attribute still documents field-based declarations, so the examples here follow the 8.4.x release notes.

View

The view defines the structure, layout and appearance of what the user sees. The MAUI book says ideally each view is defined in XAML with limited code-behind that contains no business logic, and the Windows data binding article describes the view layer as XAML with data binding expressions that connect UI components to view-model members. In WPF a view is a Window, a Page, a UserControl, or a DataTemplate that renders a view-model type (the MAUI book notes that a data template as a view has no code-behind and is designed to bind to a specific view model type).

The Bookshelf main view binds to the view model above. It declares no event handlers:

<UserControl x:Class="Bookshelf.Desktop.Views.BooksView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vm="clr-namespace:Bookshelf.Desktop.ViewModels"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d"
             d:DataContext="{d:DesignInstance Type=vm:BooksViewModel}">
    <DockPanel Margin="12">
        <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,8">
            <TextBox Width="240"
                     Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" />
            <Button Content="Add" Margin="8,0,0,0" Command="{Binding AddBookCommand}" />
            <Button Content="Edit" Margin="8,0,0,0" Command="{Binding EditSelectedCommand}" />
            <Button Content="Delete" Margin="8,0,0,0" Command="{Binding DeleteSelectedCommand}" />
        </StackPanel>

        <TextBlock DockPanel.Dock="Bottom" Margin="0,8,0,0" Text="{Binding StatusMessage}" />

        <ListView ItemsSource="{Binding Books}" SelectedItem="{Binding SelectedBook}">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Title" DisplayMemberBinding="{Binding Title}" />
                    <GridViewColumn Header="Author" DisplayMemberBinding="{Binding Author}" />
                    <GridViewColumn Header="Year" DisplayMemberBinding="{Binding Year}" />
                </GridView>
            </ListView.View>
        </ListView>
    </DockPanel>
</UserControl>

Nothing in this XAML tells the view model how to load, edit or delete. The Edit and Delete buttons are disabled automatically when no row is selected, because the commands report CanExecute as false; no code ever calls IsEnabled. The MAUI book gives the same advice as a tip: enable and disable elements by binding to view-model state, not in code-behind. The d:DataContext line is a design-time hint for the Visual Studio XAML designer and for binding tooling (it does not set the runtime DataContext); it needs the d and mc namespaces shown. Each binding is resolved by the engine described on Data Binding: it reads DataContext from the element or the nearest ancestor that sets it.

The glue: binding, commands and notifications

MVVM is possible only because the platform offers three mechanisms that let two objects cooperate without knowing each other’s types. Each is a small interface or markup feature:

Mechanism What it does Where you meet it

Data binding

Connects a target property of an element to a source property of the DataContext, in one or two directions, and converts values on the way.

{Binding Title} in WPF; {Binding} or {x:Bind} in WinUI 3; Binding objects in WinForms.

Change notification

The source tells the binding engine that a value changed, so the UI refreshes. INotifyPropertyChanged covers properties, INotifyCollectionChanged covers collections, INotifyDataErrorInfo covers validation errors.

ObservableObject (toolkit), ObservableCollection<T>, ObservableValidator.

Commands

An ICommand object represents an action plus its availability. A command source (a button, a menu item, a key binding) executes it and listens to CanExecuteChanged.

{Binding AddBookCommand} on Button.Command, MenuItem.Command, KeyBinding.Command.

The next diagram shows how the interfaces relate to the Bookshelf classes. The toolkit’s ObservableObject implements INotifyPropertyChanged, and the generated commands are RelayCommand and AsyncRelayCommand objects that implement ICommand.

classDiagram direction LR class INotifyPropertyChanged { <<interface>> +PropertyChanged event } class ICommand { <<interface>> +CanExecute(object) bool +Execute(object) void +CanExecuteChanged event } class IBookRepository { <<interface>> +GetAllAsync() Task } class ObservableObject class RelayCommand class AsyncRelayCommand class BooksViewModel { +Books ObservableCollection +SelectedBook BookViewModel +LoadCommand ICommand +AddBookCommand ICommand } class BooksView { +DataContext object } ObservableObject ..|> INotifyPropertyChanged BooksViewModel --|> ObservableObject RelayCommand ..|> ICommand AsyncRelayCommand ..|> ICommand BooksViewModel *-- AsyncRelayCommand : exposes BooksViewModel --> IBookRepository : uses BooksView ..> BooksViewModel : binds to

When a view-model property changes, the generated setter raises PropertyChanged; the binding engine, which subscribed when the binding was created, reads the new value and updates the target. When a control invokes a command, the view model code runs. The view never holds a reference to the view model’s type, only to an object assigned to DataContext, and the view model never holds a reference to the view at all. That asymmetry is the pattern.

The notification rules the MAUI book lists apply to hand-written and generated view models alike. Always raise PropertyChanged when a public property’s value changes and do not assume it can be skipped. Raise it for calculated properties that depend on others. Raise it when the object is in a safe state, because handlers run synchronously. Never raise it if the value did not change, which means comparing old and new values (the toolkit’s SetProperty and the generated setters do this). Do not raise it from the constructor while initializing, because no binding has subscribed yet. Raise it once per property per synchronous call.

Connecting a view to its view model

All approaches have the same aim, in the MAUI book’s words: the view must end up with a view model assigned to its data context (BindingContext in MAUI, DataContext in WPF and WinUI 3). The book groups the approaches into two families, which apply to WPF too even though the book describes MAUI.

View-first and ViewModel-first

View-first ViewModel-first

Conceptual model

The app is composed of views, and each view connects to the view model it depends on.

The app is composed of view models, and a service finds the view for a view model.

Who creates whom

The view (or the container that builds the view) creates or receives its view model.

A view model (or a navigation service) is created first and a view is selected for it, usually by a DataTemplate.

Strengths

View models do not depend on views, so apps are loosely coupled and testable. The structure of the app can be understood by following its visual structure. It matches platforms whose navigation constructs pages itself.

View creation can be abstracted away. View models can create other view models. Navigation can be tested without any UI.

Weaknesses

The view needs a way to obtain dependencies. View-model-to-view-model navigation needs a mediator.

The book calls it often complex, and it can be difficult to follow how parts are created and associated.

Typical Bookshelf use

The main window is constructed by the host and receives BooksViewModel.

Navigation between BooksViewModel, a statistics view model and a settings view model with implicit DataTemplates.

The MAUI book says the choice between the two is a matter of preference and complexity, and it recommends view-first for MAUI because it aligns with MAUI’s navigation system, which constructs pages itself. WPF has no such constraint: a ContentControl plus implicit DataTemplates makes ViewModel-first natural, while Window instances are still created view-first. Real WPF apps typically mix them: the main window is view-first (constructed by the host and given its view model), and the content area inside it is ViewModel-first. The navigation service patterns are on Navigation, Dialogs & Services in MVVM; neither WPF nor the MVVM toolkit ships a ViewModel-first navigation API, so those are user patterns.

DataContext assignment strategies

DataContext is an inheritable dependency property, so assigning it on the root of a view makes every descendant bind to it unless a nearer element overrides it. There are several places to assign it:

Strategy How Trade-offs

Declarative in XAML

<Window.DataContext><vm:BooksViewModel /></Window.DataContext>

Simple. Needs a public parameterless constructor, so it cannot receive the repository from a container. The MAUI book names this limitation. Fine for design-time data or tiny views.

Code-behind, new

DataContext = new BooksViewModel(repository, dialogs); in the constructor

Simple. The view must know how to obtain every dependency, which couples it to services. The MAUI book names this as the disadvantage.

Constructor injection of the view model into the view

The host’s container constructs the view and passes the view model: public MainWindow(BooksViewModel viewModel) sets DataContext = viewModel.

Keeps the view free of service knowledge and works with the Generic Host. The recommended default for this site’s examples. One line of code-behind per view.

Implicit DataTemplate

A ContentControl has the view model as Content, and a DataTemplate keyed only by DataType selects the view; the template’s content inherits the view model as DataContext.

No assignment code at all. Ideal for ViewModel-first navigation and for views nested in lists. Covered in the next section.

ViewModel locator

A resource or markup extension that resolves a view model for a view by convention, such as DataContext="{Binding Source={StaticResource Locator}, Path=Books}".

Popular in older frameworks (MvvmLight shipped one). It is a third-party or user pattern; the MVVM toolkit has none and neither does WPF. It hides the container behind static state and is largely replaced by constructor injection.

Bound from a parent

A parent view model exposes a child view model property and the child view binds to it: DataContext="{Binding Details}".

Natural for composite screens. Make sure the child is created by the parent’s container-resolved dependencies.

The Bookshelf main window uses constructor injection. The Generic Host that builds it is on Dependency Injection & the Generic Host; here only the view side matters:

using Bookshelf.Desktop.ViewModels;

namespace Bookshelf.Desktop;

public partial class MainWindow : System.Windows.Window
{
    public MainWindow(BooksViewModel viewModel)
    {
        InitializeComponent();
        DataContext = viewModel;
        Loaded += async (_, _) => await viewModel.LoadCommand.ExecuteAsync(null);
    }
}

The constructor assigns the data context and starts loading when the window is shown. Starting a command from Loaded is acceptable code-behind: it is a UI lifecycle hook, and the actual work is in the view model. (An alternative with no code-behind at all is a behavior; see below.) Note that LoadCommand is declared by the generator as IAsyncRelayCommand, which is why ExecuteAsync is available here.

The WinUI 3 version of the same idea, from the Learn article, exposes the view model as a property of the page so that {x:Bind} can compile against its type:

using Bookshelf.Desktop.ViewModels;
using Microsoft.UI.Xaml.Controls;

namespace Bookshelf.WinUI.Views;

public sealed partial class BooksPage : Page
{
    public BooksViewModel ViewModel { get; }

    public BooksPage()
    {
        ViewModel = App.GetService<BooksViewModel>();
        InitializeComponent();
    }
}

App.GetService<T>() is a helper that the WinUI template guidance builds on top of Microsoft.Extensions.DependencyInjection; it is not a framework API, and the Learn article points to its "Architecture patterns for WinUI 3 desktop apps" page for how to define it.

Implicit DataTemplates as views

A DataTemplate with a DataType and no x:Key is applied automatically to any object of that type that WPF has to display in a content presenter. That makes the template a view for the view model, with no code-behind and no DataContext assignment, because the data context of a template’s content is the data object itself. The pattern is the foundation of ViewModel-first composition in WPF:

<Window x:Class="Bookshelf.Desktop.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:Bookshelf.Desktop.ViewModels"
        xmlns:views="clr-namespace:Bookshelf.Desktop.Views"
        Title="Bookshelf Desktop" Width="900" Height="600">
    <Window.Resources>
        <DataTemplate DataType="{x:Type vm:BooksViewModel}">
            <views:BooksView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:SettingsViewModel}">
            <views:SettingsView />
        </DataTemplate>
    </Window.Resources>

    <ContentControl Content="{Binding CurrentPage}" />
</Window>

When the main view model sets CurrentPage to a BooksViewModel, WPF finds the template whose DataType matches and instantiates BooksView, whose DataContext becomes the view model. Setting it to a SettingsViewModel swaps the view. SettingsViewModel and the shell view model that owns CurrentPage are defined on Navigation, Dialogs & Services in MVVM. Details of DataType, template lookup, selectors and ItemTemplate are on Converters, Collection Views & Data Templates.

The WPF features that make this work are not available everywhere. WinUI 3 does not resolve a DataTemplate implicitly from DataType in the same way (the property exists for x:Bind typing, not for automatic lookup), and WinForms has no templates. In WinUI 3, ViewModel-first navigation is usually built with a Frame, a navigation service that maps view-model types to page types, or a ContentControl with a DataTemplateSelector. The exact support for implicit data templates in WinUI 3 is not described in one place in the official documentation, so check your Windows App SDK version if you rely on it.

Events and the view model: behaviors and commands

Controls that expose a Command property (Button, MenuItem, Hyperlink, KeyBinding) bind straight to an ICommand. Many events have no command: MouseDoubleClick on a ListView, Loaded on a window, SelectionChanged, TextChanged, Drop, Closing. The pattern gives three options, in this order of preference:

  1. Bind to a state property that captures the intent (bind SelectedItem and react in the view model).

  2. Attach a behavior that listens to the event and invokes a command.

  3. Handle the event in code-behind when it is purely a UI concern, and forward to the view model if needed.

The MAUI book describes the second approach in general terms: behaviors allow controls to be declaratively connected to a command, can be used with a range of events, and can associate command objects or methods with controls that were not designed for commands; in the book’s words this moves event-handling code to view models, where it can be unit tested. For MAUI the book uses EventToCommandBehavior from the .NET MAUI Community Toolkit.

XAML Behaviors for WPF

WPF has no built-in event-to-command facility. The ecosystem answer is the Microsoft.Xaml.Behaviors.Wpf NuGet package, built from the microsoft/XamlBehaviorsWpf repository. It is an open source project (MIT license) hosted in the microsoft GitHub organization; its README says the project works with a committee of Microsoft MVP leaders who guide it and evaluate pull requests. It is not part of .NET or of the Windows SDK and it is not documented on Microsoft Learn, so treat it as a Microsoft open source library that you add as a package, not as a platform feature. Its repository is the source for the trigger and action types used below; WinUI 3 uses a different library for the same idea (the XAML Behaviors SDK for UWP and WinUI, namespace Microsoft.Xaml.Interactivity), which is not covered on this page.

<ListView ItemsSource="{Binding Books}"
          SelectedItem="{Binding SelectedBook}"
          xmlns:i="http://schemas.microsoft.com/xaml/behaviors">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseDoubleClick">
            <i:InvokeCommandAction Command="{Binding EditSelectedCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Title" DisplayMemberBinding="{Binding Title}" />
            <GridViewColumn Header="Author" DisplayMemberBinding="{Binding Author}" />
        </GridView>
    </ListView.View>
</ListView>

A double click on a row now runs EditSelectedCommand. Because the selected row is already in SelectedBook, the command needs no parameter. The same package provides InvokeCommandAction properties to forward the event arguments (for instance to read which row was dropped); check the version you install for the exact property names, because the public README does not document them. The XAML namespace http://schemas.microsoft.com/xaml/behaviors shown above is the one the package defines.

A behavior is also a clean replacement for the Loaded handler in MainWindow above:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="Loaded">
        <i:InvokeCommandAction Command="{Binding LoadCommand}" />
    </i:EventTrigger>
</i:Interaction.Triggers>

Choosing between the options:

Option Good for Cost

Command property binding

Anything with a Command property.

None. Use it whenever the control supports it.

State property binding

Selection, text, scroll position exposed as data, window state.

One property per concern; the view model reacts in a property-changed hook.

Behavior package (InvokeCommandAction)

Events without a command and no UI logic: double click, Loaded, Closing, SelectionChanged.

An extra dependency (not a framework feature), XAML noise, and the event arguments are not typed for the view model.

Hand-written attached behavior

A reusable UI-only reaction: focus a control, scroll to the selected item, select all on focus.

You write and test it once; the view model remains unaware.

Code-behind handler

Pure UI work such as moving focus or showing a tooltip.

The handler must contain no business logic.

What belongs in code-behind

A frequent misunderstanding is that MVVM forbids code-behind. It does not. The Windows data binding article says that code-behind files can be part of the view layer to contain additional code needed to customize or manipulate the UI, or to extract data from event handler arguments before calling a view-model method that does the work. The MAUI book likewise allows UI logic in code-behind for visual behavior that is difficult to express in XAML, such as animations. The test is whether the code needs to know about business rules or can be unit tested without a UI.

Code Where Why

InitializeComponent(), assigning DataContext

Code-behind

Generated and plumbing code. Part of the view.

Moving keyboard focus after a view appears, scrolling a list so the selected row is visible

Code-behind

Pure UI concern. The view model does not know what a scroll position is.

Window chrome: dragging a borderless window, custom title bar buttons

Code-behind

Platform behavior of the shell. A custom control is better when reused.

Drag-and-drop mechanics: read the DataObject, compute a drop target

Code-behind or behavior

Depends on UI types. Pass the result (a file path, an item) to a command.

Animation triggers that depend on layout

XAML triggers or code-behind

Visual only.

Deciding whether a book can be deleted

View model

Business and presentation logic. Test it without a window.

Showing a confirmation

Service behind an interface

Keeps MessageBox and Window out of the view model. See Navigation, Dialogs & Services in MVVM.

Loading data, calling the repository

View model

Testable with a fake IBookRepository.

Enabling and disabling buttons

View model plus binding

Commands report availability, the view reflects it.

Raising dialog results back to the caller

View model or service

Do not let the view set a result that the view model has to read back.

A useful rule: if you can describe the code in terms of controls ("scroll the list", "focus the search box"), it is view code; if you can describe it in terms of the domain ("delete the selected book if the user confirms"), it is view-model code. When a handler needs the view model, it casts DataContext or, better, receives it through the constructor; when the view model needs the view, introduce an interface (a service) instead of a reference, so the dependency stays pointed downwards.

MVVM across the three Windows UI stacks

The pattern is identical in concept on every stack. What changes is how much of the glue the stack supplies:

WPF WinUI 3 Windows Forms

Data binding

Binding markup extension, rich (MultiBinding, RelativeSource, StringFormat, Delay, validation rules).

{Binding} plus the compile-time {x:Bind} (default one-time mode, binds to members of the page or template).

Binding objects and BindingSource in code or the designer; limited markup, no templates.

Data context

DataContext inheritable dependency property.

DataContext on FrameworkElement; {x:Bind} ignores it and binds to the page or template class.

BindingSource.DataSource, set per control or per form.

Change notification

INotifyPropertyChanged, INotifyCollectionChanged.

Same interfaces.

Same interfaces plus IBindingList for list binding.

Commands

ICommand on Command properties of buttons, menu items and input bindings; routed commands too.

ICommand; also XamlUICommand and StandardUICommand.

Command binding on controls such as Button.Command was added in .NET 7 and later releases; older code uses event handlers.

ViewModel-first views

Implicit DataTemplate by DataType.

Frame navigation, ContentControl with selectors; verify template lookup behaviour for your SDK version.

No templates; create user controls manually.

Toolkit support

Full (ObservableObject, [RelayCommand]).

Full.

Usable: the toolkit has no UI dependency; binding to it uses BindingSource.

The Windows Forms row is a summary only; this page does not describe the Windows Forms binding API or the designer workflow. It is notable that the view-model class from earlier in this page would compile unchanged in all three stacks, because it depends on no UI type, which is the practical meaning of "portable view model".

MVVM compared with MVC and MVP, and with other platforms

MVVM belongs to the family of patterns that separate presentation from domain logic. The main difference between the members of the family is how the presentation logic talks to the screen:

MVC MVP MVVM

Presentation logic lives in

The controller, which interprets input and chooses the view.

The presenter, which reads and writes the view through an interface.

The view model, which exposes state and commands.

View to logic

The view sends events to the controller; the controller updates the model and selects a view.

The view forwards events to the presenter, usually by calling it directly.

The view binds to the view model; there is no call from the logic to the view.

Logic to view

The view observes the model, or the controller passes data on.

The presenter calls methods on a view interface to set text, show lists and so on.

The platform’s binding engine copies state to the view after a change notification.

Test seam

Controller with a fake view.

Presenter with a mock view interface.

View model with fake services; no view at all.

Typical home

Web frameworks, Cocoa, ASP.NET MVC.

Windows Forms, Google Web Toolkit, older Android.

WPF, WinUI, MAUI, Avalonia.

MVVM trades the presenter’s explicit view interface for the framework’s binding engine. That is why it needs XAML-like data binding and why it is awkward in Windows Forms, where the bridge is BindingSource and manual wiring. A presenter-style approach is the pragmatic alternative there; it is not described further on this page.

The same idea exists, with different names and mechanisms, on the other platforms this site covers:

  • On Android, the recommended architecture keeps UI state in a ViewModel that exposes a StateFlow and uses unidirectional data flow with a sealed UI state; events flow down from the screen as method calls and state flows up. See Recommended App Architecture (UI, Domain and Data Layers). The view observes the state instead of binding to properties, so there is no ICommand.

  • On Apple platforms, SwiftUI’s observable model types and views that are a function of state play the view-model role, with the framework tracking dependencies. See App Architecture the Apple Way.

  • In Kotlin Multiplatform, the view model can be shared across Android, iOS and desktop clients as common code, which is the closest analogue to a portable Bookshelf view-model library. See Architecture, Shared ViewModels and State.

  • In the Google Web Toolkit, the pattern of the time was MVP: activities as presenters, views behind interfaces and places for navigation. See MVP, Activities & Places. It is the presenter family described in the table above.

The common ground is the same discipline: keep the state and the logic out of the UI toolkit’s types, make the data flow explicit, and test the logic without the UI. The differences are in the mechanism: WPF and WinUI use two-way binding with property-change notification, Android and Compose use unidirectional state, and GWT used explicit presenter calls.

When MVVM is overkill

MVVM is a means, not a goal. Microsoft’s Windows data binding article is direct about scale: use it for apps with complex data flows, multiple screens or team-developed code bases; for simple single-page tools or prototypes code-behind is acceptable, and MVVM can be adopted incrementally. The article also says you can get a lot of benefit just from understanding data binding and separating app logic into layers, using only what the platform provides and without an external framework. Signs that the pattern is more than you need:

  • A single window with a handful of controls and no logic worth testing: a utility, a launcher, a one-off migration tool.

  • A throwaway prototype or a demo, where the goal is to explore the UI.

  • A Windows Forms app whose forms are thin wrappers over a database and where the designer-based data binding is enough.

  • A custom control, where the control’s own properties and templates are the API (a view model would be an extra layer).

  • A team that is new to XAML and binding: start with data binding and code-behind and extract view models when you feel the pain.

Signs that it is worth it: several screens that share state; logic that has bugs and needs tests; a UI that must be redesigned or ported to WinUI 3; multiple developers working on the same screens; or the need to unit test commands and CanExecute rules. A reasonable path for Bookshelf Desktop is: code-behind for the first prototype; one view model with an observable collection once the list needs search and selection; commands once buttons need enabling rules; services and a host once dialogs and persistence appear.

Frameworks can also be overkill. The toolkit used in this section is small and mostly generates code, but the Windows data binding article warns that there is a cost to adopting any pattern or framework and the benefits are not always clear for small projects; adopt only the techniques that provide a clear benefit. Heavier frameworks such as Prism or ReactiveUI add region managers, modules or reactive types; they are third-party libraries and are not covered here.

What changed over time

Era Change that affects MVVM code

WPF on .NET Framework 3.0 to 4.8

MVVM pattern defined around WPF’s binding. View models implement INotifyPropertyChanged by hand, commands are hand-written ICommand classes, and view-model locators and frameworks (MvvmLight, Caliburn.Micro, Prism) fill the gaps. Nullable reference types do not exist.

.NET Core 3.0 to .NET 5

WPF and WinForms move to .NET (Windows only). System.ComponentModel interfaces are unchanged, so existing view models port without edits. The Windows Community Toolkit MVVM package is the successor of MvvmLight.

.NET 6 to .NET 8

Nullable annotations are enabled in new project templates; [CallerMemberName] helpers and the toolkit’s [ObservableProperty] (field-based generator) become the idiom.

.NET 9 and .NET 10

C# 13 and C# 14. CommunityToolkit.Mvvm 8.4.0 adds [ObservableProperty] on partial properties; 8.4.1 and later need the C# 14 compiler (Roslyn 5), and partial properties no longer need LangVersion preview with C# 14. Field-based generated properties keep working. WPF gains the Fluent theme, which changes appearance, not the pattern.

.NET 11 (release candidate)

No change to the binding or commanding model is listed in the WPF what’s-new page for RC1 at the time of writing; check the final release notes after general availability.

Dated versions come from the project’s release notes and the .NET release pages; the official ObservableProperty page has not been rewritten for partial properties, so rely on the release notes.

Pitfalls

  • View model references to view types. A MessageBox.Show, Window or Dispatcher call in a view model makes it untestable and ties it to WPF. Put the call behind an interface (IDialogService) and keep the implementation in the UI project.

  • Using Application.Current or static service locators. They work but hide dependencies. Inject services through the constructor.

  • Forgetting that DataContext is object. A wrong assignment is not a compile error; a binding to a missing member silently produces no value and logs a binding error in the Output window. Use the d:DataContext design-time hint, enable binding diagnostics and compile-time {x:Bind} where the stack supports it.

  • Exposing implementations instead of interfaces. The MAUI book recommends exposing commands as ICommand so the implementation can change. The toolkit’s generated members are IRelayCommand and IAsyncRelayCommand, which are fine because they derive from ICommand and add NotifyCanExecuteChanged and ExecuteAsync.

  • Raising notifications from the wrong thread. A background thread that changes a bound property or an ObservableCollection can fail or silently skip updates. Await the work and update on the UI thread, or use the synchronization context; see Threading, the Dispatcher and Async.

  • Replacing the collection instead of mutating it. Assigning a new ObservableCollection to a property needs a property-changed notification; mutating the existing one needs none. Pick one. The example above clears and refills the same collection, so the ItemsSource binding never has to change. Clearing still removes the selected item, so the list resets SelectedItem (and with it SelectedBook) to null; restore the selection by Id after the refill when it must survive a filter change.

  • A SelectedItem that is a copy. If the list holds BookViewModel wrappers and SelectedBook is rebuilt on every refresh, selection resets. Keep wrapper identity stable, or restore selection by Id after a reload.

  • Heavy logic in property setters. Setters run on every binding update, possibly per keystroke. Move heavy work to commands or debounce it (Delay on the binding or a timer).

  • Memory leaks through event handlers. A view model that subscribes to a long-lived service’s event stays alive as long as the service does. Unsubscribe, use weak events, or use the toolkit’s weak messenger; see Messaging Between View Models.

  • One giant view model. A MainViewModel that owns everything is code-behind in disguise. Split by screen and by responsibility, and compose view models.

  • Putting async void event handlers in the view to call the view model. Prefer commands (AsyncRelayCommand observes exceptions and exposes IsRunning) or a behavior; an unhandled exception in async void takes the application down.

  • Third-party package versions. Microsoft.Xaml.Behaviors.Wpf is a separate package with its own versioning; pin it, and check the project’s release notes when you upgrade the target framework.

  • Security. View models often handle input that goes to storage. Validate in the model or view model, not in the view, and never put secrets in a DataContext that is shown in the binding diagnostics or serialized into logs.

What the official documentation does not provide

  • There is no Microsoft Learn article on MVVM specifically for WPF. The guidance is in the MAUI enterprise e-book, the Windows data binding and MVVM article (aimed at WinUI), and the MVVM Toolkit documentation. This page applies it to WPF.

  • WPF has no {x:Bind}; WPF examples use {Binding} with a DataContext.

  • There is no official ViewModel-first navigation API, dialog service or view-model locator in WPF or in CommunityToolkit.Mvvm. They are user patterns, shown here and on Navigation, Dialogs & Services in MVVM. The locator belonged to MvvmLight.

  • The XAML Behaviors package for WPF is documented only in its GitHub repository, not on Learn.

  • There is no official guidance for testing WPF view models; the MAUI book’s unit-testing chapter and the toolkit’s tests are the closest references. See Testing View Models & UI.

  • The MAUI book’s MVVM chapter was last updated in 2024 and describes MAUI’s BindingContext, not WPF’s DataContext.