Goal
Why this matters
AppBuilder, lifetimes, Application.RegisterServices) lets you shape architecture to match modular backends or plug-ins.Prerequisites
AppBuilder pipeline.Program.cs: configuring the builder yourselfAvalonia templates scaffold XAML, but the real work happens in Program.BuildAvaloniaApp() (see external/Avalonia/src/Avalonia.Templates/). Code-first apps use the same AppBuilder<TApp> API.
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.ReactiveUI; // optional: add once for ReactiveUI-centric apps
internal static class Program
{
[STAThread]
public static void Main(string[] args)
{
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
}
private static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace()
.With(new Win32PlatformOptions
{
CompositionMode = new[] { Win32CompositionMode.WinUIComposition } // example tweak
})
.With(new X11PlatformOptions { EnableIme = true })
.With(new AvaloniaNativePlatformOptions { UseDeferredRendering = true })
.UseSkia();
}
Key points from AppBuilder.cs:
Configure<App>() wires Avalonia's service locator (AvaloniaLocator) with the type parameter you pass.UsePlatformDetect() resolves the proper backend at runtime. Replace it with UseWin32(), UseAvaloniaNative(), etc., to force a backend for tests..UseReactiveUI() (from Avalonia.ReactiveUI/AppBuilderExtensions.cs) registers ReactiveUI's scheduler, command binding, and view locator glue—call it in code-first projects that rely on ReactiveCommand..With<TOptions>() registers backend-specific option objects. Because you're not using App.axaml, code is the only place to set them.Remember you can split configuration across methods for clarity:
private static AppBuilder ConfigurePlatforms(AppBuilder builder)
=> builder.UsePlatformDetect()
.With(new Win32PlatformOptions { UseWgl = false })
.With(new AvaloniaNativePlatformOptions { UseGpu = true });
Chaining explicit helper methods keeps BuildAvaloniaApp readable while preserving fluent semantics.
Application subclass without XAMLApplication lives in external/Avalonia/src/Avalonia.Controls/Application.cs. The default XAML template overrides OnFrameworkInitializationCompleted() after loading XAML. In code-first scenarios you:
Initialize() to register styles/resources explicitly.RegisterServices() to set up dependency injection.OnFrameworkInitializationCompleted() to set the root visual for the selected lifetime.using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Themes.Fluent;
public sealed class App : Application
{
public override void Initialize()
{
Styles.Clear();
Styles.Add(new FluentTheme
{
Mode = FluentThemeMode.Dark
});
Styles.Add(new StyleInclude(new Uri("avares://App/Styles"))
{
Source = new Uri("avares://App/Styles/Controls.axaml") // optional: you can still load XAML fragments
});
Styles.Add(CreateButtonStyle());
Resources.MergedDictionaries.Add(CreateAppResources());
}
protected override void RegisterServices()
{
// called before Initialize(). Great spot for DI container wiring.
AvaloniaLocator.CurrentMutable.Bind<IMyService>().ToSingleton<MyService>();
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel()
};
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
{
singleView.MainView = new HomeView
{
DataContext = new HomeViewModel()
};
}
base.OnFrameworkInitializationCompleted();
}
private static Style CreateButtonStyle()
=> new(x => x.OfType<Button>())
{
Setters =
{
new Setter(Button.CornerRadiusProperty, new CornerRadius(6)),
new Setter(Button.PaddingProperty, new Thickness(16, 8)),
new Setter(Button.ClassesProperty, Classes.Parse("accent"))
}
};
private static ResourceDictionary CreateAppResources()
{
return new ResourceDictionary
{
["AccentBrush"] = new SolidColorBrush(Color.Parse("#FF4F8EF7")),
["AccentForegroundBrush"] = Brushes.White,
["BorderRadiusSmall"] = new CornerRadius(4)
};
}
}
Notes from source:
Styles is an IList<IStyle> exposed by Application. Clearing it ensures you start from a blank slate (no default theme). Add FluentTheme or your own style tree.StyleInclude can still ingest axaml fragments—code-first doesn't forbid XAML, it just avoids Application.LoadComponent.RegisterServices() is invoked early in AppBuilderBase<TApp>.Setup() before the app is instantiated. It's designed for code-first registration patterns.base.OnFrameworkInitializationCompleted() to ensure any registered OnFrameworkInitializationCompleted handlers fire.When you skip XAML, every control tree is instantiated manually. You can:
Window, UserControl, or ContentControl and compose UI in the constructor.Binding objects or extension helpers.public sealed class MainWindow : Window
{
public MainWindow()
{
Title = "Code-first Avalonia";
Width = 800;
Height = 600;
Content = BuildLayout();
}
private static Control BuildLayout()
{
return new DockPanel
{
LastChildFill = true,
Children =
{
CreateHeader(),
CreateBody()
}
};
}
private static Control CreateHeader()
=> new Border
{
Background = (IBrush)Application.Current!.Resources["AccentBrush"],
Padding = new Thickness(24, 16),
Child = new TextBlock
{
Text = "Dashboard",
FontSize = 22,
Foreground = Brushes.White,
FontWeight = FontWeight.SemiBold
}
}.DockTop();
private static Control CreateBody()
=> new StackPanel
{
Margin = new Thickness(24),
Spacing = 16,
Children =
{
new TextBlock { Text = "Welcome!", FontSize = 18 },
new Button
{
Content = "Refresh",
Command = ReactiveCommand.Create(() => Debug.WriteLine("Refresh requested"))
}
}
};
}
Helper extension methods keep layout code tidy. You can author them in a static class:
public static class DockPanelExtensions
{
public static T DockTop<T>(this T control) where T : Control
{
DockPanel.SetDock(control, Dock.Top);
return control;
}
}
Because you're constructing controls in code, you can register them with the NameScope for later lookup:
var scope = new NameScope();
NameScope.SetNameScope(this, scope);
var statusText = new TextBlock { Text = "Idle" };
scope.Register("StatusText", statusText);
This matches NameScope behaviour from XAML (see external/Avalonia/src/Avalonia.Base/LogicalTree/NameScope.cs).
Code-first projects rely on the same binding engine, but you create bindings manually or use compiled binding helpers.
var textBox = new TextBox();
textBox.Bind(TextBox.TextProperty, new Binding("Query")
{
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
ValidatesOnDataErrors = true
});
var searchButton = new Button
{
Content = "Search"
};
searchButton.Bind(Button.CommandProperty, new Binding("SearchCommand"));
Binding lives in external/Avalonia/src/Avalonia.Base/Data/Binding.cs. Anything you can express via {Binding} markup is available as properties on this class. For compiled bindings, use CompiledBindingFactory from Avalonia.Data.Core directly:
var factory = new CompiledBindingFactory();
var compiled = factory.Create<object, string>(
vmGetter: static vm => ((SearchViewModel)vm).Query,
vmSetter: static (vm, value) => ((SearchViewModel)vm).Query = value,
name: nameof(SearchViewModel.Query),
mode: BindingMode.TwoWay);
textBox.Bind(TextBox.TextProperty, compiled);
Use AvaloniaLocator.CurrentMutable (defined in Application.RegisterServices) to register services. For richer DI, integrate libraries like Microsoft.Extensions.DependencyInjection.
protected override void RegisterServices()
{
var services = new ServiceCollection();
services.AddSingleton<IMyService, MyService>();
services.AddSingleton<HomeViewModel>();
var provider = services.BuildServiceProvider();
AvaloniaLocator.CurrentMutable.Bind<IMyService>().ToSingleton(() => provider.GetRequiredService<IMyService>());
AvaloniaLocator.CurrentMutable.Bind<HomeViewModel>().ToTransient(() => provider.GetRequiredService<HomeViewModel>());
}
Later, resolve services via AvaloniaLocator.Current.GetService<HomeViewModel>() or inject them into controls. Because RegisterServices runs before Initialize, you can use registered services while building resources.
Code-first theming revolves around ResourceDictionary, Styles, and StyleInclude.
private static ResourceDictionary CreateAppResources()
{
return new ResourceDictionary
{
MergedDictionaries =
{
new ResourceDictionary
{
["Spacing.Small"] = 4.0,
["Spacing.Medium"] = 12.0,
["Spacing.Large"] = 24.0
}
},
["AccentBrush"] = Brushes.CornflowerBlue,
["AccentForegroundBrush"] = Brushes.White
};
}
Use namespaced keys (Spacing.Medium) to avoid collisions. If you rely on resizable themes, store them in a dedicated class:
public static class AppTheme
{
public static Styles Light { get; } = new Styles
{
new FluentTheme { Mode = FluentThemeMode.Light },
CreateSharedStyles()
};
public static Styles Dark { get; } = new Styles
{
new FluentTheme { Mode = FluentThemeMode.Dark },
CreateSharedStyles()
};
private static Styles CreateSharedStyles()
=> new Styles
{
new Style(x => x.OfType<Window>())
{
Setters =
{
new Setter(Window.BackgroundProperty, Brushes.Transparent)
}
}
};
}
Switch themes at runtime:
public void UseDarkTheme()
{
Application.Current!.Styles.Clear();
foreach (var style in AppTheme.Dark)
{
Application.Current.Styles.Add(style);
}
}
Iterate the collection when swapping themes—Styles implements IEnumerable<IStyle> so a simple foreach keeps dependencies minimal. Remember to freeze brushes (Brushes.Transparent is already frozen) when reusing them to avoid unnecessary allocations.
A common pattern is to place each feature in its own namespace with:
Control (for pure code) or a partial class if you mix .axaml for templates.ViewModel class registered via DI.IStyle/ResourceDictionary definitions encapsulated in static classes.Example folder layout:
src/
Infrastructure/
Services/
Styles/
Features/
Dashboard/
DashboardView.cs
DashboardViewModel.cs
DashboardStyles.cs
Settings/
SettingsView.cs
SettingsViewModel.cs
DashboardStyles might expose a Styles property you merge into Application.Styles. Keep style/helper definitions close to the controls they customize to maintain cohesion.
To convert an existing XAML-based app:
Grid.SetColumn(button, 1)).{Binding} with control.Bind(Property, new Binding("Path")). For ElementName references, call NameScope.Register and FindControl.new Style(x => x.OfType<Button>().Class("accent")) for selectors. Set Setters to match <Setter> elements.<ControlTemplate>, build FuncControlTemplate. The constructor signature matches the control type and returns the template content.<ResourceDictionary.MergedDictionaries> with ResourceDictionary.MergedDictionaries.Add(...).DynamicResource → DynamicResourceBindingExtensions, StaticResource → dictionary lookup). For OnPlatform or OnFormFactor, implement custom helper methods that return values based on RuntimeInformation.Testing after each step keeps parity. Avalonia DevTools still works with code-first UI, so inspect logical/visual trees to confirm bindings and styles resolved correctly.
App.axaml and MainWindow.axaml. Recreate them as classes mirroring their original layout using C# object initializers. Verify styles, resources, and data bindings behave identically using DevTools.Styles groups in code. Add a toggle button that swaps Application.Current.Styles and persists the choice using your service layer.RegisterServices() using your preferred container. Resolve view-models in OnFrameworkInitializationCompleted rather than new, ensuring the container owns lifetimes.Func<Control>). Inject factories through DI and demonstrate a plugin module adding new pages without touching XAML.By mastering these patterns you gain confidence that Avalonia's internals don’t require XAML. The framework's property system, theming engine, and lifetimes remain fully accessible from C#, letting teams tailor architecture to their tooling and review preferences.
What's next