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
}
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.