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

Today, we’re diving deep into state management with the Community Toolkit MVVM – the tool that’ll transform your chaotic code into a well-orchestrated symphony. By the end of this guide, you’ll be confidently sharing data between views like a seasoned developer.


Fire up JetBrains Rider and create a new Avalonia project with MVVM template.
Let’s organize our project properly. Create these folders in your project:
YourProject/
├── Assets/
│ ├── Icons.axaml
├── Models/
├── ViewModels/
│ ├── MainWindowViewModel.cs
│ ├── LoginViewModel.cs
│ ├── DashboardViewModel.cs
│ ├── ViewModelBase.cs
├── Views/
│ ├── MainWindow.axaml
│ ├── LoginView.axaml
│ ├── DashboardView.axaml
├── Services/
│ ├── UserStateService.cs
└── Converters/
└── BoolToTextConverter.cs
This structure isn’t just for show – it’ll save your sanity as your project grows.
State management starts with a service that multiple ViewModels can access. Think of it as a central information hub where all your views can grab the data they need.
Create Services/UserStateService.cs:
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.ObjectModel;
using YourProject.Models;
namespace YourProject.Services;
public partial class UserStateService : ViewModelBase
{
[ObservableProperty]
private string _currentUserName = string.Empty;
[ObservableProperty]
private bool _isUserLoggedIn = false;
[ObservableProperty]
private ObservableCollection<string> _recentActivities = new();
public void LoginUser(string username)
{
CurrentUserName = username;
IsUserLoggedIn = true;
AddActivity($"User {username} logged in");
}
public void LogoutUser()
{
var username = CurrentUserName;
CurrentUserName = string.Empty;
IsUserLoggedIn = false;
AddActivity($"User {username} logged out");
}
private void AddActivity(string activity)
{
RecentActivities.Insert(0, $"{DateTime.Now:HH:mm:ss} - {activity}");
// Keep only last 10 activities
while (RecentActivities.Count > 10)
{
RecentActivities.RemoveAt(RecentActivities.Count - 1);
}
OnPropertyChanged(nameof(RecentActivities));
}
public void AddCustomActivity(string activity)
{
AddActivity(activity);
}
}
The [ObservableProperty] attribute is Community Toolkit’s magic wand – it automatically generates property change notifications for you. No more writing those tedious PropertyChanged events manually!
In App.axaml.cs, set up dependency injection:
using Microsoft.Extensions.DependencyInjection;
using YourProject.Services;
using YourProject.ViewModels;
public partial class App : Application
{
public static ServiceProvider? ServiceProvider { get; private set; }
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
var services = new ServiceCollection();
services.AddScoped<UserStateService>();
services.AddTransient<MainWindowViewModel>();
services.AddTransient<LoginViewModel>();
services.AddTransient<DashboardViewModel>();
ServiceProvider = services.BuildServiceProvider();
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
BindingPlugins.DataValidators.RemoveAt(0);
DisableAvaloniaDataAnnotationValidation();
desktop.MainWindow = new MainWindow
{
DataContext = ServiceProvider?.GetService<MainWindowViewModel>(),
};
}
base.OnFrameworkInitializationCompleted();
}
}
Now comes the fun part – building ViewModels that can communicate with each other through our shared state service.
Create ViewModels/LoginViewModel.cs:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using YourProject.Services;
namespace YourProject.ViewModels;
public partial class LoginViewModel : ViewModelBase
{
private readonly UserStateService _userStateService;
[ObservableProperty]
private string _username = string.Empty;
[ObservableProperty]
private string _password = string.Empty;
[ObservableProperty]
private bool _isLoggingIn = false;
[ObservableProperty]
private string _errorMessage = string.Empty;
public LoginViewModel(UserStateService userStateService)
{
_userStateService = userStateService;
}
public LoginViewModel() : this(null!) { }
[RelayCommand]
private async Task LoginAsync()
{
if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password))
{
ErrorMessage = "Please enter both username and password.";
return;
}
IsLoggingIn = true;
ErrorMessage = string.Empty;
try
{
// simulating an API call
await Task.Delay(1500);
if (Password == "demo123")
_userStateService.LoginUser(Username);
else
ErrorMessage = "Invalid credentials.";
}
catch (Exception e)
{
ErrorMessage = $"Login flailed: {e.Message}";
}
finally
{
IsLoggingIn = false;
// Clear password for security
Password = string.Empty;
}
}
[RelayCommand]
private void ClearForm()
{
Username = string.Empty;
Password = string.Empty;
ErrorMessage = string.Empty;
}
}
Create ViewModels/DashboardViewModel.cs:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using YourProject.Services;
using System.Collections.ObjectModel;
namespace YourProject.ViewModels;
public partial class DashboardViewModel : ViewModelBase
{
private readonly UserStateService _userStateService;
[ObservableProperty] private int _activityCount;
public string CurrentUserName => _userStateService.CurrentUserName;
public bool IsUserLoggedIn => _userStateService.IsUserLoggedIn;
public ObservableCollection<string> RecentActivities => _userStateService.RecentActivities;
public DashboardViewModel(UserStateService userStateService)
{
_userStateService = userStateService;
ActivityCount = _userStateService.RecentActivities.Count;
// Subscribe to collection changes
_userStateService.RecentActivities.CollectionChanged += OnCollectionChanged;
// Subscribe to service property changes for other properties
_userStateService.PropertyChanged += OnServicePropertyChanged;
}
public DashboardViewModel(): this(null!) { }
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
ActivityCount = _userStateService.RecentActivities.Count;
}
private void OnServicePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(UserStateService.CurrentUserName):
OnPropertyChanged(nameof(CurrentUserName));
break;
case nameof(UserStateService.IsUserLoggedIn):
OnPropertyChanged(nameof(IsUserLoggedIn));
break;
}
}
[RelayCommand]
private void Logout()
{
_userStateService.LogoutUser();
}
[RelayCommand]
private void AddCustomActivity()
{
_userStateService.AddCustomActivity("Custom action performed by user");
}
[RelayCommand]
private void RefreshData()
{
_userStateService.AddCustomActivity("Data refreshed");
}
[RelayCommand]
private void ViewProfile()
{
_userStateService.AddCustomActivity("Profile viewed");
}
}
Your main window ViewModel acts as the conductor of your application’s symphony:
using CommunityToolkit.Mvvm.ComponentModel;
using YourProject.Services;
using Microsoft.Extensions.DependencyInjection;
namespace YourProject.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
private readonly UserStateService _userStateService;
[ObservableProperty]
private ViewModelBase _currentView;
public LoginViewModel LoginViewModel { get; }
public DashboardViewModel DashboardViewModel { get; }
public MainWindowViewModel()
{
_userStateService = App.ServiceProvider!.GetService<UserStateService>()!;
LoginViewModel = App.ServiceProvider!.GetService<LoginViewModel>()!;
DashboardViewModel = App.ServiceProvider!.GetService<DashboardViewModel>()!;
// Start with login view
CurrentView = LoginViewModel;
// Listen to login state changes
_userStateService.PropertyChanged += OnUserStateChanged;
}
private void OnUserStateChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(UserStateService.IsUserLoggedIn))
{
CurrentView = _userStateService.IsUserLoggedIn ? DashboardViewModel : LoginViewModel;
}
}
}
Now let’s create the actual UI components that users will interact with.
Views/LoginView.axaml)<UserControl x:Class="YourProject.Views.LoginView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.DataContext>
<vm:LoginViewModel />
</Design.DataContext>
<Grid>
<Border
Background="White"
BoxShadow="0 4 20 0 #20000000"
CornerRadius="15"
HorizontalAlignment="Center"
MaxWidth="400"
Padding="50"
VerticalAlignment="Center">
<StackPanel Spacing="25">
<!-- Header -->
<StackPanel HorizontalAlignment="Center" Spacing="10">
<Border
Background="#2196f3"
CornerRadius="40"
Height="80"
HorizontalAlignment="Center"
Width="80">
<PathIcon
Data="{StaticResource person_regular}"
Foreground="White"
Height="40"
Width="40" />
</Border>
<TextBlock
FontSize="28"
FontWeight="Bold"
Foreground="#333"
HorizontalAlignment="Center"
Text="Welcome Back!" />
<TextBlock
FontSize="14"
Foreground="#666"
HorizontalAlignment="Center"
Text="Please sign in to continue" />
</StackPanel>
<!-- Form Fields -->
<StackPanel Spacing="15">
<TextBox
Classes="modern"
Text="{Binding Username}"
Watermark="Enter username (try: admin)" />
<TextBox
Classes="modern"
PasswordChar="•"
Text="{Binding Password}"
Watermark="Enter password (use: demo123)" />
<!-- Error Message -->
<TextBlock
FontWeight="SemiBold"
Foreground="#f44336"
IsVisible="{Binding ErrorMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Text="{Binding ErrorMessage}"
TextWrapping="Wrap" />
</StackPanel>
<!-- Buttons -->
<StackPanel Spacing="12">
<Button
Classes="primary"
Command="{Binding LoginCommand}"
Content="{Binding IsLoggingIn, Converter={StaticResource BoolToTextConverter}}"
HorizontalAlignment="Stretch"
IsEnabled="{Binding !IsLoggingIn}" />
<Button
Classes="secondary"
Command="{Binding ClearFormCommand}"
Content="Clear Form"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center" />
</StackPanel>
<!-- Demo Instructions -->
<Border
Background="#e3f2fd"
BorderBrush="#2196f3"
BorderThickness="1"
CornerRadius="8"
Padding="15">
<StackPanel Spacing="5">
<TextBlock
FontSize="12"
FontWeight="SemiBold"
Foreground="#1976d2"
Text="Demo Instructions:" />
<TextBlock
FontSize="11"
Foreground="#1976d2"
Text="• Username: Any name you like" />
<TextBlock
FontSize="11"
Foreground="#1976d2"
Text="• Password: demo123" />
</StackPanel>
</Border>
</StackPanel>
</Border>
</Grid>
</UserControl>
Views/DashboardView.axaml)<UserControl x:Class="YourProject.Views.DashboardView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.DataContext>
<vm:DashboardViewModel />
</Design.DataContext>
<Grid Margin="30" RowDefinitions="Auto,*">
<!-- Header Section -->
<Border
BoxShadow="0 2 15 0 #30000000"
CornerRadius="10"
Grid.Row="0"
Margin="0,0,0,25"
Padding="25">
<Border.Background>
<LinearGradientBrush EndPoint="100%,0%" StartPoint="0%, 0%">
<GradientStop Color="#2196f3" Offset="0" />
<GradientStop Color="#21cbf3" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<Grid ColumnDefinitions="*,Auto,Auto,Auto">
<StackPanel Grid.Column="0" VerticalAlignment="Center">
<TextBlock
FontSize="24"
FontWeight="Bold"
Foreground="White"
Text="{Binding CurrentUserName, StringFormat='Welcome back, {0}!'}" />
<TextBlock
FontSize="14"
Foreground="#E3F2FD"
Text="{Binding ActivityCount}" />
</StackPanel>
<Button
Classes="secondary"
Command="{Binding ViewProfileCommand}"
Content="Profile"
Grid.Column="1"
Margin="10,0" />
<Button
Classes="secondary"
Command="{Binding RefreshDataCommand}"
Content="Refresh"
Grid.Column="2"
Margin="10,0" />
<Button
Classes="danger"
Command="{Binding LogoutCommand}"
Content="Logout"
Grid.Column="3" />
</Grid>
</Border>
<!-- Main Content -->
<Grid ColumnDefinitions="*,300" Grid.Row="1">
<!-- Activities Panel -->
<Border
Background="White"
BoxShadow="0 2 10 0 #20000000"
CornerRadius="10"
Grid.Column="0"
Margin="0,0,15,0"
Padding="25">
<Grid RowDefinitions="Auto,*,Auto">
<Grid
ColumnDefinitions="*,Auto"
Grid.Row="0"
Margin="0,0,0,20">
<TextBlock
FontSize="20"
FontWeight="Bold"
Foreground="#333"
Grid.Column="0"
Text="Activity Timeline" />
<TextBlock
FontSize="12"
Foreground="#666"
Grid.Column="1"
Text="{Binding ActivityCount}"
VerticalAlignment="Bottom" />
</Grid>
<ScrollViewer Grid.Row="1">
<ItemsControl ItemsSource="{Binding RecentActivities}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border
Background="#f8f9fa"
CornerRadius="8"
Margin="0,5"
Padding="15">
<Grid ColumnDefinitions="Auto,*">
<Ellipse
Fill="#2196f3"
Grid.Column="0"
Height="8"
Margin="0,6,15,0"
VerticalAlignment="Top"
Width="8" />
<TextBlock
FontSize="13"
Foreground="#555"
Grid.Column="1"
Text="{Binding}"
TextWrapping="Wrap" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Button
Classes="primary"
Command="{Binding AddCustomActivityCommand}"
Content="➕ Add Custom Activity"
Grid.Row="2"
HorizontalAlignment="Center"
Margin="0,20,0,0" />
</Grid>
</Border>
<!-- Stats Panel -->
<Border
Background="White"
BoxShadow="0 2 10 0 #20000000"
CornerRadius="10"
Grid.Column="1"
Padding="25">
<StackPanel Spacing="25">
<TextBlock
FontSize="18"
FontWeight="Bold"
Foreground="#333"
Text="Dashboard Stats" />
<!-- User Info Card -->
<Border
Background="#e8f5e8"
CornerRadius="8"
Padding="15">
<StackPanel Spacing="8">
<TextBlock
FontSize="14"
FontWeight="SemiBold"
Foreground="#2e7d32"
Text="User Status" />
<TextBlock
FontSize="16"
Foreground="#1b5e20"
Text="{Binding CurrentUserName}" />
<TextBlock
FontSize="12"
Foreground="#388e3c"
Text="Online" />
</StackPanel>
</Border>
<!-- Activity Stats -->
<Border
Background="#fff3e0"
CornerRadius="8"
Padding="15">
<StackPanel Spacing="8">
<TextBlock
FontSize="14"
FontWeight="SemiBold"
Foreground="#ef6c00"
Text="Activity Stats" />
<TextBlock
FontSize="14"
Foreground="#e65100"
Text="{Binding RecentActivities.Count, StringFormat='Total: {0} activities'}" />
<TextBlock
FontSize="12"
Foreground="#ff8f00"
Text="Session active" />
</StackPanel>
</Border>
<!-- Quick Actions -->
<Border
Background="#f3e5f5"
CornerRadius="8"
Padding="15">
<StackPanel Spacing="12">
<TextBlock
FontSize="14"
FontWeight="SemiBold"
Foreground="#7b1fa2"
Text="Quick Actions" />
<StackPanel Spacing="8">
<Button
Classes="secondary"
Command="{Binding AddCustomActivityCommand}"
CommandParameter="Viewed reports"
Content="View Reports"
HorizontalAlignment="Stretch" />
<Button
Classes="secondary"
Command="{Binding AddCustomActivityCommand}"
CommandParameter="Opened settings"
Content="Settings"
HorizontalAlignment="Stretch" />
<Button
Classes="secondary"
Command="{Binding AddCustomActivityCommand}"
CommandParameter="Created new task"
Content="New Task"
HorizontalAlignment="Stretch" />
</StackPanel>
</StackPanel>
</Border>
</StackPanel>
</Border>
</Grid>
</Grid>
</UserControl>
Views/MainWindow.axaml)<Window x:Class="YourProject.Views.MainWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="State Management Demo"
Width="700" Height="550"
WindowStartupLocation="CenterScreen">
<Design.DataContext>
<vm:MainWindowViewModel />
</Design.DataContext>
<ContentControl Content="{Binding CurrentView}">
<ContentControl.DataTemplates>
<DataTemplate DataType="{x:Type vm:LoginViewModel}">
<views:LoginView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:DashboardViewModel}">
<views:DashboardView />
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</Window>
namespace YourProjectName.Converters;
public class BoolToTextConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is bool isLoading)
{
return isLoading ? "Logging in..." : "Login";
}
return "Login";
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
</Border>
</Design.PreviewWith>
<!-- Add Styles Here -->
<Styles.Resources>
<StreamGeometry x:Key="person_regular">M35.7502,28 C38.0276853,28 39.8876578,29.7909151 39.9950978,32.0427546 L40,32.2487 L40,33 C40,36.7555 38.0583,39.5669 35.0798,41.3802 C32.1509,43.1633 28.2139,44 24,44 C19.7861,44 15.8491,43.1633 12.9202,41.3802 C10.0319285,39.6218485 8.11862909,36.9249713 8.00532378,33.3388068 L8,33 L8,32.2489 C8,29.9703471 9.79294995,28.1122272 12.0440313,28.0048972 L12.2499,28 L35.7502,28 Z M35.7502,30.5 L12.2499,30.5 C11.331345,30.5 10.5787597,31.2066575 10.5057976,32.1054618 L10.5,32.2489 L10.5,33 C10.5,35.7444 11.8602,37.8081 14.2202,39.2448 C16.6297,40.7117 20.0677,41.5 24,41.5 C27.9323,41.5 31.3703,40.7117 33.7798,39.2448 C36.0555143,37.8594107 37.4015676,35.8910074 37.4948116,33.2914406 L37.5,33 L37.5,32.2488 C37.5,31.331195 36.7934328,30.5787475 35.8937801,30.5057968 L35.7502,30.5 Z M24,4 C29.5228,4 34,8.47715 34,14 C34,19.5228 29.5228,24 24,24 C18.4772,24 14,19.5228 14,14 C14,8.47715 18.4772,4 24,4 Z M24,6.5 C19.8579,6.5 16.5,9.85786 16.5,14 C16.5,18.1421 19.8579,21.5 24,21.5 C28.1421,21.5 31.5,18.1421 31.5,14 C31.5,9.85786 28.1421,6.5 24,6.5 Z</StreamGeometry>
</Styles.Resources>
</Styles>
<Application
RequestedThemeVariant="Light"
x:Class="Project12StateManagement.App"
xmlns="https://github.com/avaloniaui"
xmlns:conv="using:Project12StateManagement.Converters"
xmlns:local="using:Project12StateManagement"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Resources>
<conv:BoolToTextConverter x:Key="BoolToTextConverter" />
</Application.Resources>
<Application.DataTemplates>
<local:ViewLocator />
</Application.DataTemplates>
<Application.Styles>
<FluentTheme />
<StyleInclude Source="Assets/Icons.axaml" />
<Style Selector="TextBox.modern">
<Setter Property="Padding" Value="12,8" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="#ddd" />
<Setter Property="Background" Value="White" />
</Style>
<Style Selector="TextBox.modern:pointerover /template/ Border#PART_BorderElement">
<Setter Property="BorderBrush" Value="#2196f3" />
<Setter Property="BorderThickness" Value="2" />
</Style>
<Style Selector="TextBox.modern:focus /template/ Border#PART_BorderElement">
<Setter Property="BorderBrush" Value="#2196f3" />
<Setter Property="BorderThickness" Value="2" />
</Style>
<Style Selector="Button.primary">
<Setter Property="Background" Value="#2196f3" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="20,10" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button.primary:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#1976d2" />
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Button.secondary">
<Setter Property="Background" Value="#f5f5f5" />
<Setter Property="Foreground" Value="#666" />
<Setter Property="Padding" Value="20,10" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="BorderBrush" Value="#ddd" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Button.danger">
<Setter Property="Background" Value="#f44336" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="15,8" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="Button.danger:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#d32f2f" />
</Style>
</Application.Styles>
</Application>
Don’t create one massive state service for everything. Instead, create specific services like UserStateService, NavigationService, SettingsService, etc. This makes your code more maintainable and easier to test.
Here’s what happens with normal event subscriptions:
public class DashboardViewModel : ViewModelBase
{
private readonly UserStateService _userStateService; // Long-lived service
public DashboardViewModel(UserStateService userStateService)
{
_userStateService = userStateService;
// POTENTIAL MEMORY LEAK
_userStateService.PropertyChanged += OnServicePropertyChanged;
}
private void OnServicePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
// Handle property changes...
}
}
The Problem:
UserStateService is a singleton (lives for the entire app lifetime)DashboardViewModel might be recreated multiple times (when navigating between views)+=, the service holds a strong reference to your ViewModelApp Startup:
Service ────> DashboardViewModel₁ (created)
User navigates to Login:
Service ────> DashboardViewModel₁ (should be destroyed, but can't!)
└──> DashboardViewModel₂ (created when returning to dashboard)
User navigates again:
Service ────> DashboardViewModel₁ (zombie - still in memory!)
├──> DashboardViewModel₂ (zombie - still in memory!)
└──> DashboardViewModel₃ (newly created)
we’re eliminating the lifetime mismatch by making the service scoped rather than singleton.
This way, when you navigate away from the dashboard, both the ViewModel AND the service instance can be garbage collected together.
Always wrap your state changes in try-catch blocks, especially when dealing with async operations:
[RelayCommand]
private async Task SaveDataAsync()
{
try
{
IsLoading = true;
await _dataService.SaveAsync(Data);
// Update state on success
}
catch (Exception ex)
{
// Handle error and update UI accordingly
ErrorMessage = "Failed to save data. Please try again.";
}
finally
{
IsLoading = false;
}
}
On EndeavourOS, testing is straightforward with the dotnet CLI. Create a simple test to verify your state service works correctly:
[Test]
public void LoginUser_UpdatesStateCorrectly()
{
// Arrange
var userStateService = new UserStateService();
var username = "testuser";
// Act
userStateService.LoginUser(username);
// Assert
Assert.IsTrue(userStateService.IsUserLoggedIn);
Assert.AreEqual(username, userStateService.CurrentUserName);
Assert.IsTrue(userStateService.RecentActivities.Count > 0);
}
Run your tests with:
dotnet test
Just because you can make everything a singleton doesn’t mean you should. Only use singletons for truly global state that needs to persist throughout the application lifecycle.
Avoid having views talk directly to each other. Always go through your ViewModels and state services. This keeps your architecture clean and testable.
When your ViewModels subscribe to events from services, remember to unsubscribe in the Dispose method to prevent memory leaks.
You’ve just built a solid foundation for state management in Avalonia UI! Your application can now:
The Community Toolkit MVVM takes care of all the boilerplate code, letting you focus on building great user experiences.
Happy coding!