csharp-avalonia
SKILL.md
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;
}
}
}
Weekly Installs
1
Repository
phuetz/code-buddyGitHub Stars
6
First Seen
11 days ago
Security Audits
Installed on
amp1
cline1
trae1
trae-cn1
opencode1
cursor1