skills/phuetz/code-buddy/csharp-avalonia

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
GitHub Stars
6
First Seen
11 days ago
Installed on
amp1
cline1
trae1
trae-cn1
opencode1
cursor1