Introduction: The Battle Against Spaghetti Code
Enter .NET MAUI (Multi-platform App UI) and the Model-View-ViewModel (MVVM) architectural pattern. MVVM is not just a “suggestion” in the world of .NET cross-platform development; it is the industry standard. It provides a clean separation of concerns, making your code testable, maintainable, and scalable across Android, iOS, macOS, and Windows.
In this guide, we will dive deep into MVVM in .NET MAUI. We will move from basic concepts to advanced implementation using the latest Source Generators, Dependency Injection, and the Community Toolkit. Whether you are a beginner looking to build your first app or an intermediate developer seeking to refactor legacy code, this guide is your definitive roadmap.
Understanding the MVVM Trio: Model, View, and ViewModel
To master MVVM, we must first understand the three distinct roles that make up the architecture. Think of it like a restaurant: the Chef (Model), the Waiter (ViewModel), and the Customer (View).
1. The Model (The Data and Logic)
The Model represents the data structures and the business rules of your application. It knows nothing about the UI. It might be a simple class representing a “Product” or a service that fetches data from a REST API. In our restaurant analogy, the Model is the kitchen and the ingredients.
2. The View (The Visuals)
The View is what the user sees and interacts with. In .NET MAUI, this is typically defined in XAML (Extensible Application Markup Language). The View should be “dumb”—it should only care about how things look, not how the data is processed. The View is the customer’s table and the menu.
3. The ViewModel (The Orchestrator)
The ViewModel is the bridge. It acts as a converter that changes Model data into a format the View can easily display. It handles user input (via Commands) and notifies the View when data changes (via Data Binding). Crucially, the ViewModel has no reference to the View. It doesn’t know if it’s running on an iPhone or a Windows PC. The ViewModel is the waiter who takes the order and brings the food.
Setting Up Your .NET MAUI Environment for MVVM
Before we write code, we need the right tools. While you can implement MVVM manually by implementing INotifyPropertyChanged, modern developers use the CommunityToolkit.Mvvm library. It uses C# Source Generators to write the repetitive “boilerplate” code for you.
Step 1: Install the NuGet Package
Open your NuGet Package Manager or use the CLI to install:
dotnet add package CommunityToolkit.Mvvm
Step 2: Organize Your Folder Structure
A clean project structure is vital for SEO and maintainability. Create the following folders in your .NET MAUI project:
- Models: For your data objects.
- ViewModels: For your logic classes.
- Views: For your XAML pages.
- Services: For API or Database interaction.
Implementing the Model
Let’s build a simple “Task Manager” application. We start with our Model. This is a plain old C# object (POCO).
namespace MauiMvvmGuide.Models
{
public class TodoItem
{
public string Title { get; set; }
public bool IsCompleted { get; set; }
}
}
Notice that this class is simple. It doesn’t inherit from anything or use any special MAUI namespaces. This makes it extremely easy to unit test or move to a different project later.
The Power of the ViewModel (Modern Approach)
In the old days, you had to write 10 lines of code for every property to notify the UI of a change. With the Community Toolkit, we use the [ObservableProperty] attribute. The toolkit’s source generator will automatically create the Title and IsCompleted properties with all the notification logic included.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MauiMvvmGuide.Models;
using System.Collections.ObjectModel;
namespace MauiMvvmGuide.ViewModels
{
// Partial class is required for Source Generators to work
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
string newTaskTitle;
// ObservableCollection automatically updates the UI when items are added/removed
public ObservableCollection<TodoItem> Tasks { get; } = new();
[RelayCommand]
void AddTask()
{
if (string.IsNullOrWhiteSpace(NewTaskTitle))
return;
Tasks.Add(new TodoItem { Title = NewTaskTitle, IsCompleted = false });
// Clear the entry field
NewTaskTitle = string.Empty;
}
[RelayCommand]
void DeleteTask(TodoItem item)
{
if (Tasks.Contains(item))
{
Tasks.Remove(item);
}
}
}
}
Why this is better:
- ObservableObject: Implements
INotifyPropertyChangedfor you. - [ObservableProperty]: Generates a property named
NewTaskTitlefrom the fieldnewTaskTitle. - [RelayCommand]: Generates an
ICommandcalledAddTaskCommandwhich the View can bind to.
Building the View: XAML and Data Binding
Now, let’s connect the ViewModel to the View. In .NET MAUI, we use the BindingContext to tell the XAML page which class is providing its data.
<?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:viewmodel="clr-namespace:MauiMvvmGuide.ViewModels"
x:Class="MauiMvvmGuide.Views.MainPage"
x:DataType="viewmodel:MainViewModel">
<VerticalStackLayout Padding="20" Spacing="15">
<!-- Entry for new task title -->
<Entry Text="{Binding NewTaskTitle}"
Placeholder="Enter a new task..." />
<!-- Button to trigger the AddTask command -->
<Button Text="Add Task"
Command="{Binding AddTaskCommand}" />
<!-- List of tasks -->
<CollectionView ItemsSource="{Binding Tasks}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:TodoItem">
<HorizontalStackLayout Spacing="10" Padding="5">
<CheckBox IsChecked="{Binding IsCompleted}" />
<Label Text="{Binding Title}" VerticalOptions="Center" />
</HorizontalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
</ContentPage>
Key Concept: x:DataType
Always use x:DataType. This is called Compiled Bindings. Without it, MAUI uses reflection at runtime to find properties, which is slow and error-prone. With it, the compiler checks that your bindings are correct, improving performance and catching bugs during development.
Step-by-Step: Wiring Up Dependency Injection (DI)
In modern .NET apps, we don’t usually create ViewModels manually with new MainViewModel(). Instead, we use the built-in Dependency Injection container. This allows us to inject services (like an API client) into our ViewModel easily.
Step 1: Register in MauiProgram.cs
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// Register Services
builder.Services.AddSingleton<IDataService, MyDataService>();
// Register ViewModels
builder.Services.AddTransient<MainViewModel>();
// Register Views
builder.Services.AddTransient<MainPage>();
return builder.Build();
}
}
Step 2: Inject into the View’s Constructor
Go to your MainPage.xaml.cs (the code-behind) and inject the ViewModel:
public partial class MainPage : ContentPage
{
public MainPage(MainViewModel viewModel)
{
InitializeComponent();
// Set the BindingContext to the injected ViewModel
BindingContext = viewModel;
}
}
Advanced MVVM: Navigation and Shell
Navigating between pages in MVVM can be tricky because the ViewModel shouldn’t know about the UI “Page” objects. .NET MAUI Shell simplifies this using Route-based navigation.
To navigate from a ViewModel:
[RelayCommand]
async Task GoToDetails(TodoItem item)
{
// Pass the object to the next page using Query Parameters
var navigationParameter = new Dictionary<string, object>
{
{ "Item", item }
};
await Shell.Current.GoToAsync("DetailsPage", navigationParameter);
}
On the receiving ViewModel, use the [QueryProperty] attribute to grab the data:
[QueryProperty(nameof(Item), "Item")]
public partial class DetailsViewModel : ObservableObject
{
[ObservableProperty]
TodoItem item;
}
Common MVVM Mistakes and How to Fix Them
1. Blocking the UI Thread
The Mistake: Performing long-running tasks (like fetching API data) inside a property setter or a synchronous Command.
The Fix: Always use async Task in your RelayCommands. The Community Toolkit supports [RelayCommand] async Task MyMethod() automatically.
2. Forgetting to Use ObservableCollection
The Mistake: Using List<T> for data bound to a CollectionView. When you add an item to a List, the UI doesn’t know it needs to refresh.
The Fix: Use ObservableCollection<T>. It implements INotifyCollectionChanged, which tells the UI to add or remove rows dynamically.
3. Massive ViewModels
The Mistake: Putting 1,000 lines of logic into one ViewModel. This is just “Spaghetti Code 2.0.”
The Fix: Use Services. If you are making an HTTP call, put that logic in a WeatherService class. Inject that service into the ViewModel. The ViewModel should only contain the logic necessary to support the View.
4. Logic in the Code-Behind
The Mistake: Handling button clicks in the .xaml.cs file via Clicked="OnButtonClicked".
The Fix: Use Command="{Binding MyCommand}". This keeps your logic in the ViewModel, where it can be unit tested without a physical device or emulator.
Performance Optimization for .NET MAUI MVVM
Mobile devices have limited resources. To keep your app buttery smooth, follow these SEO-friendly performance tips:
- One-Way Bindings: If a Label only displays data and never changes it, use
Mode=OneWayorOneTime. This reduces the number of event listeners MAUI has to manage. - Image Loading: Don’t load massive high-res images into a list. The Model should provide optimized URIs or thumbnails.
- Memory Leaks: Be careful with static events. If your ViewModel subscribes to a global event, unsubscribe in an “Unloaded” event to prevent memory leaks.
- Compiled Bindings: I will repeat this: Use
x:DataTypeeverywhere! It significantly reduces CPU overhead during UI rendering.
Real-World Example: A Weather Dashboard ViewModel
Let’s look at a more complex ViewModel that handles loading states, error handling, and data transformation.
public partial class WeatherViewModel : ObservableObject
{
private readonly IWeatherService _weatherService;
[ObservableProperty]
private string city;
[ObservableProperty]
private double temperature;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsNotLoading))]
private bool isLoading;
public bool IsNotLoading => !IsLoading;
public WeatherViewModel(IWeatherService weatherService)
{
_weatherService = weatherService;
}
[RelayCommand]
async Task RefreshWeatherAsync()
{
try
{
IsLoading = true;
var data = await _weatherService.GetWeatherForCity(City);
Temperature = data.Temp;
}
catch (Exception ex)
{
// Handle error (e.g., show an alert)
await Shell.Current.DisplayAlert("Error", "Could not fetch weather", "OK");
}
finally
{
IsLoading = false;
}
}
}
This example demonstrates the [NotifyPropertyChangedFor] attribute, which is extremely useful for dependent properties like “IsNotLoading” that rely on “IsLoading”.
Summary and Key Takeaways
Mastering MVVM in .NET MAUI is the single most important skill for a cross-platform developer. It transforms a messy project into a professional, testable application.
- Separation: Keep Models for data, Views for XAML, and ViewModels for logic.
- Toolkit: Use the
CommunityToolkit.Mvvmto eliminate boilerplate code via Source Generators. - Binding: Use
x:DataTypefor compiled bindings to boost performance. - Commands: Replace event handlers with
ICommandandRelayCommand. - DI: Register your ViewModels and Services in
MauiProgram.cs.
Frequently Asked Questions (FAQ)
1. Do I HAVE to use MVVM for small apps?
While you can use code-behind for a tiny “Hello World” app, it is better to practice MVVM even on small projects. It builds muscle memory and makes it much easier to expand the app later when “simple” becomes “complex.”
2. What is the difference between ObservableCollection and List?
A List<T> is just a collection of data in memory. An ObservableCollection<T> is a specialized collection that sends a “notification” to the UI every time an item is added, removed, or the entire list is cleared, allowing the UI to update automatically.
3. How do I handle UI events like “Page Appearing” in MVVM?
You can use EventToCommandBehavior from the .NET MAUI Community Toolkit. This allows you to map XAML events (like Appearing) directly to a Command in your ViewModel without writing code in the code-behind.
4. Can I use MVVM with other frameworks like ReactiveUI?
Yes, .NET MAUI is flexible. While the Community Toolkit is the most popular, you can use ReactiveUI or Prism if you prefer a different flavor of MVVM logic (like functional reactive programming).
5. Why is my binding not working?
The three most common reasons are: 1) You forgot to set the BindingContext. 2) You are binding to a field instead of a property (properties must have { get; set; }). 3) Your class doesn’t implement INotifyPropertyChanged.
