Skip to main content

Data Binding and the MVVM Pattern

Data Binding and MVVM

Data binding is a powerful feature in Avalonia UI that creates a connection between the UI and business logic, enabling automatic data synchronization. When combined with the Model-View-ViewModel (MVVM) pattern, it creates a clean separation of concerns that makes your applications more maintainable, testable, and extensible.

Understanding Data Binding

Data binding establishes a connection between a property of a UI element (the target) and a property of a data object (the source). This connection can automatically propagate changes in either direction, depending on the binding mode.

Key Benefits of Data Binding

  • Separation of UI and Logic: Keep presentation separate from business logic
  • Automatic Updates: UI reflects changes in data without manual synchronization
  • Reduced Code: Minimize UI manipulation code
  • Testability: Business logic can be tested independently of the UI
  • Design-Time Data: Use sample data during design for better visualization

Data Binding Basics

Binding Syntax

The basic syntax for data binding in XAML uses the {Binding} markup extension:

<!-- Basic property binding -->
<TextBlock Text="{Binding Username}" />

<!-- Binding with path to nested property -->
<TextBlock Text="{Binding User.Address.City}" />

<!-- Binding with string format -->
<TextBlock Text="{Binding Price, StringFormat='{}{0:C}'}" />

<!-- Binding with fallback value -->
<TextBlock Text="{Binding Status, FallbackValue='Unknown'}" />

Each binding connects a target property (like Text) to a source property (like Username) in the current data context.

Data Context

The data context is the object that serves as the source for bindings. It can be set at any level in the control hierarchy and is inherited by child elements:

<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MyApp.ViewModels"
x:Class="MyApp.Views.MainWindow">

<!-- Setting DataContext in XAML -->
<Window.DataContext>
<vm:MainWindowViewModel />
</Window.DataContext>

<StackPanel Margin="20">
<!-- These bindings use the MainWindowViewModel as their source -->
<TextBlock Text="{Binding Greeting}"
FontSize="24"
Margin="0,0,0,20" />

<TextBox Text="{Binding Username}"
Watermark="Enter your name"
Width="250"
HorizontalAlignment="Left"
Margin="0,0,0,10" />

<CheckBox Content="Remember me"
IsChecked="{Binding RememberUser}"
Margin="0,0,0,20" />

<!-- Child element with its own DataContext -->
<Border Background="#f5f5f5"
Padding="15"
CornerRadius="4">
<Border.DataContext>
<vm:UserProfileViewModel />
</Border.DataContext>

<!-- These bindings use UserProfileViewModel as their source -->
<StackPanel>
<TextBlock Text="{Binding FullName}" FontWeight="Bold" />
<TextBlock Text="{Binding Email}" />
</StackPanel>
</Border>
</StackPanel>
</Window>

You can also set the data context programmatically in code-behind:

public MainWindow()
{
InitializeComponent();

// Set DataContext in code
DataContext = new MainWindowViewModel
{
Greeting = "Welcome to Avalonia UI!",
Username = "John Doe",
RememberUser = true
};

// You can also set DataContext for specific controls
UserProfileSection.DataContext = new UserProfileViewModel
{
FullName = "John Doe",
Email = "john.doe@example.com"
};
}

Binding Modes

Avalonia UI supports different binding modes that control how data flows between the source (ViewModel) and target (UI element):

ModeData FlowDescriptionCommon Use Cases
OneWaySource → TargetChanges to the source update the targetDisplaying read-only data (labels, status indicators)
TwoWaySource ↔ TargetChanges to either the source or target update the otherEditable forms, user input fields
OneTimeSource → Target (once)The target is updated once when the binding is createdStatic content, initialization values
OneWayToSourceTarget → SourceChanges to the target update the sourceSpecial UI controls that manage their own state
<!-- Login form with different binding modes -->
<StackPanel Margin="20" Width="300">
<TextBlock Text="{Binding WelcomeMessage, Mode=OneWay}"
FontSize="20"
Margin="0,0,0,20" />

<TextBlock Text="Username:" Margin="0,0,0,5" />
<TextBox Text="{Binding Username, Mode=TwoWay}"
Margin="0,0,0,15" />

<TextBlock Text="Password:" Margin="0,0,0,5" />
<TextBox PasswordChar=""
Text="{Binding Password, Mode=TwoWay}"
Margin="0,0,0,15" />

<CheckBox Content="Remember credentials"
IsChecked="{Binding RememberMe, Mode=TwoWay}"
Margin="0,0,0,20" />

<Button Content="Log In"
Command="{Binding LoginCommand}"
HorizontalAlignment="Right"
Padding="15,8" />

<TextBlock Text="{Binding CurrentTime, Mode=OneTime}"
Foreground="#757575"
FontSize="12"
Margin="0,20,0,0" />
</StackPanel>

Default Binding Modes by Property Type:

Many properties have sensible default binding modes based on their typical usage:

Control PropertyDefault ModeRationale
TextBox.TextTwoWayUser typically edits text
TextBlock.TextOneWayDisplay-only text
CheckBox.IsCheckedTwoWayUser toggles state
Slider.ValueTwoWayUser adjusts value
Image.SourceOneWayDisplay-only image
Button.CommandOneWayCommands are invoked, not modified

You can override the default mode when needed:

// In your ViewModel
private string _readOnlyUsername;
public string ReadOnlyUsername
{
get => _readOnlyUsername;
set => this.RaiseAndSetIfChanged(ref _readOnlyUsername, value);
}

// In your XAML
<TextBox Text="{Binding ReadOnlyUsername, Mode=OneWay}" IsReadOnly="True" />

Path Syntax and Property Resolution

Bindings can target simple properties or navigate through object hierarchies:

Basic Property Binding

<!-- Binds to the Title property of the current DataContext -->
<TextBlock Text="{Binding Title}" />

Nested Property Paths

Use dot notation to navigate through object hierarchies:

<!-- Customer profile with nested properties -->
<StackPanel Margin="20">
<TextBlock Text="{Binding Customer.FullName}"
FontSize="20"
FontWeight="SemiBold" />

<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
<TextBlock Text="Email: " FontWeight="SemiBold" />
<TextBlock Text="{Binding Customer.ContactInfo.Email}" />
</StackPanel>

<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<TextBlock Text="Phone: " FontWeight="SemiBold" />
<TextBlock Text="{Binding Customer.ContactInfo.Phone}" />
</StackPanel>

<Border Background="#f5f5f5"
Margin="0,15,0,0"
Padding="15"
CornerRadius="4">
<StackPanel>
<TextBlock Text="Shipping Address:"
FontWeight="SemiBold"
Margin="0,0,0,5" />
<TextBlock Text="{Binding Customer.Address.Street}" />
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}, {1} {2}">
<Binding Path="Customer.Address.City" />
<Binding Path="Customer.Address.State" />
<Binding Path="Customer.Address.ZipCode" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock Text="{Binding Customer.Address.Country}" />
</StackPanel>
</Border>
</StackPanel>

Indexed Properties

Access collection items using indexers:

<!-- Display items from a collection -->
<StackPanel>
<TextBlock Text="Recent Orders:"
FontWeight="SemiBold"
Margin="0,0,0,10" />

<!-- Access by index -->
<TextBlock Text="{Binding Orders[0].OrderNumber}" />
<TextBlock Text="{Binding Orders[0].OrderDate, StringFormat='{}Ordered on: {0:d}'}" />
<TextBlock Text="{Binding Orders[0].TotalAmount, StringFormat='{}{0:C}'}" />

<!-- Better approach: use ItemsControl instead of hard-coded indices -->
<ItemsControl Items="{Binding Orders}" Margin="0,15,0,0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#f9f9f9"
Margin="0,0,0,8"
Padding="10"
CornerRadius="4">
<StackPanel>
<TextBlock Text="{Binding OrderNumber}" FontWeight="SemiBold" />
<TextBlock Text="{Binding OrderDate, StringFormat='{}Ordered on: {0:d}'}" />
<TextBlock Text="{Binding TotalAmount, StringFormat='{}{0:C}'}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>

Dictionary Access

Access dictionary entries using indexers:

<!-- Access dictionary values -->
<StackPanel>
<TextBlock Text="{Binding Settings['Theme']}" />
<TextBlock Text="{Binding Translations['Welcome']}" />
</StackPanel>

Element Binding

Element binding allows you to bind to properties of other UI elements, which is useful for creating interdependent controls:

<!-- Theme selector with live preview -->
<Grid RowDefinitions="Auto,*" ColumnDefinitions="*,*" Margin="20" RowSpacing="20" ColumnSpacing="20">
<!-- Controls -->
<StackPanel Grid.Row="0" Grid.Column="0" Spacing="10">
<TextBlock Text="Theme Preview" FontWeight="SemiBold" />

<!-- Named slider controls -->
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="Primary Hue:" VerticalAlignment="Center" Width="100" />
<Slider x:Name="hueSlider" Minimum="0" Maximum="360" Value="200" Width="200" />
<TextBlock Text="{Binding #hueSlider.Value, StringFormat='{}{0:N0}°'}" Width="50" />
</StackPanel>

<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="Saturation:" VerticalAlignment="Center" Width="100" />
<Slider x:Name="satSlider" Minimum="0" Maximum="100" Value="70" Width="200" />
<TextBlock Text="{Binding #satSlider.Value, StringFormat='{}{0:N0}%'}" Width="50" />
</StackPanel>

<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="Lightness:" VerticalAlignment="Center" Width="100" />
<Slider x:Name="lightSlider" Minimum="20" Maximum="80" Value="50" Width="200" />
<TextBlock Text="{Binding #lightSlider.Value, StringFormat='{}{0:N0}%'}" Width="50" />
</StackPanel>

<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="Radius:" VerticalAlignment="Center" Width="100" />
<Slider x:Name="radiusSlider" Minimum="0" Maximum="20" Value="4" Width="200" />
<TextBlock Text="{Binding #radiusSlider.Value, StringFormat='{}{0:N0}px'}" Width="50" />
</StackPanel>
</StackPanel>

<!-- Live preview using element binding -->
<Border Grid.Row="0" Grid.Column="1" Grid.RowSpan="2"
Padding="20"
CornerRadius="{Binding #radiusSlider.Value}">
<Border.Background>
<!-- Dynamically generate background color from HSL values -->
<SolidColorBrush Color="{Binding #hueSlider.Value, #satSlider.Value, #lightSlider.Value,
Converter={StaticResource HslToColorConverter}}" />
</Border.Background>

<StackPanel Spacing="15">
<TextBlock Text="Preview Panel"
FontSize="20"
FontWeight="SemiBold"
Foreground="White" />

<TextBlock Text="This panel updates in real-time as you adjust the sliders."
Foreground="White"
TextWrapping="Wrap" />

<Button Content="Sample Button"
Margin="0,10,0,0"
HorizontalAlignment="Left" />
</StackPanel>
</Border>

<!-- Color swatches -->
<ItemsControl Grid.Row="1" Grid.Column="0"
Items="{Binding ColorPresets}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Width="40"
Height="40"
Margin="5"
Background="{Binding Color}"
Command="{Binding $parent[ItemsControl].DataContext.ApplyColorCommand}"
CommandParameter="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>

Avalonia provides two syntaxes for element binding:

  1. Traditional syntax with ElementName:

    <TextBlock Text="{Binding Value, ElementName=slider}" />
  2. Shorthand syntax with # (recommended):

    <TextBlock Text="{Binding #slider.Value}" />

Relative Source Binding

Relative source binding allows you to reference elements relative to the binding target in the visual tree, which is particularly useful for creating reusable controls and templates.

Avalonia provides a powerful syntax for relative source binding using the $ prefix:

SyntaxDescriptionExample Use Case
$selfBinds to a property on the element itselfBinding a control's property to another property on the same control
$parentBinds to the logical parent of the elementAccessing properties from a parent container
$parent[Type]Binds to the first ancestor of the specified typeFinding a specific parent type in a nested hierarchy
$findAncestor[Type]Same as $parent[Type]Alternative syntax for finding ancestors
$templatedParentBinds to the control that a template is applied toAccessing the control from within a control template

Examples of Relative Source Binding

<!-- Custom list item template that references properties from different levels -->
<ListBox Items="{Binding Products}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Background="#f5f5f5"
Padding="12"
Margin="0,0,0,8"
CornerRadius="4">
<Grid ColumnDefinitions="*,Auto">
<!-- Product info from the item's DataContext -->
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="{Binding Name}"
FontWeight="SemiBold" />
<TextBlock Text="{Binding Description}"
TextWrapping="Wrap"
Opacity="0.7" />
<TextBlock Text="{Binding Price, StringFormat='{}{0:C}'}"
FontWeight="SemiBold"
Foreground="#1976D2" />
</StackPanel>

<!-- Button that uses a command from the ListBox's DataContext -->
<Button Grid.Column="1"
Content="Add to Cart"
Margin="12,0,0,0"
VerticalAlignment="Center"
Command="{Binding $parent[ListBox].DataContext.AddToCartCommand}"
CommandParameter="{Binding}" />
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Custom control that adapts to its container's properties -->
<UserControl x:Class="MyApp.Controls.AdaptivePanel">
<Border Background="{Binding $parent[Window].Background}"
BorderBrush="{Binding $parent[Window].Foreground}"
BorderThickness="1"
Padding="15"
CornerRadius="4">
<StackPanel>
<!-- Self-referencing binding to get the control's own width -->
<TextBlock Text="{Binding $self.Bounds.Width, StringFormat='Width: {0:N0}px'}" />

<!-- Binding to the parent window's title -->
<TextBlock Text="{Binding $parent[Window].Title, StringFormat='Window: {0}'}"
Margin="0,5,0,0" />

<!-- Content that adapts to the theme -->
<ContentControl Content="{Binding}"
Margin="0,10,0,0" />
</StackPanel>
</Border>
</UserControl>

Finding Specific Ancestors

You can navigate up the visual tree to find specific ancestor types:

<!-- Find the nearest TabControl ancestor and bind to its SelectedIndex -->
<TextBlock Text="{Binding $parent[TabControl].SelectedIndex,
StringFormat='Current Tab: {0}'}" />

<!-- Find a specific named ancestor -->
<Button Command="{Binding $parent[UserControl].DataContext.SaveCommand}" />

Templated Parent Binding

When creating control templates, you can bind back to the control the template is applied to:

<ControlTemplate x:Key="CustomButtonTemplate" TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<Grid>
<!-- Bind to the templated parent's IsPressed property -->
<Border Background="#20000000"
IsVisible="{Binding $templatedParent.IsPressed}" />

<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Grid>
</Border>
</ControlTemplate>

Value Converters

Value converters transform data as it flows between the source and target, allowing you to adapt data types, formats, or representations without modifying your view models.

Common Use Cases for Converters

  • Converting between different data types (e.g., boolean to visibility)
  • Formatting data for display (e.g., dates, numbers, currencies)
  • Inverting or negating values
  • Combining multiple values into one
  • Translating enum values to user-friendly strings
  • Conditional styling based on data values

Creating a Value Converter

Value converters implement the IValueConverter interface:

using Avalonia.Controls;
using Avalonia.Data.Converters;
using System;
using System.Globalization;

namespace MyApp.Converters
{
/// <summary>
/// Converts a boolean value to a Visibility value and vice versa.
/// </summary>
public class BoolToVisibilityConverter : IValueConverter
{
/// <summary>
/// Gets or sets a value indicating whether the conversion should be inverted.
/// When true, false becomes Visible and true becomes Collapsed.
/// </summary>
public bool Invert { get; set; }

/// <summary>
/// Converts a boolean value to a Visibility value.
/// </summary>
/// <param name="value">The boolean value to convert.</param>
/// <param name="targetType">The type of the target property.</param>
/// <param name="parameter">An optional parameter that can invert the conversion.</param>
/// <param name="culture">The culture to use for the conversion.</param>
/// <returns>
/// Visibility.Visible if the value is true (or false if Invert is true);
/// otherwise, Visibility.Collapsed.
/// </returns>
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
// Validate input
if (value is not bool boolValue)
{
return Visibility.Collapsed;
}

// Check if we should invert based on property or parameter
bool shouldInvert = Invert;

if (parameter is string paramString &&
paramString.Equals("Invert", StringComparison.OrdinalIgnoreCase))
{
shouldInvert = !shouldInvert; // Toggle inversion if parameter specified
}

// Apply the conversion logic
return (shouldInvert ? !boolValue : boolValue)
? Visibility.Visible
: Visibility.Collapsed;
}

/// <summary>
/// Converts a Visibility value back to a boolean value.
/// </summary>
/// <param name="value">The Visibility value to convert.</param>
/// <param name="targetType">The type of the target property.</param>
/// <param name="parameter">An optional parameter that can invert the conversion.</param>
/// <param name="culture">The culture to use for the conversion.</param>
/// <returns>
/// True if the value is Visibility.Visible (or Visibility.Collapsed if Invert is true);
/// otherwise, false.
/// </returns>
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
// Validate input
if (value is not Visibility visibility)
{
return false;
}

// Check if we should invert based on property or parameter
bool shouldInvert = Invert;

if (parameter is string paramString &&
paramString.Equals("Invert", StringComparison.OrdinalIgnoreCase))
{
shouldInvert = !shouldInvert; // Toggle inversion if parameter specified
}

// Apply the conversion logic
bool result = visibility == Visibility.Visible;
return shouldInvert ? !result : result;
}
}
}

Using Value Converters in XAML

To use a converter in XAML, you need to define it as a resource and then reference it in your binding:

<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:MyApp.Converters"
x:Class="MyApp.MainWindow">

<Window.Resources>
<!-- Define converters as resources -->
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:BoolToVisibilityConverter x:Key="InverseBoolToVisibilityConverter" Invert="True" />
<converters:StringToUpperConverter x:Key="StringToUpperConverter" />
<converters:DateTimeToStringConverter x:Key="DateTimeToStringConverter" Format="MMMM d, yyyy" />
</Window.Resources>

<StackPanel Margin="20" Spacing="10">
<!-- Using the converter in a binding -->
<CheckBox Content="Show Additional Options"
x:Name="ShowOptionsCheckBox"
IsChecked="False" />

<!-- Panel visible only when checkbox is checked -->
<Border Background="#f5f5f5"
Padding="15"
CornerRadius="4"
IsVisible="{Binding IsChecked, ElementName=ShowOptionsCheckBox,
Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Spacing="10">
<TextBlock Text="Additional Options" FontWeight="SemiBold" />
<CheckBox Content="Enable notifications" />
<CheckBox Content="Auto-save changes" />
</StackPanel>
</Border>

<!-- Message visible only when checkbox is NOT checked -->
<TextBlock Text="Check the box above to see additional options"
Foreground="#757575"
IsVisible="{Binding IsChecked, ElementName=ShowOptionsCheckBox,
Converter={StaticResource InverseBoolToVisibilityConverter}}" />

<!-- Using a string converter -->
<TextBlock Text="{Binding Username, Converter={StaticResource StringToUpperConverter}}"
FontWeight="Bold" />

<!-- Using a date converter -->
<TextBlock Text="{Binding LastLogin, Converter={StaticResource DateTimeToStringConverter}}" />
</StackPanel>
</Window>

Built-in Converters

Avalonia provides several built-in converters:

<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="using:Avalonia.Data.Converters"
x:Class="MyApp.MainWindow">

<Window.Resources>
<!-- Built-in converters -->
<conv:BoolConverters.Not x:Key="BoolNegationConverter" />
<conv:StringConverters.IsNullOrEmpty x:Key="IsNullOrEmptyConverter" />
<conv:ObjectConverters.IsNull x:Key="IsNullConverter" />
</Window.Resources>

<StackPanel>
<!-- Negate a boolean value -->
<CheckBox Content="Disable Feature"
x:Name="DisableFeatureCheckBox" />

<TextBox IsEnabled="{Binding IsChecked, ElementName=DisableFeatureCheckBox,
Converter={StaticResource BoolNegationConverter}}"
Watermark="Feature input..." />

<!-- Check if a string is null or empty -->
<TextBlock Text="Please enter your name"
Foreground="Red"
IsVisible="{Binding Name, Converter={StaticResource IsNullOrEmptyConverter}}" />
</StackPanel>
</Window>

Multi-Value Converters

For scenarios where you need to combine multiple values, use IMultiValueConverter:

public class FullNameConverter : IMultiValueConverter
{
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
{
if (values.Count >= 2 && values[0] is string firstName && values[1] is string lastName)
{
string middleName = values.Count > 2 && values[2] is string middle ? $" {middle} " : " ";
return $"{firstName}{middleName}{lastName}";
}

return string.Empty;
}
}
<Window.Resources>
<converters:FullNameConverter x:Key="FullNameConverter" />
</Window.Resources>

<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource FullNameConverter}">
<Binding Path="FirstName" />
<Binding Path="LastName" />
<Binding Path="MiddleName" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>

StringFormat

You can format bound values using the StringFormat parameter:

<TextBlock Text="{Binding Price, StringFormat=\{0:C\}}" />

FallbackValue and TargetNullValue

You can specify fallback values for when the binding fails or the source value is null:

<TextBlock Text="{Binding Name, FallbackValue=No name available}" />
<TextBlock Text="{Binding Address, TargetNullValue=No address provided}" />

MVVM Architecture

MVVM Pattern

The Model-View-ViewModel (MVVM) pattern is a design pattern that separates an application into three distinct components, creating a clean architecture that's maintainable, testable, and promotes separation of concerns.

MVVM Components

ComponentResponsibilityExample
ModelData structures and business logicDomain entities, data repositories, business rules
ViewUser interface and presentationXAML files, code-behind with minimal logic
ViewModelPresentation logic and state managementData for the view, commands, property change notifications

Key MVVM Principles

  1. Separation of Concerns: Each component has a specific responsibility
  2. Data Binding: Views bind to properties and commands exposed by ViewModels
  3. Testability: ViewModels can be tested independently of the UI
  4. Designer-Developer Workflow: Designers can work on Views while developers work on ViewModels

Model

The Model represents your application's data and business logic. It should be completely independent of the UI and contain no references to UI frameworks:

using System;

namespace MyAvaloniaApp.Models;

/// <summary>
/// Represents a person with basic personal information.
/// </summary>
/// <remarks>
/// This class demonstrates a simple model in the MVVM pattern.
/// It contains data and business logic but no UI-specific code.
/// </remarks>
public class Person
{
/// <summary>
/// Gets or sets the person's first name.
/// </summary>
public string FirstName { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the person's last name.
/// </summary>
public string LastName { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the person's date of birth.
/// </summary>
public DateTime DateOfBirth { get; set; } = DateTime.Today;

/// <summary>
/// Gets the person's age in years, calculated from their date of birth.
/// </summary>
/// <remarks>
/// This property demonstrates a calculated property that depends on other properties.
/// The age is calculated based on the current date and the person's date of birth,
/// accounting for whether their birthday has occurred this year.
/// </remarks>
public int Age
{
get
{
// Get the current date
var today = DateTime.Today;

// Calculate the age based on year difference
var age = today.Year - DateOfBirth.Year;

// Adjust age if the birthday hasn't occurred yet this year
// This handles the case where someone is born on December 31 and today is January 1
if (DateOfBirth.Date > today.AddYears(-age))
{
age--;
}

return age;
}
}

/// <summary>
/// Gets the person's full name, combining their first and last names.
/// </summary>
/// <remarks>
/// This is an example of an expression-bodied property that combines other properties.
/// </remarks>
public string FullName => $"{FirstName} {LastName}".Trim();

/// <summary>
/// Determines whether the person is an adult based on their age.
/// </summary>
/// <param name="adultAge">The age at which a person is considered an adult (default is 18).</param>
/// <returns>True if the person is an adult; otherwise, false.</returns>
public bool IsAdult(int adultAge = 18) => Age >= adultAge;

/// <summary>
/// Returns a string representation of the person.
/// </summary>
/// <returns>A string containing the person's full name and age.</returns>
public override string ToString() => $"{FullName}, Age: {Age}";
}

ViewModel

The ViewModel exposes data and commands to the View. It implements property change notification to support data binding:

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using MyAvaloniaApp.Models;

namespace MyAvaloniaApp.ViewModels;

/// <summary>
/// Base class for view models that implements property change notification.
/// </summary>
public abstract class ViewModelBase : INotifyPropertyChanged
{
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;

/// <summary>
/// Raises the PropertyChanged event for the specified property.
/// </summary>
/// <param name="propertyName">The name of the property that changed.</param>
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

/// <summary>
/// Raises the PropertyChanged event for multiple properties.
/// </summary>
/// <param name="propertyNames">The names of the properties that changed.</param>
protected void OnPropertyChanged(params string[] propertyNames)
{
if (propertyNames == null || propertyNames.Length == 0)
return;

foreach (var name in propertyNames)
{
OnPropertyChanged(name);
}
}
}

/// <summary>
/// View model for a Person object that provides property change notification.
/// </summary>
/// <remarks>
/// This class demonstrates the ViewModel part of the MVVM pattern.
/// It wraps a Person model and provides property change notification
/// to support data binding in the UI.
/// </remarks>
public class PersonViewModel : ViewModelBase
{
private readonly Person _person;

/// <summary>
/// Initializes a new instance of the <see cref="PersonViewModel"/> class.
/// </summary>
/// <param name="person">The person model to wrap.</param>
/// <exception cref="ArgumentNullException">Thrown if person is null.</exception>
public PersonViewModel(Person person)
{
_person = person ?? throw new ArgumentNullException(nameof(person));
}

/// <summary>
/// Gets or sets the person's first name.
/// </summary>
/// <remarks>
/// When this property changes, it also notifies that the FullName property has changed
/// since FullName depends on FirstName.
/// </remarks>
public string FirstName
{
get => _person.FirstName;
set
{
if (_person.FirstName != value)
{
_person.FirstName = value;
// Notify that both FirstName and FullName have changed
OnPropertyChanged(nameof(FirstName), nameof(FullName));
}
}
}

/// <summary>
/// Gets or sets the person's last name.
/// </summary>
/// <remarks>
/// When this property changes, it also notifies that the FullName property has changed
/// since FullName depends on LastName.
/// </remarks>
public string LastName
{
get => _person.LastName;
set
{
if (_person.LastName != value)
{
_person.LastName = value;
// Notify that both LastName and FullName have changed
OnPropertyChanged(nameof(LastName), nameof(FullName));
}
}
}

/// <summary>
/// Gets or sets the person's date of birth.
/// </summary>
/// <remarks>
/// When this property changes, it also notifies that the Age property has changed
/// since Age is calculated from DateOfBirth.
/// </remarks>
public DateTime DateOfBirth
{
get => _person.DateOfBirth;
set
{
if (_person.DateOfBirth != value)
{
_person.DateOfBirth = value;
// Notify that both DateOfBirth and Age have changed
OnPropertyChanged(nameof(DateOfBirth), nameof(Age));
}
}
}

/// <summary>
/// Gets the person's age in years.
/// </summary>
/// <remarks>
/// This is a read-only property that delegates to the underlying Person model.
/// </remarks>
public int Age => _person.Age;

/// <summary>
/// Gets the person's full name.
/// </summary>
/// <remarks>
/// This is a read-only property that delegates to the underlying Person model.
/// </remarks>
public string FullName => _person.FullName;

/// <summary>
/// Gets whether the person is an adult (18 years or older).
/// </summary>
public bool IsAdult => _person.IsAdult();

/// <summary>
/// Gets the underlying Person model.
/// </summary>
/// <remarks>
/// This property provides access to the wrapped model object.
/// Use with caution as changes to the model won't automatically
/// trigger property change notifications.
/// </remarks>
public Person Model => _person;
}

The ViewModelBase class shown above implements INotifyPropertyChanged, which is essential for data binding. It provides methods to raise property change notifications when properties in the view model change.

View

The View is defined in XAML and binds to properties and commands exposed by the ViewModel:

<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MyApp.Views.PersonView">

<StackPanel Spacing="10" Margin="20">
<TextBlock Text="{Binding FullName}" FontSize="20" FontWeight="Bold" />

<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto" VerticalAlignment="Top">
<TextBlock Grid.Row="0" Grid.Column="0" Text="First Name:" VerticalAlignment="Center" Margin="0,0,10,0" />
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding FirstName}" />

<TextBlock Grid.Row="1" Grid.Column="0" Text="Last Name:" VerticalAlignment="Center" Margin="0,0,10,0" />
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding LastName}" />

<TextBlock Grid.Row="2" Grid.Column="0" Text="Date of Birth:" VerticalAlignment="Center" Margin="0,0,10,0" />
<DatePicker Grid.Row="2" Grid.Column="1" SelectedDate="{Binding DateOfBirth}" />
</Grid>

<TextBlock Text="{Binding Age, StringFormat=Age: {0}}" />
</StackPanel>

</UserControl>

Benefits of MVVM

The MVVM pattern offers several benefits:

  1. Separation of concerns: UI logic is separated from business logic
  2. Testability: ViewModels can be tested independently of the UI
  3. Designer-developer workflow: Designers can work on the View while developers work on the ViewModel
  4. Code reuse: ViewModels can be reused across different Views

Commands and Actions

Commands provide a way to expose actions in the ViewModel that can be bound to UI elements like buttons.

ICommand Interface

Commands implement the ICommand interface:

public interface ICommand
{
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}

ReactiveCommand

Avalonia UI works well with ReactiveUI, which provides ReactiveCommand for implementing commands:

using System;
using System.Diagnostics;
using System.Reactive;
using System.Reactive.Linq;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;

namespace MyAvaloniaApp.ViewModels;

/// <summary>
/// View model for the main window demonstrating ReactiveUI features.
/// </summary>
/// <remarks>
/// This class shows how to use ReactiveUI's ReactiveCommand and derived properties
/// to implement the MVVM pattern in an Avalonia application.
/// </remarks>
public class MainWindowViewModel : ReactiveObject
{
// Using ReactiveUI.Fody.Helpers to automatically implement INotifyPropertyChanged
[Reactive] public string Name { get; set; } = string.Empty;

// This field will back our derived Greeting property
private readonly ObservableAsPropertyHelper<string> _greeting;

/// <summary>
/// Gets the command that executes the greeting action.
/// </summary>
/// <remarks>
/// ReactiveCommand is a powerful implementation of ICommand from ReactiveUI.
/// It supports observables for determining when the command can execute,
/// and it can return a result when executed.
/// </remarks>
public ReactiveCommand<Unit, Unit> SayHelloCommand { get; }

/// <summary>
/// Initializes a new instance of the <see cref="MainWindowViewModel"/> class.
/// </summary>
public MainWindowViewModel()
{
// Create an observable that determines when the command can execute
// The command will be enabled only when Name is not null or empty
var canExecute = this.WhenAnyValue(
x => x.Name,
name => !string.IsNullOrWhiteSpace(name)
);

// Create the command with the execution logic and can-execute condition
SayHelloCommand = ReactiveCommand.Create(ExecuteSayHello, canExecute);

// Create a derived property that updates automatically when the command executes
// This is a powerful ReactiveUI feature that creates properties derived from observables
_greeting = SayHelloCommand
.Select(_ => $"Hello, {Name}!")
.ToProperty(this, x => x.Greeting);

// Handle errors that might occur during command execution
SayHelloCommand.ThrownExceptions
.Subscribe(ex => Debug.WriteLine($"Error in SayHelloCommand: {ex.Message}"));
}

/// <summary>
/// Gets the greeting message derived from the command execution.
/// </summary>
/// <remarks>
/// This is a derived property that updates automatically when SayHelloCommand executes.
/// It demonstrates how to create properties that depend on command execution in ReactiveUI.
/// </remarks>
public string Greeting => _greeting.Value;

/// <summary>
/// Executes the greeting action.
/// </summary>
/// <remarks>
/// This method is called when SayHelloCommand is executed.
/// It demonstrates a simple action that uses the current Name property.
/// </remarks>
private void ExecuteSayHello()
{
// Log the greeting message
Debug.WriteLine($"Hello, {Name}!");

// In a real application, you might:
// - Update other properties
// - Call a service
// - Navigate to another view
// - Show a dialog
}
}

Binding to the command in XAML:

<StackPanel>
<TextBox Text="{Binding Name}" Watermark="Enter your name" />
<Button Content="Say Hello" Command="{Binding SayHelloCommand}" />
<TextBlock Text="{Binding Greeting}" />
</StackPanel>

Command Parameters

You can pass parameters to commands:

using System;
using System.Diagnostics;
using System.Reactive;
using ReactiveUI;

namespace MyAvaloniaApp.ViewModels;

/// <summary>
/// View model demonstrating commands with parameters.
/// </summary>
public class ItemsViewModel : ReactiveObject
{
/// <summary>
/// Gets the command that deletes an item by its ID.
/// </summary>
/// <remarks>
/// This command accepts a string parameter (the item ID) and returns no result (Unit).
/// The generic type parameters specify:
/// - string: The type of the parameter the command accepts
/// - Unit: The type of result the command produces (Unit means no result)
/// </remarks>
public ReactiveCommand<string, Unit> DeleteItemCommand { get; }

/// <summary>
/// Initializes a new instance of the <see cref="ItemsViewModel"/> class.
/// </summary>
public ItemsViewModel()
{
// Create a command that accepts a string parameter (item ID)
DeleteItemCommand = ReactiveCommand.Create<string>(ExecuteDeleteItem);

// You can also add a can-execute condition that depends on the parameter
// For example, to prevent deleting items with IDs that start with "protected-":
/*
DeleteItemCommand = ReactiveCommand.Create<string>(
ExecuteDeleteItem,
this.WhenAnyObservable(
x => x.DeleteItemCommand.CanExecute,
(itemId) => itemId != null && !itemId.StartsWith("protected-")
)
);
*/

// Handle exceptions that might occur during command execution
DeleteItemCommand.ThrownExceptions.Subscribe(ex =>
Debug.WriteLine($"Error deleting item: {ex.Message}")
);
}

/// <summary>
/// Executes the delete action for an item with the specified ID.
/// </summary>
/// <param name="itemId">The ID of the item to delete.</param>
/// <exception cref="ArgumentException">Thrown if itemId is null or empty.</exception>
private void ExecuteDeleteItem(string itemId)
{
// Validate the parameter
if (string.IsNullOrEmpty(itemId))
{
throw new ArgumentException("Item ID cannot be null or empty", nameof(itemId));
}

// Log the deletion
Debug.WriteLine($"Deleting item with ID: {itemId}");

// In a real application, you would:
// 1. Call a service to delete the item from a database or API
// 2. Remove the item from a collection in the view model
// 3. Update the UI to reflect the deletion
// 4. Handle any errors that might occur
}
}

Binding with a parameter:

<Button Content="Delete" 
Command="{Binding DeleteItemCommand}"
CommandParameter="{Binding Id}" />

DelegateCommand

If you're not using ReactiveUI, you can implement a simple DelegateCommand:

using System;
using System.Windows.Input;

namespace MyAvaloniaApp.Commands;

/// <summary>
/// A command implementation that delegates its actions to provided methods.
/// </summary>
/// <remarks>
/// This is a simple implementation of ICommand that can be used when you're not using ReactiveUI.
/// It allows you to specify the execute and can-execute logic as delegates.
/// </remarks>
public class DelegateCommand : ICommand
{
private readonly Action<object?> _execute;
private readonly Func<object?, bool>? _canExecute;

/// <summary>
/// Occurs when changes occur that affect whether the command can execute.
/// </summary>
public event EventHandler? CanExecuteChanged;

/// <summary>
/// Initializes a new instance of the <see cref="DelegateCommand"/> class.
/// </summary>
/// <param name="execute">The action to execute when the command is invoked.</param>
/// <param name="canExecute">A function that determines whether the command can execute.</param>
/// <exception cref="ArgumentNullException">Thrown if the execute action is null.</exception>
public DelegateCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}

/// <summary>
/// Creates a command that does not take a parameter.
/// </summary>
/// <param name="execute">The action to execute when the command is invoked.</param>
/// <param name="canExecute">A function that determines whether the command can execute.</param>
/// <returns>A new DelegateCommand instance.</returns>
public static DelegateCommand Create(Action execute, Func<bool>? canExecute = null)
{
if (execute == null)
throw new ArgumentNullException(nameof(execute));

return new DelegateCommand(
_ => execute(),
canExecute == null ? null : _ => canExecute());
}

/// <summary>
/// Creates a command that takes a parameter of type T.
/// </summary>
/// <typeparam name="T">The type of the command parameter.</typeparam>
/// <param name="execute">The action to execute when the command is invoked.</param>
/// <param name="canExecute">A function that determines whether the command can execute.</param>
/// <returns>A new DelegateCommand instance.</returns>
public static DelegateCommand Create<T>(Action<T> execute, Func<T, bool>? canExecute = null)
{
if (execute == null)
throw new ArgumentNullException(nameof(execute));

return new DelegateCommand(
o => execute(o is T t ? t : default!),
canExecute == null ? null : o => o is T t ? canExecute(t) : false);
}

/// <summary>
/// Determines whether the command can execute in its current state.
/// </summary>
/// <param name="parameter">Data used by the command.</param>
/// <returns>true if this command can be executed; otherwise, false.</returns>
public bool CanExecute(object? parameter)
{
return _canExecute == null || _canExecute(parameter);
}

/// <summary>
/// Executes the command.
/// </summary>
/// <param name="parameter">Data used by the command.</param>
public void Execute(object? parameter)
{
if (CanExecute(parameter))
{
_execute(parameter);
}
}

/// <summary>
/// Raises the CanExecuteChanged event.
/// </summary>
/// <remarks>
/// Call this method when the can-execute state of the command changes.
/// This will cause the UI to re-evaluate whether the command can execute.
/// </remarks>
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}

Observable Collections

Observable collections notify the UI when items are added, removed, or replaced, enabling automatic updates to bound controls.

ObservableCollection<T>

The standard ObservableCollection<T> class implements INotifyCollectionChanged and INotifyPropertyChanged:

using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Reactive;
using ReactiveUI;
using MyAvaloniaApp.Models;

namespace MyAvaloniaApp.ViewModels;

/// <summary>
/// View model for managing a collection of people.
/// </summary>
/// <remarks>
/// This class demonstrates how to use ObservableCollection with ReactiveUI
/// to create a view model that manages a collection of items.
/// </remarks>
public class PeopleViewModel : ReactiveObject
{
private ObservableCollection<Person> _people;

/// <summary>
/// Initializes a new instance of the <see cref="PeopleViewModel"/> class.
/// </summary>
public PeopleViewModel()
{
// Initialize the collection with some sample data
_people = new ObservableCollection<Person>
{
new Person
{
FirstName = "John",
LastName = "Doe",
DateOfBirth = new DateTime(1980, 1, 1)
},
new Person
{
FirstName = "Jane",
LastName = "Smith",
DateOfBirth = new DateTime(1985, 5, 10)
}
};

// Create commands for adding and removing people
AddPersonCommand = ReactiveCommand.Create(ExecuteAddPerson);

// The RemovePersonCommand requires a Person parameter
RemovePersonCommand = ReactiveCommand.Create<Person>(ExecuteRemovePerson);

// Subscribe to collection changes to log them
// This is optional but useful for debugging
_people.CollectionChanged += (sender, e) =>
{
switch (e.Action)
{
case System.Collections.Specialized.NotifyCollectionChangedAction.Add:
Debug.WriteLine($"Added {e.NewItems?.Count ?? 0} item(s) at index {e.NewStartingIndex}");
break;
case System.Collections.Specialized.NotifyCollectionChangedAction.Remove:
Debug.WriteLine($"Removed {e.OldItems?.Count ?? 0} item(s) at index {e.OldStartingIndex}");
break;
case System.Collections.Specialized.NotifyCollectionChangedAction.Replace:
Debug.WriteLine($"Replaced items at index {e.NewStartingIndex}");
break;
case System.Collections.Specialized.NotifyCollectionChangedAction.Move:
Debug.WriteLine($"Moved items from index {e.OldStartingIndex} to {e.NewStartingIndex}");
break;
case System.Collections.Specialized.NotifyCollectionChangedAction.Reset:
Debug.WriteLine("Collection was reset");
break;
}
};
}

/// <summary>
/// Gets or sets the collection of people.
/// </summary>
/// <remarks>
/// ObservableCollection implements INotifyCollectionChanged, which notifies the UI
/// when items are added, removed, or replaced in the collection.
/// </remarks>
public ObservableCollection<Person> People
{
get => _people;
set => this.RaiseAndSetIfChanged(ref _people, value);
}

/// <summary>
/// Gets the command that adds a new person to the collection.
/// </summary>
public ReactiveCommand<Unit, Unit> AddPersonCommand { get; }

/// <summary>
/// Gets the command that removes a person from the collection.
/// </summary>
public ReactiveCommand<Person, Unit> RemovePersonCommand { get; }

/// <summary>
/// Executes the add person action.
/// </summary>
private void ExecuteAddPerson()
{
// Create a new person with default values
var newPerson = new Person
{
FirstName = "New",
LastName = "Person",
DateOfBirth = DateTime.Today.AddYears(-30)
};

// Add the person to the collection
// This will automatically update the UI because ObservableCollection
// raises the CollectionChanged event when items are added
People.Add(newPerson);

Debug.WriteLine($"Added new person: {newPerson.FullName}");
}

/// <summary>
/// Executes the remove person action.
/// </summary>
/// <param name="person">The person to remove from the collection.</param>
private void ExecuteRemovePerson(Person person)
{
if (person == null)
{
Debug.WriteLine("Cannot remove null person");
return;
}

// Remove the person from the collection
// This will automatically update the UI because ObservableCollection
// raises the CollectionChanged event when items are removed
bool removed = People.Remove(person);

if (removed)
{
Debug.WriteLine($"Removed person: {person.FullName}");
}
else
{
Debug.WriteLine($"Person not found in collection: {person.FullName}");
}
}

/// <summary>
/// Clears all people from the collection.
/// </summary>
public void ClearPeople()
{
People.Clear();
Debug.WriteLine("Cleared all people from collection");
}
}

Binding to the collection in XAML:

<StackPanel>
<Button Content="Add Person" Command="{Binding AddPersonCommand}" />

<ListBox Items="{Binding People}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="{Binding FullName}" VerticalAlignment="Center" />
<Button Grid.Column="1" Content="Remove"
Command="{Binding $parent[ListBox].DataContext.RemovePersonCommand}"
CommandParameter="{Binding}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>

ReactiveList<T>

ReactiveUI provides ReactiveList&lt;T&gt;, which offers additional features like change tracking and filtering:

using System;
using System.Diagnostics;
using System.Reactive.Linq;
using DynamicData;
using ReactiveUI;
using MyAvaloniaApp.Models;

namespace MyAvaloniaApp.ViewModels;

/// <summary>
/// View model for managing a collection of people using ReactiveUI's DynamicData.
/// </summary>
/// <remarks>
/// This class demonstrates how to use DynamicData (the modern replacement for ReactiveList)
/// to create a view model that manages a collection of items with advanced features.
/// </remarks>
public class PeopleViewModel : ReactiveObject
{
// Source list for managing the collection
private readonly SourceList<Person> _peopleSource;

// ReadOnlyObservableCollection that will be exposed to the view
private readonly ReadOnlyObservableCollection<Person> _people;

/// <summary>
/// Initializes a new instance of the <see cref="PeopleViewModel"/> class.
/// </summary>
public PeopleViewModel()
{
// Initialize the source list
_peopleSource = new SourceList<Person>();

// Add initial data
_peopleSource.Edit(list =>
{
list.Add(new Person { FirstName = "John", LastName = "Doe", DateOfBirth = new DateTime(1980, 1, 1) });
list.Add(new Person { FirstName = "Jane", LastName = "Smith", DateOfBirth = new DateTime(1985, 5, 10) });
});

// Connect the source list to create a read-only observable collection
_peopleSource.Connect()
// Transform the items if needed (e.g., to PersonViewModels)
//.Transform(person => new PersonViewModel(person))

// Filter items if needed
//.Filter(person => person.Age >= 18)

// Sort items if needed
.Sort(SortExpressionComparer<Person>.Ascending(p => p.LastName).ThenByAscending(p => p.FirstName))

// Bind to the observable collection
.Bind(out _people)

// Subscribe to changes
.Subscribe(changes =>
{
foreach (var change in changes)
{
switch (change.Reason)
{
case ListChangeReason.Add:
Debug.WriteLine($"Added: {change.Item.Current.FullName}");
break;
case ListChangeReason.Remove:
Debug.WriteLine($"Removed: {change.Item.Current.FullName}");
break;
case ListChangeReason.Moved:
Debug.WriteLine($"Moved: {change.Item.Current.FullName}");
break;
case ListChangeReason.Update:
Debug.WriteLine($"Updated: {change.Item.Current.FullName}");
break;
}
}
});
}

/// <summary>
/// Gets the read-only collection of people.
/// </summary>
/// <remarks>
/// This is a read-only collection that automatically updates when the source list changes.
/// It's the recommended approach for exposing collections in ReactiveUI.
/// </remarks>
public ReadOnlyObservableCollection<Person> People => _people;

/// <summary>
/// Adds a new person to the collection.
/// </summary>
/// <param name="person">The person to add.</param>
public void AddPerson(Person person)
{
if (person == null)
throw new ArgumentNullException(nameof(person));

_peopleSource.Edit(list => list.Add(person));
}

/// <summary>
/// Removes a person from the collection.
/// </summary>
/// <param name="person">The person to remove.</param>
/// <returns>True if the person was removed; otherwise, false.</returns>
public bool RemovePerson(Person person)
{
if (person == null)
return false;

bool removed = false;
_peopleSource.Edit(list => removed = list.Remove(person));
return removed;
}

/// <summary>
/// Updates a person in the collection.
/// </summary>
/// <param name="person">The person to update.</param>
public void UpdatePerson(Person person)
{
if (person == null)
return;

_peopleSource.Edit(list =>
{
// Find the person by ID or other unique identifier
var index = list.FindIndex(p => p.FullName == person.FullName);
if (index >= 0)
{
// Replace the person at the found index
list.RemoveAt(index);
list.Insert(index, person);
}
});
}

/// <summary>
/// Clears all people from the collection.
/// </summary>
public void ClearPeople()
{
_peopleSource.Edit(list => list.Clear());
}
}

Example: MVVM Contact Manager

Let's put everything together in a simple contact manager application:

Model

using System;

namespace MyAvaloniaApp.Models;

/// <summary>
/// Represents a contact with basic contact information.
/// </summary>
/// <remarks>
/// This class demonstrates a simple model in the MVVM pattern.
/// It contains data and business logic but no UI-specific code.
/// </remarks>
public class Contact
{
/// <summary>
/// Gets or sets the unique identifier for the contact.
/// </summary>
/// <remarks>
/// The ID is automatically generated when a new contact is created.
/// </remarks>
public string Id { get; set; } = Guid.NewGuid().ToString();

/// <summary>
/// Gets or sets the contact's first name.
/// </summary>
public string FirstName { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the contact's last name.
/// </summary>
public string LastName { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the contact's email address.
/// </summary>
public string Email { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the contact's phone number.
/// </summary>
public string Phone { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the contact's address.
/// </summary>
public string Address { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the contact's notes.
/// </summary>
public string Notes { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the date when the contact was created.
/// </summary>
public DateTime CreatedDate { get; set; } = DateTime.Now;

/// <summary>
/// Gets or sets the date when the contact was last modified.
/// </summary>
public DateTime ModifiedDate { get; set; } = DateTime.Now;

/// <summary>
/// Gets the contact's full name, combining their first and last names.
/// </summary>
/// <remarks>
/// This is an example of a calculated property that depends on other properties.
/// </remarks>
public string FullName => $"{FirstName} {LastName}".Trim();

/// <summary>
/// Gets a display-friendly version of the contact's email address.
/// </summary>
/// <remarks>
/// Returns the email address if available; otherwise, returns "No email".
/// </remarks>
public string DisplayEmail => string.IsNullOrEmpty(Email) ? "No email" : Email;

/// <summary>
/// Gets a display-friendly version of the contact's phone number.
/// </summary>
/// <remarks>
/// Returns the phone number if available; otherwise, returns "No phone".
/// </remarks>
public string DisplayPhone => string.IsNullOrEmpty(Phone) ? "No phone" : Phone;

/// <summary>
/// Determines whether the contact has valid email and phone information.
/// </summary>
/// <returns>True if either the email or phone is not empty; otherwise, false.</returns>
public bool HasContactInfo() => !string.IsNullOrEmpty(Email) || !string.IsNullOrEmpty(Phone);

/// <summary>
/// Updates the modified date to the current date and time.
/// </summary>
public void UpdateModifiedDate()
{
ModifiedDate = DateTime.Now;
}

/// <summary>
/// Returns a string representation of the contact.
/// </summary>
/// <returns>A string containing the contact's full name and email.</returns>
public override string ToString() => $"{FullName} ({DisplayEmail})";
}

ViewModel

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using ReactiveUI;
using MyAvaloniaApp.Models;

namespace MyAvaloniaApp.ViewModels;

/// <summary>
/// View model for a single contact that provides property change notification.
/// </summary>
/// <remarks>
/// This class demonstrates the ViewModel part of the MVVM pattern.
/// It wraps a Contact model and provides property change notification
/// to support data binding in the UI.
/// </remarks>
public class ContactViewModel : ReactiveObject
{
private readonly Contact _contact;

/// <summary>
/// Initializes a new instance of the <see cref="ContactViewModel"/> class.
/// </summary>
/// <param name="contact">The contact model to wrap.</param>
/// <exception cref="ArgumentNullException">Thrown if contact is null.</exception>
public ContactViewModel(Contact contact)
{
_contact = contact ?? throw new ArgumentNullException(nameof(contact));
}

/// <summary>
/// Gets the unique identifier for the contact.
/// </summary>
public string Id => _contact.Id;

/// <summary>
/// Gets or sets the contact's first name.
/// </summary>
/// <remarks>
/// When this property changes, it also notifies that the FullName property has changed
/// since FullName depends on FirstName.
/// </remarks>
public string FirstName
{
get => _contact.FirstName;
set
{
if (_contact.FirstName != value)
{
_contact.FirstName = value;
_contact.UpdateModifiedDate();
this.RaisePropertyChanged();
this.RaisePropertyChanged(nameof(FullName));
this.RaisePropertyChanged(nameof(ModifiedDate));
}
}
}

/// <summary>
/// Gets or sets the contact's last name.
/// </summary>
/// <remarks>
/// When this property changes, it also notifies that the FullName property has changed
/// since FullName depends on LastName.
/// </remarks>
public string LastName
{
get => _contact.LastName;
set
{
if (_contact.LastName != value)
{
_contact.LastName = value;
_contact.UpdateModifiedDate();
this.RaisePropertyChanged();
this.RaisePropertyChanged(nameof(FullName));
this.RaisePropertyChanged(nameof(ModifiedDate));
}
}
}

/// <summary>
/// Gets or sets the contact's email address.
/// </summary>
/// <remarks>
/// When this property changes, it also notifies that the DisplayEmail property has changed
/// since DisplayEmail depends on Email.
/// </remarks>
public string Email
{
get => _contact.Email;
set
{
if (_contact.Email != value)
{
_contact.Email = value;
_contact.UpdateModifiedDate();
this.RaisePropertyChanged();
this.RaisePropertyChanged(nameof(DisplayEmail));
this.RaisePropertyChanged(nameof(HasContactInfo));
this.RaisePropertyChanged(nameof(ModifiedDate));
}
}
}

/// <summary>
/// Gets or sets the contact's phone number.
/// </summary>
/// <remarks>
/// When this property changes, it also notifies that the DisplayPhone property has changed
/// since DisplayPhone depends on Phone.
/// </remarks>
public string Phone
{
get => _contact.Phone;
set
{
if (_contact.Phone != value)
{
_contact.Phone = value;
_contact.UpdateModifiedDate();
this.RaisePropertyChanged();
this.RaisePropertyChanged(nameof(DisplayPhone));
this.RaisePropertyChanged(nameof(HasContactInfo));
this.RaisePropertyChanged(nameof(ModifiedDate));
}
}
}

/// <summary>
/// Gets or sets the contact's address.
/// </summary>
public string Address
{
get => _contact.Address;
set
{
if (_contact.Address != value)
{
_contact.Address = value;
_contact.UpdateModifiedDate();
this.RaisePropertyChanged();
this.RaisePropertyChanged(nameof(ModifiedDate));
}
}
}

/// <summary>
/// Gets or sets the contact's notes.
/// </summary>
public string Notes
{
get => _contact.Notes;
set
{
if (_contact.Notes != value)
{
_contact.Notes = value;
_contact.UpdateModifiedDate();
this.RaisePropertyChanged();
this.RaisePropertyChanged(nameof(ModifiedDate));
}
}
}

/// <summary>
/// Gets the date when the contact was created.
/// </summary>
public DateTime CreatedDate => _contact.CreatedDate;

/// <summary>
/// Gets the date when the contact was last modified.
/// </summary>
public DateTime ModifiedDate => _contact.ModifiedDate;

/// <summary>
/// Gets the contact's full name.
/// </summary>
public string FullName => _contact.FullName;

/// <summary>
/// Gets a display-friendly version of the contact's email address.
/// </summary>
public string DisplayEmail => _contact.DisplayEmail;

/// <summary>
/// Gets a display-friendly version of the contact's phone number.
/// </summary>
public string DisplayPhone => _contact.DisplayPhone;

/// <summary>
/// Gets a value indicating whether the contact has valid contact information.
/// </summary>
public bool HasContactInfo => _contact.HasContactInfo();

/// <summary>
/// Gets the underlying Contact model.
/// </summary>
public Contact Model => _contact;
}

/// <summary>
/// View model for managing a list of contacts.
/// </summary>
/// <remarks>
/// This class demonstrates a more complex view model that manages a collection of items
/// and provides filtering and selection functionality.
/// </remarks>
public class ContactListViewModel : ReactiveObject
{
private ObservableCollection<ContactViewModel> _contacts;
private ContactViewModel? _selectedContact;
private string _searchText = string.Empty;
private readonly ObservableAsPropertyHelper<IEnumerable<ContactViewModel>> _filteredContacts;

/// <summary>
/// Initializes a new instance of the <see cref="ContactListViewModel"/> class.
/// </summary>
public ContactListViewModel()
{
// Initialize the contacts collection with sample data
_contacts = new ObservableCollection<ContactViewModel>
{
new ContactViewModel(new Contact
{
FirstName = "John",
LastName = "Doe",
Email = "john@example.com",
Phone = "555-1234",
Address = "123 Main St, Anytown, USA",
Notes = "Met at conference"
}),
new ContactViewModel(new Contact
{
FirstName = "Jane",
LastName = "Smith",
Email = "jane@example.com",
Phone = "555-5678",
Address = "456 Oak Ave, Somewhere, USA",
Notes = "Referred by Bob"
}),
new ContactViewModel(new Contact
{
FirstName = "Bob",
LastName = "Johnson",
Email = "bob@example.com",
Phone = "555-9012",
Address = "789 Pine St, Nowhere, USA",
Notes = "College friend"
})
};

// Create commands for adding and removing contacts
AddContactCommand = ReactiveCommand.Create(ExecuteAddContact);

// The RemoveContactCommand is enabled only when a contact is selected
RemoveContactCommand = ReactiveCommand.Create(
ExecuteRemoveContact,
this.WhenAnyValue(x => x.SelectedContact, contact => contact != null)
);

// Create a derived property for filtered contacts
// This property updates automatically when SearchText or Contacts changes
_filteredContacts = this.WhenAnyValue(
x => x.SearchText,
x => x.Contacts
)
.Select(x => FilterContacts(x.Item1, x.Item2))
.ToProperty(this, x => x.FilteredContacts);

// Handle errors that might occur during command execution
AddContactCommand.ThrownExceptions.Subscribe(ex =>
Debug.WriteLine($"Error adding contact: {ex.Message}")
);

RemoveContactCommand.ThrownExceptions.Subscribe(ex =>
Debug.WriteLine($"Error removing contact: {ex.Message}")
);
}

/// <summary>
/// Gets or sets the collection of contacts.
/// </summary>
public ObservableCollection<ContactViewModel> Contacts
{
get => _contacts;
set => this.RaiseAndSetIfChanged(ref _contacts, value);
}

/// <summary>
/// Gets the filtered collection of contacts based on the search text.
/// </summary>
/// <remarks>
/// This is a derived property that updates automatically when SearchText or Contacts changes.
/// </remarks>
public IEnumerable<ContactViewModel> FilteredContacts => _filteredContacts.Value;

/// <summary>
/// Gets or sets the currently selected contact.
/// </summary>
public ContactViewModel? SelectedContact
{
get => _selectedContact;
set => this.RaiseAndSetIfChanged(ref _selectedContact, value);
}

/// <summary>
/// Gets or sets the search text used to filter contacts.
/// </summary>
public string SearchText
{
get => _searchText;
set => this.RaiseAndSetIfChanged(ref _searchText, value);
}

/// <summary>
/// Gets the command that adds a new contact.
/// </summary>
public ReactiveCommand<Unit, Unit> AddContactCommand { get; }

/// <summary>
/// Gets the command that removes the selected contact.
/// </summary>
public ReactiveCommand<Unit, Unit> RemoveContactCommand { get; }

/// <summary>
/// Executes the add contact action.
/// </summary>
private void ExecuteAddContact()
{
// Create a new contact with default values
var newContact = new ContactViewModel(new Contact
{
FirstName = "New",
LastName = "Contact",
CreatedDate = DateTime.Now,
ModifiedDate = DateTime.Now
});

// Add the contact to the collection
Contacts.Add(newContact);

// Select the new contact
SelectedContact = newContact;

Debug.WriteLine($"Added new contact: {newContact.FullName}");
}

/// <summary>
/// Executes the remove contact action.
/// </summary>
private void ExecuteRemoveContact()
{
if (SelectedContact == null)
{
Debug.WriteLine("No contact selected to remove");
return;
}

// Store the contact information for logging
var contactName = SelectedContact.FullName;

// Remove the contact from the collection
Contacts.Remove(SelectedContact);

// Clear the selection
SelectedContact = null;

Debug.WriteLine($"Removed contact: {contactName}");
}

/// <summary>
/// Filters the contacts based on the search text.
/// </summary>
/// <param name="searchText">The text to search for.</param>
/// <param name="contacts">The collection of contacts to filter.</param>
/// <returns>A filtered collection of contacts.</returns>
private IEnumerable<ContactViewModel> FilterContacts(string searchText, ObservableCollection<ContactViewModel> contacts)
{
// If the search text is empty, return all contacts
if (string.IsNullOrWhiteSpace(searchText))
return contacts;

// Convert the search text to lowercase for case-insensitive comparison
var search = searchText.ToLowerInvariant();

// Filter the contacts based on the search text
return contacts.Where(c =>
c.FullName.ToLowerInvariant().Contains(search) ||
c.Email.ToLowerInvariant().Contains(search) ||
c.Phone.ToLowerInvariant().Contains(search) ||
c.Address.ToLowerInvariant().Contains(search) ||
c.Notes.ToLowerInvariant().Contains(search)
);
}
}

View

<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MyApp.ViewModels"
x:Class="MyApp.Views.MainWindow"
Title="Contact Manager"
Width="800" Height="600">

<Window.DataContext>
<vm:ContactListViewModel />
</Window.DataContext>

<Grid ColumnDefinitions="250,*" RowDefinitions="Auto,*">
<!-- Search and Add/Remove buttons -->
<Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="*,Auto" Margin="10">
<TextBox Grid.Column="0" Text="{Binding SearchText}" Watermark="Search contacts..." />
<Button Grid.Column="1" Content="+" Command="{Binding AddContactCommand}" Margin="5,0,0,0" />
</Grid>

<!-- Contact list -->
<ListBox Grid.Row="1" Grid.Column="0"
Items="{Binding FilteredContacts}"
SelectedItem="{Binding SelectedContact}"
Margin="10">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FullName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

<!-- Contact details -->
<Grid Grid.Row="0" Grid.RowSpan="2" Grid.Column="1"
RowDefinitions="Auto,*,Auto" Margin="10"
IsVisible="{Binding SelectedContact, Converter={x:Static ObjectConverters.IsNotNull}}">

<TextBlock Grid.Row="0" Text="Contact Details" FontSize="20" FontWeight="Bold" Margin="0,0,0,10" />

<Grid Grid.Row="1" RowDefinitions="Auto,Auto,Auto,Auto" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0" Text="First Name:" VerticalAlignment="Center" Margin="0,0,10,10" />
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding SelectedContact.FirstName}" Margin="0,0,0,10" />

<TextBlock Grid.Row="1" Grid.Column="0" Text="Last Name:" VerticalAlignment="Center" Margin="0,0,10,10" />
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding SelectedContact.LastName}" Margin="0,0,0,10" />

<TextBlock Grid.Row="2" Grid.Column="0" Text="Email:" VerticalAlignment="Center" Margin="0,0,10,10" />
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding SelectedContact.Email}" Margin="0,0,0,10" />

<TextBlock Grid.Row="3" Grid.Column="0" Text="Phone:" VerticalAlignment="Center" Margin="0,0,10,10" />
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding SelectedContact.Phone}" Margin="0,0,0,10" />
</Grid>

<Button Grid.Row="2" Content="Remove Contact"
Command="{Binding RemoveContactCommand}"
HorizontalAlignment="Right" />
</Grid>

<!-- No contact selected message -->
<TextBlock Grid.Row="0" Grid.RowSpan="2" Grid.Column="1"
Text="Select a contact to view details"
HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding SelectedContact, Converter={x:Static ObjectConverters.IsNull}}" />
</Grid>

</Window>

This example demonstrates:

  • Data binding with different modes
  • Commands for user actions
  • Observable collections for dynamic lists
  • Filtering data based on user input
  • MVVM pattern with clear separation of concerns

In the next section, we'll explore styling and theming in Avalonia UI.