Using MVVM Design Pattern in Windows Forms

By Fons Sonnemans, posted on
8390 Views 1 Comments

The MVVM Design Pattern is mainly used in XAML enviroments like WPF, UWP, WinUI, Xamarin Forms and MAUI, because they support databinding between Controls and  Models but also between Buttons and Commands.

In .NET 8 it is possible to implement the MVVM Design Pattern in Windows Forms apps. It uses a new data binding engine which was in preview with .NET 7, and is now fully enabled in .NET 8. This new engine is modeled after WPF, which makes it easier to implement MVVM design principles. Time to write a blog post about it.

For this post I have written a simple demo Solution containing 3 projects. The ModelsLibrary project contains a ViewModel and is referenced by a Windows Forms and WPF project. I have published this solution in this GitHub repository

The UI of the Windows Forms and WPF apps are very simple. It shows the value of a Counter and has an Increment button to increment the value by 1. Simular to the Counter sample page of a Blazor app.

Example apps

ModelsLibrary

This project is a .NET Stardard 2.0 project with the LangVersion set to 10.0. I use .NET Standard to allow it to be referenced from a UWP project. I use C# 10 because I use C# Source Generators from the CommunityToolkit.Mvvm NuGet package. The CommunityToolkit.Mvvm package (aka MVVM Toolkit, formerly named Microsoft.Toolkit.Mvvm) is a modern, fast, and modular MVVM library. I use it in all my MVVM projects.

In this project there is only one model class named MainViewModel. It is a partial class to support C# source generation. It inherits from ObservableObject to allow data binding (by implementing the INotifyPropertyChanged interface).

The private field _counter is annotated with the ObservableProperty attribute. This attribute generates observable properties from annotated fields. Its purpose is to greatly reduce the amount of boilerplate that is needed to define observable properties.

The Increment method is annotated with the RelayCommand attribute. The method will increase the Counter property by 1. This attribute generates relay command properties for annotated methods. Its purpose is to completely eliminate the boilerplate that is needed to define commands wrapping private methods in a viewmodel. It is often useful to be able to disable commands, and to then later on invalidate their state and have them check again whether they can be executed or not. In order to support this, the RelayCommand attribute exposes the CanExecute property, which can be used to indicate a target property or method to use to evaluate whether a command can be executed. In this demo the Increment is only valid if the Counter value is less than 10.

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace ModelsLibrary.ViewModels;

// This must be a partial class, the CommunityToolkit.Mvvm
// Source Generator will add Properties to it.
public partial class MainViewModel : ObservableObject {

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(IncrementCommand))]
    private int _counter;

    [RelayCommand(CanExecute = nameof(CanIncrement))]
    private void Increment() {
        Counter++;
    }

    private bool CanIncrement => Counter < 10;
}

WPF app

The WPF app has a MainWindow in the Views/Windows folder. The project references the ModelsLibrary project.

In the MainWindow the Window.DataContext property creates a new instance (object) of the MainViewModel class. The Text of the TextBlock is databound to the (generated) Counter property of the MainViewModel object. The Command of the Button is databound to the (generated) IncrementCommand property. There is no code added to the codebehind (MainWindow.xaml.cs). This binding is declared in XAML using {Binding}. The Binding of the TextBlock uses a C2 StringFormat to format the value to a currency.

<Window x:Class="WpfMvvmApp.Views.Windows.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:vm="clr-namespace:ModelsLibrary.ViewModels;assembly=ModelsLibrary"
        Title="Mvvm WPF app"
        Width="300"
        Height="200"
        FontSize="25"
        mc:Ignorable="d">
    <Window.DataContext>
        <vm:MainViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="1*" />
            <RowDefinition Height="1*" />
        </Grid.RowDefinitions>
        <TextBlock HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   Text="{Binding Counter, Mode=OneWay, StringFormat=C2}" />
        <Button Grid.Row="1"
                Padding="8"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Command="{Binding IncrementCommand, Mode=OneTime}"
                Content="Increment" />
    </Grid>
</Window>

Windows Forms app

The Windows Forms app has a MainForm in the Views/Forms folder. This project also references the ModelsLibrary project.

The MainForm has a Label and a Button control. The databinding is defined in the codebehind of the form. I could have used the Windows Forms designer for this but I prefer to do this manually in the CodeBehind. In the constructor of the MainForm a new instance of the MainViewModel is created. It is stored in a private readonly field so you can use it everwhere. Not that I'm using it in this demo.

In the constructor I created a Binding object by calling the Add method of the DataBindings property of the Label. The Add method binds the Text property to the Counter property of the MainViewModel instance. On this Binding object I set the FormatString property to C2 and enabled the Formatting.

The Command property of the Button control is new in .NET 8. I have set it to the IncrementProperty of the MainViewModel instance. When the button is clicked the Command is executed. The button will be disabled when the value of the Counter reaches 10.

using ModelsLibrary.ViewModels;

namespace WinFormsMvvmApp.Views.Forms; 

public partial class MainForm : Form {

    private readonly MainViewModel _vm;

    public MainForm() {
        InitializeComponent();

        _vm = new MainViewModel();

        var binding = labelCounter.DataBindings.Add(nameof(labelCounter.Text), _vm, nameof(_vm.Counter));
        binding.FormatString = "C2";
        binding.FormattingEnabled = true;

        buttonIncrement.Command = _vm.IncrementCommand;
    }
}

Closure

I love the MVVM Design pattern. I use it in all my XAML apps. You can use it now in Windows Forms apps too. This is very cool and easy. Way better than implementing Click events in the CodeBehind. Especially if you care about writing Unit Tests

In a next blog I will explain some more advanced Windows Forms scenarios like binding to ObservableCollections and using Command Parameters.

Below you can find some links about the usage of the MVVM design patterns in UWP, WPF and MAUI apps.

All postings/content on this blog are provided "AS IS" with no warranties, and confer no rights. All entries in this blog are my opinion and don't necessarily reflect the opinion of my employer or sponsors. The content on this site is licensed under a Creative Commons Attribution By license.

Leave a comment

Blog comments

Albano

03-Jan-2024 4:01
Thank you for the article. Could you enhance the example by utilizing the new DataContext property? Additionally, when binding from the designer, Microsoft consistently generates a BindingSource even for a single object. What distinguishes binding through this BindingSource versus directly with the ViewModel, as demonstrated here?