Skip to main content

Overview

This guide covers best practices for developing Avalonia applications, including coding standards, architectural patterns, performance optimization, and contribution guidelines based on the Avalonia project itself.

Coding Standards

Avalonia follows the .NET Core coding style guidelines.

General Guidelines

1

Line length

Keep lines of code around 120 characters or less. This is not a hard limit, but try to keep code readable.
// Good: concise and readable
public void ProcessData(string input, int maxLength, bool validate)
{
    // ...
}

// Avoid: overly long lines
public void ProcessDataWithMultipleParametersAndAVeryLongMethodNameThatExceedsReadableLength(string input, int maxLength, bool validate, string additionalData)
2

Documentation

All public methods must have XML documentation:
/// <summary>
/// Initializes a new instance of the control.
/// </summary>
/// <param name="name">The name of the control.</param>
/// <returns>The initialized control instance.</returns>
public Control InitializeControl(string name)
{
    // Implementation
}
3

Naming conventions

Follow consistent naming:
// Classes and methods: PascalCase
public class MyCustomControl : Control
{
    // Private fields: _camelCase
    private string _internalState;
    
    // Public properties: PascalCase
    public string DisplayName { get; set; }
    
    // Methods: PascalCase
    public void UpdateState()
    {
        // Local variables: camelCase
        var currentValue = GetValue();
    }
}
4

Avoid regions

DO NOT USE #REGIONS - they hide code organization issues:
// Bad: using regions
#region Properties
public string Name { get; set; }
#endregion

// Good: well-organized code without regions
public string Name { get; set; }
public int Age { get; set; }

Code Style

public class UserViewModel : ViewModelBase
{
    private string _userName;
    
    public string UserName
    {
        get => _userName;
        set => SetProperty(ref _userName, value);
    }
    
    public void ValidateUser()
    {
        if (string.IsNullOrEmpty(UserName))
        {
            throw new ArgumentException("User name cannot be empty");
        }
    }
}

Architectural Patterns

MVVM (Model-View-ViewModel)

Avalonia is designed for MVVM. Follow this pattern for maintainable applications:
// Model: Plain data class
public class User
{
    public string Name { get; set; }
    public string Email { get; set; }
}

// ViewModel: Implements INotifyPropertyChanged
public class UserViewModel : INotifyPropertyChanged
{
    private User _user;
    private string _statusMessage;
    
    public string Name
    {
        get => _user.Name;
        set
        {
            _user.Name = value;
            OnPropertyChanged();
        }
    }
    
    public string StatusMessage
    {
        get => _statusMessage;
        set
        {
            _statusMessage = value;
            OnPropertyChanged();
        }
    }
    
    public ICommand SaveCommand { get; }
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
<!-- View: AXAML with data binding -->
<Window xmlns="https://github.com/avaloniaui"
        xmlns:vm="using:MyApp.ViewModels"
        x:DataType="vm:UserViewModel">
    <StackPanel>
        <TextBox Text="{Binding Name}" />
        <TextBlock Text="{Binding StatusMessage}" />
        <Button Content="Save" Command="{Binding SaveCommand}" />
    </StackPanel>
</Window>

Dependency Injection

Use dependency injection for better testability:
public class MainViewModel
{
    private readonly IUserService _userService;
    private readonly INavigationService _navigationService;
    
    public MainViewModel(IUserService userService, INavigationService navigationService)
    {
        _userService = userService;
        _navigationService = navigationService;
    }
}

// Register in App.xaml.cs
public override void OnFrameworkInitializationCompleted()
{
    if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
    {
        var services = new ServiceCollection();
        services.AddSingleton<IUserService, UserService>();
        services.AddSingleton<INavigationService, NavigationService>();
        
        var serviceProvider = services.BuildServiceProvider();
        
        desktop.MainWindow = new MainWindow
        {
            DataContext = serviceProvider.GetRequiredService<MainViewModel>()
        };
    }
}

Property System Best Practices

StyledProperty vs DirectProperty

Use StyledProperty for properties that support styling and binding:
public class MyControl : Control
{
    public static readonly StyledProperty<string> TitleProperty =
        AvaloniaProperty.Register<MyControl, string>(nameof(Title));
    
    public string Title
    {
        get => GetValue(TitleProperty);
        set => SetValue(TitleProperty, value);
    }
}
Use DirectProperty for performance-critical properties:
public class MyControl : Control
{
    private int _count;
    
    public static readonly DirectProperty<MyControl, int> CountProperty =
        AvaloniaProperty.RegisterDirect<MyControl, int>(
            nameof(Count),
            o => o.Count,
            (o, v) => o.Count = v);
    
    public int Count
    {
        get => _count;
        set => SetAndRaise(CountProperty, ref _count, value);
    }
}

Attached Properties

Create attached properties for cross-cutting concerns:
public class ToolTipHelper
{
    public static readonly AttachedProperty<string> ToolTipProperty =
        AvaloniaProperty.RegisterAttached<ToolTipHelper, Control, string>("ToolTip");
    
    public static string GetToolTip(Control control)
    {
        return control.GetValue(ToolTipProperty);
    }
    
    public static void SetToolTip(Control control, string value)
    {
        control.SetValue(ToolTipProperty, value);
    }
}

Performance Optimization

Virtualization

Use virtualized controls for large data sets:
<ListBox VirtualizationMode="Simple"
         ItemsSource="{Binding LargeCollection}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Name}" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Compiled Bindings

Use compiled bindings for better performance:
<Window xmlns:vm="using:MyApp.ViewModels"
        x:DataType="vm:MainViewModel">
    <!-- Compiled binding: faster, type-safe -->
    <TextBox Text="{Binding UserName}" />
</Window>

Async Operations

Keep UI responsive with async operations:
public class DataViewModel : ViewModelBase
{
    private bool _isLoading;
    
    public bool IsLoading
    {
        get => _isLoading;
        set => SetProperty(ref _isLoading, value);
    }
    
    public async Task LoadDataAsync()
    {
        IsLoading = true;
        try
        {
            // Perform async operation
            await Task.Delay(1000);
            // Update UI on dispatcher if needed
            await Dispatcher.UIThread.InvokeAsync(() =>
            {
                // Update properties
            });
        }
        finally
        {
            IsLoading = false;
        }
    }
}

Resource Management

Properly dispose of resources:
public class MyControl : Control, IDisposable
{
    private IDisposable _subscription;
    
    protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
    {
        base.OnAttachedToVisualTree(e);
        _subscription = Observable.Timer(TimeSpan.FromSeconds(1))
            .Subscribe(OnTimer);
    }
    
    protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
    {
        base.OnDetachedFromVisualTree(e);
        _subscription?.Dispose();
    }
    
    public void Dispose()
    {
        _subscription?.Dispose();
    }
}

Testing Best Practices

Test Naming Convention

Use descriptive, sentence-style test names:
// Good: Describes what is being tested
public void Calling_Foo_Should_Increment_Bar()
{
    // Arrange
    var sut = new MyClass();
    
    // Act
    sut.Foo();
    
    // Assert
    Assert.Equal(1, sut.Bar);
}

// Render tests should describe the output
public void Rectangle_2px_Stroke_Filled()
{
    // Test implementation
}

Test Categories

Avalonia uses different test types:
For non-platform features. Located in tests/ directory:
[Fact]
public void Property_Change_Should_Raise_PropertyChanged()
{
    var viewModel = new TestViewModel();
    var raised = false;
    
    viewModel.PropertyChanged += (s, e) =>
    {
        if (e.PropertyName == nameof(TestViewModel.Name))
            raised = true;
    };
    
    viewModel.Name = "New Name";
    
    Assert.True(raised);
}
For platform-specific features. Located in tests/Avalonia.IntegrationTests.Appium/:
[Test]
public void Window_Should_Resize_Correctly()
{
    using var window = new Window { Width = 800, Height = 600 };
    window.Show();
    
    window.Width = 1024;
    window.Height = 768;
    
    Assert.Equal(1024, window.Width);
    Assert.Equal(768, window.Height);
}
For visual output. Located in tests/Avalonia.RenderTests/:
[Fact]
public void Button_With_Red_Background_Renders_Correctly()
{
    var button = new Button
    {
        Content = "Test",
        Background = Brushes.Red
    };
    
    RenderToFile(button);
    CompareImages();
}

Contributing Guidelines

When contributing to Avalonia or building your own projects:

Pull Request Best Practices

1

Write clear descriptions

Provide comprehensive PR descriptions:
  • What changes were made
  • Why they were made
  • How to test the changes
  • Link related issues with Fixes #1234
2

Commit hygiene

Maintain clean commit history:
# Good commit message
git commit -m "Add data validation to UserViewModel

Implemented INotifyDataErrorInfo to provide real-time
validation feedback for user input fields.

Fixes #123"

# Bad commit message
git commit -m "fixed stuff"
3

Avoid unrelated changes

  • Do not change code unrelated to your fix/feature
  • Do not introduce spurious formatting changes
  • Do not fix style issues in unrelated code (create separate PRs)
4

Test your changes

Before submitting:
# Run unit tests
dotnet test

# Build release configuration
dotnet build -c Release

# Run integration tests if applicable
cd tests/Avalonia.IntegrationTests.Appium
dotnet test

Breaking Changes

During a major release cycle, do not introduce source or binary breaking changes. This is checked by automated tools.
For future breaking changes:
// TODO12: Remove deprecated method in version 12
[Obsolete("Use NewMethod instead. This will be removed in v12.")]
public void OldMethod()
{
    NewMethod();
}

public void NewMethod()
{
    // New implementation
}

Project Organization

Solution Structure

Organize multi-platform projects:
MyApp/
├── src/
│   ├── MyApp/                    # Shared UI and logic
│   ├── MyApp.Desktop/            # Desktop entry point
│   ├── MyApp.Android/            # Android entry point
│   ├── MyApp.iOS/                # iOS entry point
│   └── MyApp.Browser/            # WebAssembly entry point
├── tests/
│   ├── MyApp.Tests/              # Unit tests
│   └── MyApp.IntegrationTests/   # Integration tests
└── MyApp.sln

Use Solution Filters

Create .slnf files for focused development:
{
  "solution": {
    "path": "MyApp.sln",
    "projects": [
      "src/MyApp/MyApp.csproj",
      "src/MyApp.Desktop/MyApp.Desktop.csproj",
      "tests/MyApp.Tests/MyApp.Tests.csproj"
    ]
  }
}

Security Best Practices

Input Validation

Always validate user input:
public class UserInputValidator
{
    public static bool ValidateEmail(string email)
    {
        if (string.IsNullOrWhiteSpace(email))
            return false;
        
        try
        {
            var addr = new System.Net.Mail.MailAddress(email);
            return addr.Address == email;
        }
        catch
        {
            return false;
        }
    }
}

Secure Data Storage

Use platform-specific secure storage:
public interface ISecureStorage
{
    Task SetAsync(string key, string value);
    Task<string> GetAsync(string key);
}

// Platform-specific implementation
public class WindowsSecureStorage : ISecureStorage
{
    // Use Windows Credential Manager
}

public class MacOSSecureStorage : ISecureStorage
{
    // Use macOS Keychain
}

Accessibility

Ensure your applications are accessible:
<Button AutomationProperties.Name="Save Document"
        AutomationProperties.HelpText="Saves the current document to disk">
    <PathIcon Data="{StaticResource SaveIcon}" />
</Button>
// Implement AutomationPeer for custom controls
protected override AutomationPeer OnCreateAutomationPeer()
{
    return new MyControlAutomationPeer(this);
}

Documentation

XML Documentation

Document all public APIs:
/// <summary>
/// Represents a custom control for displaying user information.
/// </summary>
/// <remarks>
/// This control automatically formats and displays user data
/// with support for avatars and status indicators.
/// </remarks>
public class UserInfoControl : Control
{
    /// <summary>
    /// Gets or sets the user name to display.
    /// </summary>
    /// <value>
    /// The user name, or <c>null</c> if not set.
    /// </value>
    public string UserName { get; set; }
}

Code Comments

Use comments for complex logic:
// Calculate the layout bounds considering the parent's constraints
// and the child's desired size. We need to handle three cases:
// 1. Child fits within parent constraints
// 2. Child exceeds constraints but can be scrolled
// 3. Child exceeds constraints and must be clipped
var bounds = CalculateLayoutBounds(parentSize, childSize);

Additional Resources

Contributing Guide

Full contributing guidelines for Avalonia

Code of Conduct

Community code of conduct

Samples Repository

Official samples demonstrating best practices

Architecture Guide

Understanding Avalonia’s architecture

Summary

Following these best practices will help you:
  • Write maintainable, readable code
  • Build performant applications
  • Contribute effectively to Avalonia
  • Create high-quality user experiences
  • Maintain security and accessibility standards
Remember: consistency and clarity are more valuable than cleverness.

Build docs developers (and LLMs) love