ViewModels in .NET MAUI MVVM

ViewModels are a cornerstone of the Model-View-ViewModel (MVVM) architectural pattern, especially when building applications with .NET MAUI. They act as intermediaries between the View (UI) and the Model (data and business logic), facilitating data binding and command execution.

Understanding the ViewModel Role

A ViewModel's primary responsibility is to expose data and commands to the View. It holds the state of the View and handles user interactions by invoking commands, which in turn update the Model or perform other operations. Key characteristics of a ViewModel include:

  • Data Exposure: It exposes properties that the View can bind to for displaying data. When these properties change, the View automatically updates.
  • Command Exposure: It exposes commands that the View can trigger through user actions (e.g., button clicks).
  • Separation of Concerns: It decouples the View from the Model, making the application more maintainable and testable.
  • Platform Independence: ViewModels are typically platform-agnostic, allowing for code reuse across Android, iOS, macOS, and Windows.

Creating a ViewModel

ViewModels are typically implemented as plain C# classes. To facilitate data binding and notifications of property changes, it's highly recommended to inherit from INotifyPropertyChanged.


public class MyViewModel : INotifyPropertyChanged
{
    private string _message;
    public string Message
    {
        get => _message;
        set
        {
            if (_message != value)
            {
                _message = value;
                OnPropertyChanged(nameof(Message));
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    // Other properties and commands would go here
}
                
Tip: Consider using community MVVM frameworks like .NET MAUI Community Toolkit or MvvmCross. These frameworks provide base classes (like ObservableObject) that simplify implementing INotifyPropertyChanged and offer additional features like asynchronous commands.

Properties and Data Binding

Properties in your ViewModel are the source of data for your View. When a property's value changes, the UI should reflect that change. This is achieved through data binding and the INotifyPropertyChanged interface.

For example, consider binding a Label's text to the Message property of our MyViewModel:


<!-- In your XAML View -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:YourApp.ViewModels"
             x:Class="YourApp.Views.MyPage">

    <ContentPage.BindingContext>
        <local:MyViewModel />
    </ContentPage.BindingContext>

    <StackLayout Padding="20">
        <Label Text="{Binding Message}"
               FontSize="Medium"
               HorizontalOptions="Center"
               VerticalOptions="Center" />
    </StackLayout>
</ContentPage>
                

Commands

ViewModels also expose actions that the View can invoke. These are typically implemented as ICommand properties. When a user interacts with a UI element (like a button), it can be bound to a command in the ViewModel.

Here's how you might add a command to MyViewModel:


using System.Windows.Input;
using CommunityToolkit.Mvvm.Input; // Example using Community Toolkit

public class MyViewModel : INotifyPropertyChanged
{
    private string _message;
    public string Message
    {
        get => _message;
        set
        {
            if (_message != value)
            {
                _message = value;
                OnPropertyChanged(nameof(Message));
            }
        }
    }

    public ICommand ChangeMessageCommand { get; }

    public MyViewModel()
    {
        // Using RelayCommand from Community Toolkit for simplicity
        ChangeMessageCommand = new RelayCommand(ChangeMessage);
        Message = "Initial Message";
    }

    private void ChangeMessage()
    {
        Message = $"Message changed at {DateTime.Now:HH:mm:ss}";
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
                

And how you would bind a button to this command in XAML:


<!-- In your XAML View -->
<StackLayout Padding="20" Spacing="10">
    <Label Text="{Binding Message}"
           FontSize="Medium"
           HorizontalOptions="Center"
           VerticalOptions="Center" />

    <Button Text="Change Message"
            Command="{Binding ChangeMessageCommand}"
            HorizontalOptions="Center" />
</StackLayout>
                

ViewModel Lifecycle and Dependency Injection

Managing the lifecycle of ViewModels and injecting dependencies (like services) is crucial for robust application design. .NET MAUI integrates well with .NET's built-in Dependency Injection container.

You can register your ViewModels and Services in your application's startup code (e.g., MauiProgram.cs) and then resolve them in your Views or other ViewModels.


// In MauiProgram.cs
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

        // Register services
        builder.Services.AddSingleton<IMyService, MyService>();

        // Register ViewModels (scoped or transient depending on need)
        builder.Services.AddTransient<MyViewModel>();
        builder.Services.AddTransient<AnotherViewModel>();

        return builder.Build();
    }
}
                

Then, inject them into your View or ViewModel constructor:


public partial class MyPage : ContentPage
{
    public MyPage(MyViewModel viewModel) // ViewModel injected
    {
        InitializeComponent();
        BindingContext = viewModel;
    }
}
                

Best Practices for ViewModels

  • Keep them lean: ViewModels should focus on presentation logic and state, not complex business logic or direct UI manipulation.
  • Avoid UI references: ViewModels should not directly reference UI elements from the View.
  • Use properties for state: Expose data through properties that trigger notifications.
  • Use commands for actions: Encapsulate user-triggered actions in ICommand.
  • Consider a ViewModel factory or DI: For managing ViewModel creation and dependencies.
  • Testable: Design your ViewModels so they can be easily unit tested without needing a UI.