csharp-avalonia
C# + Avalonia UI Cross-Platform Development
Guidelines for building cross-platform applications with .NET and Avalonia UI targeting Windows, macOS, Linux, iOS, Android, and WebAssembly.
Project Setup
Prerequisites
- .NET SDK 8.0+ (
dotnet --version) - Avalonia templates:
dotnet new install Avalonia.Templates
Scaffolding
# Create solution with MVVM pattern (recommended)
dotnet new avalonia.mvvm -o MyApp
cd MyApp
# Or with full cross-platform targets
dotnet new avalonia.xplat -o MyApp
# Solution structure for large apps
dotnet new sln -o MyApp
cd MyApp
dotnet new avalonia.mvvm -o MyApp.Desktop
dotnet new classlib -o MyApp.Core # Shared business logic
dotnet new classlib -o MyApp.Services # Platform services
dotnet sln add MyApp.Desktop MyApp.Core MyApp.Services
Recommended Solution Structure
MyApp.sln
src/
MyApp.Core/ # Shared logic (no UI dependency)
Models/
Services/
Interfaces/
ViewModels/ # ViewModels live here (shared)
MyApp.UI/ # Avalonia UI project
Assets/ # Fonts, images, icons
Controls/ # Custom reusable controls
Converters/ # Value converters
Styles/ # Global styles and themes
Views/ # AXAML views
App.axaml # Application root
App.axaml.cs
ViewLocator.cs
MyApp.Desktop/ # Desktop entry point
Program.cs
MyApp.Android/ # Android entry point (optional)
MyApp.iOS/ # iOS entry point (optional)
MyApp.Browser/ # WebAssembly entry point (optional)
tests/
MyApp.Core.Tests/
MyApp.UI.Tests/
MVVM Pattern (CommunityToolkit.Mvvm)
Always use MVVM with CommunityToolkit.Mvvm (source generators):
dotnet add package CommunityToolkit.Mvvm
ViewModel Pattern
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class MainViewModel : ViewModelBase
{
[ObservableProperty]
private string _name = string.Empty;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SubmitCommand))]
private bool _isValid;
[RelayCommand(CanExecute = nameof(IsValid))]
private async Task SubmitAsync()
{
// Business logic here
}
}
ViewModelBase
using CommunityToolkit.Mvvm.ComponentModel;
public abstract class ViewModelBase : ObservableObject { }
View Binding (AXAML)
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MyApp.ViewModels"
x:DataType="vm:MainViewModel">
<Design.DataContext>
<vm:MainViewModel />
</Design.DataContext>
<StackPanel Margin="20" Spacing="10">
<TextBox Text="{Binding Name}" Watermark="Enter name..." />
<Button Content="Submit" Command="{Binding SubmitCommand}" />
</StackPanel>
</Window>
AXAML Best Practices
Always use compiled bindings
<!-- Always set x:DataType for compile-time checked bindings -->
<UserControl x:DataType="vm:MyViewModel">
<TextBlock Text="{Binding Title}" />
</UserControl>
Styles and Themes
<!-- App.axaml — Global styles -->
<Application.Styles>
<FluentTheme />
<StyleInclude Source="/Styles/Global.axaml" />
</Application.Styles>
<!-- Styles/Global.axaml -->
<Styles xmlns="https://github.com/avaloniaui">
<Style Selector="Button.primary">
<Setter Property="Background" Value="{DynamicResource AccentBrush}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="CornerRadius" Value="4" />
</Style>
</Styles>
Responsive Layouts
<!-- Use Grid and panels, avoid fixed sizes -->
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="*,2*">
<TextBlock Grid.Row="0" Text="Header" />
<ScrollViewer Grid.Row="1" Grid.ColumnSpan="2">
<ItemsControl ItemsSource="{Binding Items}" />
</ScrollViewer>
</Grid>
<!-- Adaptive layout with classes -->
<Panel Classes.narrow="{Binding IsNarrow}">
<Panel.Styles>
<Style Selector="Panel.narrow > StackPanel">
<Setter Property="Orientation" Value="Vertical" />
</Style>
</Panel.Styles>
<StackPanel Orientation="Horizontal">
<!-- content -->
</StackPanel>
</Panel>
Navigation
Use a router/navigator pattern:
public partial class MainViewModel : ViewModelBase
{
[ObservableProperty]
private ViewModelBase _currentPage;
public MainViewModel()
{
_currentPage = new HomeViewModel();
}
[RelayCommand]
private void NavigateTo(string page) => CurrentPage = page switch
{
"home" => new HomeViewModel(),
"settings" => new SettingsViewModel(),
_ => CurrentPage,
};
}
<DockPanel>
<StackPanel DockPanel.Dock="Left" Width="200">
<Button Content="Home" Command="{Binding NavigateToCommand}" CommandParameter="home" />
<Button Content="Settings" Command="{Binding NavigateToCommand}" CommandParameter="settings" />
</StackPanel>
<ContentControl Content="{Binding CurrentPage}" />
</DockPanel>
Dependency Injection
Use Microsoft.Extensions.DependencyInjection:
// App.axaml.cs
public partial class App : Application
{
public static IServiceProvider Services { get; private set; } = null!;
public override void OnFrameworkInitializationCompleted()
{
var services = new ServiceCollection();
services.AddSingleton<IDataService, DataService>();
services.AddSingleton<INavigationService, NavigationService>();
services.AddTransient<MainViewModel>();
Services = services.BuildServiceProvider();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = Services.GetRequiredService<MainViewModel>()
};
}
base.OnFrameworkInitializationCompleted();
}
}
Platform-Specific Code
Use interfaces + platform implementations:
// In MyApp.Core
public interface IPlatformService
{
string GetPlatformName();
Task<string?> PickFileAsync();
}
// In MyApp.Desktop
public class DesktopPlatformService : IPlatformService
{
public string GetPlatformName() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? "Windows" : RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
? "macOS" : "Linux";
public async Task<string?> PickFileAsync()
{
var topLevel = TopLevel.GetTopLevel(/* ... */);
var files = await topLevel!.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Open file",
AllowMultiple = false,
});
return files.FirstOrDefault()?.Path.LocalPath;
}
}
Useful NuGet Packages
| Package | Purpose |
|---|---|
CommunityToolkit.Mvvm |
MVVM source generators (ObservableProperty, RelayCommand) |
Avalonia.Xaml.Behaviors |
Behaviors and interactions |
Avalonia.Controls.DataGrid |
DataGrid control |
FluentAvalonia |
Fluent Design system controls |
Material.Avalonia |
Material Design theme |
Semi.Avalonia |
Semi Design theme |
Avalonia.Svg.Skia |
SVG rendering |
AsyncImageLoader.Avalonia |
Async image loading with cache |
HotAvalonia |
XAML hot reload during development |
Dock.Avalonia |
Docking layout (like VS Code panels) |
Testing
dotnet add package Moq
dotnet add package FluentAssertions
dotnet add package xunit
dotnet add package Avalonia.Headless.XUnit # UI tests without display
ViewModel Tests
public class MainViewModelTests
{
[Fact]
public void Name_WhenSet_RaisesPropertyChanged()
{
var vm = new MainViewModel();
var raised = false;
vm.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(MainViewModel.Name)) raised = true;
};
vm.Name = "test";
raised.Should().BeTrue();
}
}
Headless UI Tests
[AvaloniaFact]
public void Button_ShouldBeDisabled_WhenFormInvalid()
{
var window = new MainWindow { DataContext = new MainViewModel() };
window.Show();
var button = window.FindControl<Button>("SubmitButton");
button!.IsEnabled.Should().BeFalse();
}
Build & Publish
# Debug run
dotnet run --project src/MyApp.Desktop
# Publish self-contained for each platform
dotnet publish src/MyApp.Desktop -c Release -r win-x64 --self-contained
dotnet publish src/MyApp.Desktop -c Release -r osx-arm64 --self-contained
dotnet publish src/MyApp.Desktop -c Release -r linux-x64 --self-contained
# Single-file executable
dotnet publish src/MyApp.Desktop -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -p:PublishTrimmed=true
# AOT compilation (faster startup, smaller size)
dotnet publish src/MyApp.Desktop -c Release -r win-x64 -p:PublishAot=true
Common Patterns
Dialog Service
public interface IDialogService
{
Task<string?> ShowInputDialogAsync(string title, string message);
Task ShowMessageAsync(string title, string message);
Task<bool> ShowConfirmAsync(string title, string message);
}
Settings / Preferences
// Use a JSON file in AppData
public class SettingsService
{
private static readonly string Path = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"MyApp", "settings.json");
public AppSettings Load() => File.Exists(Path)
? JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(Path))!
: new AppSettings();
public void Save(AppSettings settings)
{
Directory.CreateDirectory(System.IO.Path.GetDirectoryName(Path)!);
File.WriteAllText(Path, JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true }));
}
}
Async Data Loading
public partial class DataViewModel : ViewModelBase
{
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private ObservableCollection<Item> _items = [];
[RelayCommand]
private async Task LoadDataAsync()
{
IsLoading = true;
try
{
var data = await _dataService.GetItemsAsync();
Items = new ObservableCollection<Item>(data);
}
finally
{
IsLoading = false;
}
}
}
More from phuetz/code-buddy
blender
Blender 3D modeling, animation, and rendering automation via Python bpy scripting and CLI
19figma
Automate Figma design workflows via REST API, Plugin API, and MCP integration
3github
Interact with GitHub using the gh CLI for issues, PRs, CI runs, releases, and API queries
3gif-search
Search and download GIFs from Tenor and Giphy APIs
3ableton-live
Ableton Live music production automation via OSC protocol, MIDI, and Max for Live
3gitlab
GitLab DevOps platform with CI/CD pipelines, API automation, and glab CLI control
3