Mohammed Chami
.NET Developer | Content Creator
Mohammed Chami
.NET Developer | Content Creator

If you’ve been following along with our Avalonia journey, you’ve mastered the basics of creating projects and understanding the framework. Now it’s time to dive into the architectural pattern that will make your applications maintainable, testable, and scalable: Model-View-ViewModel (MVVM).
In this hands-on tutorial, we’ll build a practical todo application that demonstrates proper MVVM implementation using Community Toolkit MVVM. By the end, you’ll understand how to create ViewModels, implement commands, and maintain clean separation of concerns that professional developers swear by.
We’re creating a simple yet complete todo application featuring:
Fire up JetBrains Rider and create a new Avalonia project with MVVM template. I’ll assume you know the drill from our previous articles, so let’s jump straight to the package installation.
This package is your best friend for MVVM in .NET. It provides source generators that eliminate boilerplate code and makes your ViewModels incredibly clean.
Let’s start with the foundation – our MainViewModel. Create a new folder called ViewModels and add this class:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
namespace TodoApp.ViewModels;
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private string newTaskText = string.Empty;
public ObservableCollection<TodoItemViewModel> Tasks { get; } = new();
[RelayCommand]
private void AddTask()
{
if (!string.IsNullOrWhiteSpace(NewTaskText))
{
Tasks.Add(new TodoItemViewModel { Text = NewTaskText });
NewTaskText = string.Empty;
}
}
[RelayCommand]
private void RemoveTask(TodoItemViewModel task)
{
Tasks.Remove(task);
}
}
The partial keyword: Community Toolkit MVVM uses source generators, so your class needs to be partial to allow the generated code to merge seamlessly.
[ObservableProperty]: This attribute generates a full property with INotifyPropertyChanged implementation. No more tedious boilerplate! It creates a public property called NewTaskText from your private field newTaskText.
[RelayCommand]: Transforms your methods into ICommand implementations automatically. The method AddTask() becomes an AddTaskCommand property that your View can bind to.
Now let’s create the individual todo item ViewModel:
using CommunityToolkit.Mvvm.ComponentModel;
namespace TodoApp.ViewModels;
public partial class TodoItemViewModel : ObservableObject
{
[ObservableProperty]
private string text = string.Empty;
[ObservableProperty]
private bool isCompleted;
[ObservableProperty]
private string completionStatus = "Pending";
partial void OnIsCompletedChanged(bool value)
{
CompletionStatus = value ? "Completed" : "Pending";
}
}
The partial void OnIsCompletedChanged: This is a generated partial method hook that gets called whenever IsCompleted changes. Perfect for updating dependent properties!
Now let’s create the UI that connects to our ViewModels. Update your MainWindow.axaml:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:TodoApp.ViewModels"
x:Class="TodoApp.MainWindow"
Title="MVVM Todo App"
Width="500" Height="600">
<Window.DataContext>
<vm:MainViewModel />
</Window.DataContext>
<Grid RowDefinitions="Auto,*" Margin="20">
<!-- Add Task Section -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="10" Margin="0,0,0,20">
<TextBox Text="{Binding NewTaskText}"
Watermark="Enter a new task..."
Width="300" />
<Button Content="Add Task"
Command="{Binding AddTaskCommand}" />
</StackPanel>
<!-- Tasks List -->
<ScrollViewer Grid.Row="1">
<ItemsControl ItemsSource="{Binding Tasks}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:TodoItemViewModel">
<Border BorderBrush="LightGray"
BorderThickness="1"
Padding="10"
Margin="0,5"
CornerRadius="5">
<Grid ColumnDefinitions="*,Auto,Auto">
<TextBlock Grid.Column="0"
Text="{Binding Text}"
VerticalAlignment="Center"
TextDecorations="{Binding IsCompleted, Converter={StaticResource BoolToStrikethroughConverter}}" />
<TextBlock Grid.Column="1"
Text="{Binding CompletionStatus}"
VerticalAlignment="Center"
Margin="10,0"
Foreground="{Binding IsCompleted, Converter={StaticResource BoolToColorConverter}}" />
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="5">
<CheckBox IsChecked="{Binding IsCompleted}" />
<Button Content="Delete"
Command="{Binding $parent[Window].DataContext.RemoveTaskCommand}"
CommandParameter="{Binding}" />
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Window>
Create a Converters folder and add these converters to enhance the user experience:
using Avalonia.Data.Converters;
using Avalonia.Media;
using System;
using System.Globalization;
namespace TodoApp.Converters;
public class BoolToColorConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value is true ? Brushes.Green : Brushes.Orange;
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class BoolToStrikethroughConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value is true ? TextDecorations.Strikethrough : null;
}
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Don’t forget to register these converters in your App.axaml:
<Application.Resources>
<converters:BoolToColorConverter x:Key="BoolToColorConverter" />
<converters:BoolToStrikethroughConverter x:Key="BoolToStrikethroughConverter" />
</Application.Resources>
Let’s break down what makes this architecture so powerful:
Commands are the backbone of MVVM interaction. Here’s why they’re superior to event handlers:
Let’s enhance our AddTask command with validation:
[RelayCommand(CanExecute = nameof(CanAddTask))]
private void AddTask()
{
if (!string.IsNullOrWhiteSpace(NewTaskText))
{
Tasks.Add(new TodoItemViewModel { Text = NewTaskText });
NewTaskText = string.Empty;
}
}
private bool CanAddTask() => !string.IsNullOrWhiteSpace(NewTaskText);
partial void OnNewTaskTextChanged(string value)
{
AddTaskCommand.NotifyCanExecuteChanged();
}
Now the Add Task button automatically enables/disables based on whether there’s text in the input field!
One of MVVM’s greatest strengths is testability. Here’s how you can unit test your ViewModels:
using Xunit;
using TodoApp.ViewModels;
namespace TodoApp.Tests;
public class MainViewModelTests
{
[Fact]
public void AddTask_WithValidText_AddsTaskToCollection()
{
// Arrange
var viewModel = new MainViewModel();
viewModel.NewTaskText = "Test task";
// Act
viewModel.AddTaskCommand.Execute(null);
// Assert
Assert.Single(viewModel.Tasks);
Assert.Equal("Test task", viewModel.Tasks[0].Text);
Assert.Empty(viewModel.NewTaskText);
}
[Fact]
public void AddTask_WithEmptyText_DoesNotAddTask()
{
// Arrange
var viewModel = new MainViewModel();
viewModel.NewTaskText = "";
// Act
viewModel.AddTaskCommand.Execute(null);
// Assert
Assert.Empty(viewModel.Tasks);
}
}
Hit F5 in Rider or run from the terminal:
dotnet run
You should see a clean todo application where you can:
// DON'T DO THIS
public class BadViewModel
{
public void DoSomething(MainWindow window)
{
window.SomeTextBox.Text = "Bad!";
}
}
// DON'T DO THIS in code-behind
private void Button_Click(object sender, RoutedEventArgs e)
{
// Complex business logic here...
var result = CalculateComplexStuff();
UpdateDatabase(result);
}
Without proper change notification, your UI won’t update when data changes. Always use [ObservableProperty] or implement INotifyPropertyChanged manually.
For large datasets, consider virtualization or implement custom collection change handling:
// For better performance with large lists
public ObservableCollection<TodoItemViewModel> Tasks { get; } = new();
// Consider this approach for very large datasets
private void AddMultipleTasks(IEnumerable<TodoItemViewModel> newTasks)
{
Tasks.Clear();
foreach (var task in newTasks)
{
Tasks.Add(task);
}
}
Only notify when values actually change:
[ObservableProperty]
private string text = string.Empty;
partial void OnTextChanged(string value)
{
// Only execute expensive operations if needed
if (value?.Length > 100)
{
PerformExpensiveValidation(value);
}
}
You’ve just built a complete MVVM application that demonstrates all the core concepts: ViewModels with proper data binding, commands for user interactions, and clean separation of concerns. The Community Toolkit MVVM has eliminated most of the boilerplate code, letting you focus on building great features rather than fighting with infrastructure.
This architectural pattern will serve you well as your applications grow in complexity. The separation of concerns makes your code easier to test, maintain, and extend. Your future self (and your team) will thank you for writing clean, well-structured MVVM code.