MVVM Made Simple with CommunityToolkit.Mvvm in .NET MAUI
If you've written MVVM code in .NET, you know the pain: endless INotifyPropertyChanged implementations, repetitive command declarations, and boilerplate that obscures your actual logic. The CommunityToolkit.Mvvm package fixes this with source generators that write the ceremony for you.
Setting Up
Install the package:
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
That's it. No platform-specific setup, no initialisation calls. The package is pure .NET — it works in MAUI, WPF, WinUI, or any other .NET project.
Observable Properties Without the Noise
Traditionally, every bindable property looks like this:
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
With the toolkit, you write a field and an attribute:
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private string _name;
}
The source generator produces the full property with change notification. The generated property is called Name (the leading underscore and lowercase are automatically corrected). You can also hook into property changes:
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private string _name;
// Called when Name changes
partial void OnNameChanged(string value)
{
Debug.WriteLine($"Name changed to: {value}");
}
// Called before Name changes — you can validate here
partial void OnNameChanging(string value)
{
Debug.WriteLine($"Name changing to: {value}");
}
}
Commands from Methods
The [RelayCommand] attribute turns any method into an ICommand:
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private string _searchQuery;
[ObservableProperty]
private bool _isLoading;
[RelayCommand]
private async Task SearchAsync()
{
IsLoading = true;
try
{
var results = await _searchService.FindAsync(SearchQuery);
Results = new ObservableCollection<string>(results);
}
finally
{
IsLoading = false;
}
}
}
The generator creates a SearchCommand property (the method name minus the Async suffix, plus Command). In XAML, you bind to it directly:
<Button Text="Search" Command="{Binding SearchCommand}" />
Commands with Parameters
Pass parameters naturally:
[RelayCommand]
private void DeleteItem(TodoItem item)
{
Items.Remove(item);
}
This generates DeleteItemCommand which accepts a TodoItem. In XAML:
<Button Text="Delete"
Command="{Binding DeleteItemCommand}"
CommandParameter="{Binding .}" />
CanExecute
You can tie command availability to a property:
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string _title;
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save()
{
// save logic
}
private bool CanSave() => !string.IsNullOrWhiteSpace(Title);
The [NotifyCanExecuteChangedFor] attribute ensures that when Title changes, the SaveCommand re-evaluates whether it can execute. The button bound to this command will automatically enable and disable.
Dependent Properties
When one property depends on another, use [NotifyPropertyChangedFor]:
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string _firstName;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string _lastName;
public string FullName => $"{FirstName} {LastName}";
Whenever FirstName or LastName changes, a PropertyChanged event fires for FullName too.
Messaging Between ViewModels
The toolkit includes a messenger for decoupled communication:
// Define a message
public record LoggedInMessage(User User);
// Send from one view model
WeakReferenceMessenger.Default.Send(new LoggedInMessage(currentUser));
// Receive in another
public partial class DashboardViewModel : ObservableRecipient
{
protected override void OnActivated()
{
Messenger.Register<LoggedInMessage>(this, (r, m) =>
{
CurrentUser = m.User;
});
}
}
ObservableRecipient extends ObservableObject with messenger support. Call IsActive = true to activate the registrations.
Validation
The toolkit integrates with System.ComponentModel.DataAnnotations:
public partial class RegisterViewModel : ObservableValidator
{
[ObservableProperty]
[Required]
[MinLength(3)]
[NotifyDataErrorInfo]
private string _username;
[ObservableProperty]
[Required]
[EmailAddress]
[NotifyDataErrorInfo]
private string _email;
[RelayCommand]
private void Submit()
{
ValidateAllProperties();
if (HasErrors)
return;
// proceed with registration
}
}
The [NotifyDataErrorInfo] attribute makes the generator include INotifyDataErrorInfo support, so validation errors propagate to the UI automatically.
Why This Matters
The CommunityToolkit.Mvvm package eliminates the most tedious aspects of MVVM without hiding what's happening. Because it uses source generators rather than reflection, there's no runtime performance cost. You can inspect the generated code in your IDE (look under Dependencies > Analyzers > CommunityToolkit.Mvvm.SourceGenerators) to see exactly what's produced.
For MAUI apps, this means cleaner view models, fewer bugs from copy-paste errors in property implementations, and more time spent on actual application logic.