MVVM Pattern in .NET MAUI
The Model-View-ViewModel (MVVM) architectural pattern is a popular choice for building .NET MAUI applications. It promotes a clear separation of concerns between the user interface (View), the business logic and data (Model), and the presentation logic that bridges the two (ViewModel).
Why Use MVVM?
MVVM offers several benefits:
- Testability: The ViewModel can be tested independently of the UI, making it easier to write unit tests for your application logic.
- Maintainability: Clear separation of concerns makes the codebase easier to understand, modify, and extend.
- Reusability: ViewModels can often be reused across different Views or even different projects.
- Developer Productivity: Designers can focus on the View, while developers can focus on the ViewModel and Model, enabling parallel development.
Core Components of MVVM
1. Model
The Model represents the application's data and business logic. It is typically composed of Plain Old CLR Objects (POCOs) and may include services for data access or external integrations. The Model is UI-agnostic; it has no knowledge of the View or ViewModel.
Note: Changes in the Model do not directly affect the UI. They are communicated through the ViewModel.
2. View
The View is the user interface of the application. In .NET MAUI, this is typically implemented using XAML. The View is responsible for displaying data from the ViewModel and sending user input to the ViewModel. It should contain minimal logic, primarily for visual presentation and UI event handling.
Crucially, the View binds to properties and commands exposed by the ViewModel. It should have no direct reference to the Model.
3. ViewModel
The ViewModel acts as an intermediary between the View and the Model. It exposes data from the Model in a way that is easily consumable by the View, often through properties that implement INotifyPropertyChanged
. It also exposes commands that the View can invoke to trigger actions in the application.
The ViewModel contains presentation logic, such as formatting data for display, handling user interactions, and orchestrating calls to the Model. It has no direct reference to the View but communicates with it via data binding and events.
Implementing MVVM in .NET MAUI
Data Binding
Data binding is the cornerstone of MVVM. It allows for automatic synchronization of data between the View and the ViewModel. In .NET MAUI XAML, you use the {Binding}
markup extension.
Consider a simple ViewModel with a property:
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));
}
}
And a corresponding View that binds to this property:
<!-- In your XAML file, e.g., MainPage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:YourApp.ViewModels"
x:Class="YourApp.Views.MainPage">
<ContentPage.BindingContext>
<viewmodels:MyViewModel />
</ContentPage.BindingContext>
<StackLayout Padding="20">
<Label Text="{Binding Message}" FontSize="Large" HorizontalOptions="Center" VerticalOptions="Center" />
</StackLayout>
</ContentPage>
Key Point: The BindingContext
of the View is set to an instance of the ViewModel. This allows the XAML elements to bind directly to the ViewModel's properties.
Commands
Commands provide a way for the View to trigger actions in the ViewModel without tightly coupling to specific UI elements. .NET MAUI supports the ICommand
interface, often implemented using Microsoft.Maui.Controls.Command
or Microsoft.Maui.Controls.Command<T>
.
ViewModel with a command:
public class MyViewModel : INotifyPropertyChanged
{
// ... Message property from above ...
public ICommand ClickMeCommand { get; }
public MyViewModel()
{
ClickMeCommand = new Command(ExecuteClickMe);
}
private void ExecuteClickMe()
{
Message = "Button was clicked!";
// Potentially interact with the Model here
}
// ... INotifyPropertyChanged implementation ...
}
View invoking the command:
<!-- In your XAML file -->
<StackLayout Padding="20">
<Label Text="{Binding Message}" FontSize="Large" HorizontalOptions="Center" VerticalOptions="Center" />
<Button Text="Click Me" Command="{Binding ClickMeCommand}" HorizontalOptions="Center" VerticalOptions="Center" />
</StackLayout>
MVVM Frameworks
While you can implement MVVM manually, using an MVVM framework can significantly simplify development by providing:
- ViewModel base classes with built-in property change notification and command implementations.
- Navigation services to decouple navigation logic from the View.
- Dependency injection support.
- Messaging/Event Aggregator patterns for inter-ViewModel communication.
Popular MVVM frameworks for .NET MAUI include:
- Community Toolkit MVVM (Microsoft-supported, high-performance, source-generated helpers).
- Mvvmcross (Mature and feature-rich framework).
- Prism for .NET MAUI (Powerful enterprise-grade framework).
Tip: For new projects, the Community Toolkit MVVM is an excellent starting point due to its performance and ease of use.
Example: A Simple Counter App
Model (Optional for this simple case, but good practice)
Let's imagine a hypothetical CounterService
if we wanted to persist the count.
ViewModel
using System.ComponentModel;
using System.Windows.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace YourApp.ViewModels;
public partial class CounterViewModel : ObservableObject
{
[ObservableProperty]
private int _count;
[ObservableProperty]
private string _statusMessage;
public IRelayCommand IncrementCommand { get; }
public IRelayCommand DecrementCommand { get; }
public CounterViewModel()
{
_count = 0;
_statusMessage = "Counter App";
IncrementCommand = new RelayCommand(Increment);
DecrementCommand = new RelayCommand(Decrement, CanDecrement);
}
private void Increment()
{
Count++;
StatusMessage = $"Count is now: {Count}";
// Important: If a command's ability to execute depends on properties,
// you need to notify the command when those properties change.
// In this case, CanDecrement depends on Count.
DecrementCommand.NotifyCanExecuteChanged();
}
private void Decrement()
{
if (Count > 0)
{
Count--;
StatusMessage = $"Count is now: {Count}";
DecrementCommand.NotifyCanExecuteChanged();
}
}
private bool CanDecrement()
{
return Count > 0;
}
}
View
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:YourApp.ViewModels"
x:Class="YourApp.Views.CounterPage"
Title="CounterPage">
<ContentPage.BindingContext>
<viewmodels:CounterViewModel />
</ContentPage.BindingContext>
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
<Label
Text="{Binding StatusMessage}"
SemanticProperties.HeadingLevel="Level1"
FontSize="32"
HorizontalOptions="Center" />
<Label
Text="{Binding Count, StringFormat='Current Count: {0}'}"
SemanticProperties.HeadingLevel="Level1"
FontSize="24"
HorizontalOptions="Center" />
<Button
Text="+"
Command="{Binding IncrementCommand}"
SemanticProperties.Hint="Increments the counter"
HorizontalOptions="Center" />
<Button
Text="-"
Command="{Binding DecrementCommand}"
SemanticProperties.Hint="Decrements the counter"
HorizontalOptions="Center"
IsEnabled="{Binding DecrementCommand.CanExecute}"/>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
By adopting the MVVM pattern, you create applications that are more robust, easier to test, and more enjoyable to maintain. Embrace data binding and commands to unlock the full potential of MVVM in your .NET MAUI projects.