Skip to main content
In this tutorial, you’ll build a complete Todo application that demonstrates core Avalonia concepts including data binding, collections, commands, and multi-view navigation.

What You’ll Build

A fully functional Todo application with:
  • Add, complete, and delete tasks
  • Filter tasks by status (All, Active, Completed)
  • Persistent data using ViewModel
  • Clean MVVM architecture
  • Modern Fluent UI design
This tutorial builds on concepts from the Quick Start guide. Make sure you’re comfortable with basic Avalonia concepts before proceeding.

Project Setup

1

Create the Project

Create a new Avalonia MVVM application:
dotnet new avalonia.mvvm -o TodoApp
cd TodoApp
2

Add Required Packages

The MVVM template includes CommunityToolkit.Mvvm, but let’s ensure we have everything:
dotnet add package CommunityToolkit.Mvvm
dotnet add package Avalonia.Themes.Fluent
3

Verify the Build

Make sure everything compiles:
dotnet build

Creating the Todo Model

First, let’s create a model to represent a todo item.
1

Create Models Directory

mkdir Models
2

Create TodoItem.cs

Create Models/TodoItem.cs:
Models/TodoItem.cs
using CommunityToolkit.Mvvm.ComponentModel;

namespace TodoApp.Models;

public partial class TodoItem : ObservableObject
{
    [ObservableProperty]
    private string _description = string.Empty;

    [ObservableProperty]
    private bool _isCompleted;

    public TodoItem(string description)
    {
        Description = description;
    }
}
The ObservableObject base class from CommunityToolkit.Mvvm provides INotifyPropertyChanged implementation.

Building the ViewModel

Now let’s create a ViewModel to manage our todo list logic.
1

Update MainWindowViewModel.cs

Replace the contents of ViewModels/MainWindowViewModel.cs:
ViewModels/MainWindowViewModel.cs
using System.Collections.ObjectModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using TodoApp.Models;

namespace TodoApp.ViewModels;

public partial class MainWindowViewModel : ViewModelBase
{
    [ObservableProperty]
    private string _newTodoText = string.Empty;

    [ObservableProperty]
    private string _filterMode = "All";

    public ObservableCollection<TodoItem> TodoItems { get; } = new();

    public ObservableCollection<TodoItem> FilteredTodoItems => FilterMode switch
    {
        "Active" => new(TodoItems.Where(x => !x.IsCompleted)),
        "Completed" => new(TodoItems.Where(x => x.IsCompleted)),
        _ => new(TodoItems)
    };

    public int ActiveCount => TodoItems.Count(x => !x.IsCompleted);
    public int CompletedCount => TodoItems.Count(x => x.IsCompleted);

    [RelayCommand]
    private void AddTodo()
    {
        if (string.IsNullOrWhiteSpace(NewTodoText))
            return;

        TodoItems.Add(new TodoItem(NewTodoText));
        NewTodoText = string.Empty;
        
        OnPropertyChanged(nameof(FilteredTodoItems));
        OnPropertyChanged(nameof(ActiveCount));
    }

    [RelayCommand]
    private void DeleteTodo(TodoItem item)
    {
        TodoItems.Remove(item);
        
        OnPropertyChanged(nameof(FilteredTodoItems));
        OnPropertyChanged(nameof(ActiveCount));
        OnPropertyChanged(nameof(CompletedCount));
    }

    [RelayCommand]
    private void ToggleTodo(TodoItem item)
    {
        item.IsCompleted = !item.IsCompleted;
        
        OnPropertyChanged(nameof(FilteredTodoItems));
        OnPropertyChanged(nameof(ActiveCount));
        OnPropertyChanged(nameof(CompletedCount));
    }

    [RelayCommand]
    private void SetFilter(string mode)
    {
        FilterMode = mode;
        OnPropertyChanged(nameof(FilteredTodoItems));
    }

    [RelayCommand]
    private void ClearCompleted()
    {
        var completed = TodoItems.Where(x => x.IsCompleted).ToList();
        foreach (var item in completed)
        {
            TodoItems.Remove(item);
        }
        
        OnPropertyChanged(nameof(FilteredTodoItems));
        OnPropertyChanged(nameof(CompletedCount));
    }
}

Designing the User Interface

Now let’s create a beautiful, functional UI.
1

Update MainWindow.axaml

Replace the contents of Views/MainWindow.axaml:
Views/MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:TodoApp.ViewModels"
        xmlns:models="using:TodoApp.Models"
        x:Class="TodoApp.Views.MainWindow"
        x:DataType="vm:MainWindowViewModel"
        Title="Todo App"
        Width="600"
        Height="700"
        MinWidth="400"
        MinHeight="500">
    
    <Design.DataContext>
        <vm:MainWindowViewModel/>
    </Design.DataContext>

    <Window.Styles>
        <Style Selector="ListBoxItem">
            <Setter Property="Padding" Value="0"/>
        </Style>
        <Style Selector="Button.filter">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="BorderThickness" Value="0"/>
        </Style>
        <Style Selector="Button.filter:pointerover">
            <Setter Property="Background" Value="#F0F0F0"/>
        </Style>
    </Window.Styles>

    <DockPanel>
        <!-- Header -->
        <Border DockPanel.Dock="Top" 
                Background="#007ACC" 
                Padding="20">
            <StackPanel>
                <TextBlock Text="Todo App" 
                           FontSize="32" 
                           FontWeight="Light"
                           Foreground="White"
                           HorizontalAlignment="Center"/>
                <TextBlock Text="Get things done!" 
                           FontSize="14" 
                           Foreground="#E0E0E0"
                           HorizontalAlignment="Center"
                           Margin="0,5,0,0"/>
            </StackPanel>
        </Border>

        <!-- Footer with stats -->
        <Border DockPanel.Dock="Bottom" 
                Background="#F5F5F5" 
                BorderBrush="#E0E0E0" 
                BorderThickness="0,1,0,0"
                Padding="20,15">
            <Grid ColumnDefinitions="*,Auto,*">
                <StackPanel Grid.Column="0" 
                            Orientation="Horizontal" 
                            Spacing="5">
                    <TextBlock Text="{Binding ActiveCount}" 
                               FontWeight="Bold"/>
                    <TextBlock Text="active"/>
                    <TextBlock Text="•" Margin="5,0"/>
                    <TextBlock Text="{Binding CompletedCount}" 
                               FontWeight="Bold"/>
                    <TextBlock Text="completed"/>
                </StackPanel>

                <!-- Filter buttons -->
                <StackPanel Grid.Column="1" 
                            Orientation="Horizontal" 
                            Spacing="5">
                    <Button Content="All" 
                            Classes="filter"
                            Command="{Binding SetFilterCommand}"
                            CommandParameter="All"
                            Padding="10,5"/>
                    <Button Content="Active" 
                            Classes="filter"
                            Command="{Binding SetFilterCommand}"
                            CommandParameter="Active"
                            Padding="10,5"/>
                    <Button Content="Completed" 
                            Classes="filter"
                            Command="{Binding SetFilterCommand}"
                            CommandParameter="Completed"
                            Padding="10,5"/>
                </StackPanel>

                <Button Grid.Column="2"
                        Content="Clear Completed"
                        Command="{Binding ClearCompletedCommand}"
                        HorizontalAlignment="Right"
                        Padding="10,5"/>
            </Grid>
        </Border>

        <!-- Main content -->
        <StackPanel Margin="20">
            <!-- Input area -->
            <Border BorderBrush="#E0E0E0" 
                    BorderThickness="1" 
                    CornerRadius="4"
                    Padding="10"
                    Margin="0,0,0,20">
                <Grid ColumnDefinitions="*,Auto">
                    <TextBox Grid.Column="0"
                             Text="{Binding NewTodoText}"
                             Watermark="What needs to be done?"
                             BorderThickness="0"
                             Background="Transparent">
                        <TextBox.KeyBindings>
                            <KeyBinding Gesture="Enter" 
                                      Command="{Binding AddTodoCommand}"/>
                        </TextBox.KeyBindings>
                    </TextBox>
                    <Button Grid.Column="1"
                            Content="Add"
                            Command="{Binding AddTodoCommand}"
                            Background="#007ACC"
                            Foreground="White"
                            Padding="20,8"
                            Margin="10,0,0,0"/>
                </Grid>
            </Border>

            <!-- Todo list -->
            <ScrollViewer>
                <ListBox ItemsSource="{Binding FilteredTodoItems}"
                         Background="Transparent"
                         BorderThickness="0">
                    <ListBox.ItemTemplate>
                        <DataTemplate x:DataType="models:TodoItem">
                            <Border BorderBrush="#E0E0E0" 
                                    BorderThickness="0,0,0,1"
                                    Padding="15,12">
                                <Grid ColumnDefinitions="Auto,*,Auto">
                                    <CheckBox Grid.Column="0"
                                              IsChecked="{Binding IsCompleted}"
                                              VerticalAlignment="Center"
                                              Margin="0,0,15,0"/>
                                    
                                    <TextBlock Grid.Column="1"
                                               Text="{Binding Description}"
                                               VerticalAlignment="Center"
                                               TextWrapping="Wrap">
                                        <TextBlock.Styles>
                                            <Style Selector="TextBlock">
                                                <Style Selector="^[IsEnabled=False]">
                                                    <Setter Property="TextDecorations" 
                                                            Value="Strikethrough"/>
                                                    <Setter Property="Foreground" 
                                                            Value="#999999"/>
                                                </Style>
                                            </Style>
                                        </TextBlock.Styles>
                                        <Interaction.Behaviors>
                                            <DataTriggerBehavior Binding="{Binding IsCompleted}" 
                                                                 Value="True">
                                                <ChangePropertyAction PropertyName="IsEnabled" 
                                                                     Value="False"/>
                                            </DataTriggerBehavior>
                                        </Interaction.Behaviors>
                                    </TextBlock>
                                    
                                    <Button Grid.Column="2"
                                            Content="✕"
                                            Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DeleteTodoCommand}"
                                            CommandParameter="{Binding}"
                                            Background="Transparent"
                                            BorderThickness="0"
                                            Foreground="#CC0000"
                                            Padding="10,5"
                                            VerticalAlignment="Center"/>
                                </Grid>
                            </Border>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
            </ScrollViewer>
        </StackPanel>
    </DockPanel>
</Window>

Running Your Application

1

Build the Project

dotnet build
2

Run the Application

dotnet run
Your Todo application should now launch with a beautiful interface!
3

Test the Features

Try out the functionality:
  • Type a task and click “Add” or press Enter
  • Check the checkbox to mark items complete
  • Click the ✕ button to delete items
  • Use the filter buttons to view All, Active, or Completed tasks
  • Click “Clear Completed” to remove all completed tasks

Key Concepts Explained

ObservableCollection

public ObservableCollection<TodoItem> TodoItems { get; } = new();
ObservableCollection automatically notifies the UI when items are added or removed, keeping the list synchronized.

RelayCommand with Parameters

[RelayCommand]
private void DeleteTodo(TodoItem item)
{
    TodoItems.Remove(item);
}
Commands can accept parameters from the UI through CommandParameter:
<Button Command="{Binding DeleteTodoCommand}"
        CommandParameter="{Binding}" />

Computed Properties

public ObservableCollection<TodoItem> FilteredTodoItems => FilterMode switch
{
    "Active" => new(TodoItems.Where(x => !x.IsCompleted)),
    "Completed" => new(TodoItems.Where(x => x.IsCompleted)),
    _ => new(TodoItems)
};
Properties can compute values based on other properties, providing filtered or transformed data to the UI.

Key Bindings

<TextBox.KeyBindings>
    <KeyBinding Gesture="Enter" 
                Command="{Binding AddTodoCommand}"/>
</TextBox.KeyBindings>
Key bindings allow keyboard shortcuts to trigger commands, improving user experience.

Parent Binding

Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DeleteTodoCommand}"
Inside a DataTemplate, $parent[Window] navigates up the visual tree to access the window’s DataContext.

Enhancing Your Application

Adding Persistence

To save todos between sessions, add JSON serialization:
using System.IO;
using System.Text.Json;

private const string SaveFileName = "todos.json";

private void SaveTodos()
{
    var json = JsonSerializer.Serialize(TodoItems);
    File.WriteAllText(SaveFileName, json);
}

private void LoadTodos()
{
    if (File.Exists(SaveFileName))
    {
        var json = File.ReadAllText(SaveFileName);
        var items = JsonSerializer.Deserialize<List<TodoItem>>(json);
        if (items != null)
        {
            foreach (var item in items)
            {
                TodoItems.Add(item);
            }
        }
    }
}

Adding Animations

Add smooth transitions for completed items:
<Style Selector="TextBlock">
    <Setter Property="Transitions">
        <Transitions>
            <DoubleTransition Property="Opacity" Duration="0:0:0.3"/>
        </Transitions>
    </Setter>
</Style>

Custom Themes

Create custom color schemes by modifying the App.axaml:
App.axaml
<Application.Resources>
    <SolidColorBrush x:Key="PrimaryBrush" Color="#007ACC"/>
    <SolidColorBrush x:Key="AccentBrush" Color="#00BCF2"/>
</Application.Resources>

Best Practices

Separation of Concerns

Keep your Models, ViewModels, and Views separate. Models contain data, ViewModels contain logic, and Views contain UI.

Use Commands

Always use commands instead of event handlers in XAML for better testability and MVVM compliance.

Observable Properties

Use [ObservableProperty] for automatic change notification rather than manually implementing INotifyPropertyChanged.

Data Binding

Leverage data binding to keep UI in sync with data automatically, avoiding manual UI updates.

What You’ve Learned

Congratulations! You’ve built a complete Avalonia application and learned:
  • ✅ Application structure and MVVM pattern
  • ✅ Data binding with ObservableCollection
  • ✅ Commands with and without parameters
  • ✅ Layout controls (DockPanel, Grid, StackPanel)
  • ✅ Styling and theming
  • ✅ User input handling
  • ✅ Collection filtering and computed properties
  • ✅ Complex UI layouts with templates

Next Steps

Data Binding

Learn advanced binding techniques and converters

Styling & Themes

Master Avalonia’s powerful styling system

Custom Controls

Build reusable custom controls

Dialogs & Navigation

Implement multi-window apps and dialogs
You’re now ready to build real-world Avalonia applications!

Build docs developers (and LLMs) love