AngelSix.ThemeEngine
1.2.0
dotnet add package AngelSix.ThemeEngine --version 1.2.0
NuGet\Install-Package AngelSix.ThemeEngine -Version 1.2.0
<PackageReference Include="AngelSix.ThemeEngine" Version="1.2.0" />
<PackageVersion Include="AngelSix.ThemeEngine" Version="1.2.0" />
<PackageReference Include="AngelSix.ThemeEngine" />
paket add AngelSix.ThemeEngine --version 1.2.0
#r "nuget: AngelSix.ThemeEngine, 1.2.0"
#:package AngelSix.ThemeEngine@1.2.0
#addin nuget:?package=AngelSix.ThemeEngine&version=1.2.0
#tool nuget:?package=AngelSix.ThemeEngine&version=1.2.0
AngelSix.ThemeEngine
Strongly-typed Avalonia theming with a Roslyn source generator. Decorate a class with [Theme], install this single NuGet package, and the bundled generator emits one {theme:PropertyName} markup extension per public property — XAML binds theme values directly, with IntelliSense, no runtime code generation.
Install
dotnet add package AngelSix.ThemeEngine
That single line gives you both the runtime library (Theme, ThemeContext, ThemeAttribute) and the source generator (AngelSix.ThemeEngine.SourceGen) — the analyzer is shipped inside this package under analyzers/dotnet/cs/, so NuGet wires it automatically.
Define a theme
Decorate any class with [Theme]. The base class is up to you — ObservableObject if you want per-property change notifications, plain class if you only ever swap themes wholesale.
using AngelSix.ThemeEngine;
using Avalonia;
using Avalonia.Media;
[Theme]
public class DefaultTheme
{
public string ThemeName => "Default";
public Color ThemeColor1 => Color.Parse("#FFFFFFFF");
public SolidColorBrush ThemeColor1Brush => new(ThemeColor1);
public Thickness ButtonPadding => new(8, 5, 8, 6);
public CornerRadius ControlCornerRadius => new(3);
public Color AccentColor => Color.Parse("#0F766E");
public SolidColorBrush AccentBrush => new(AccentColor);
}
The generator scans your compilation for every [Theme]-attributed class and emits one *Extension : MarkupExtension per public property.
Bootstrap a ThemeContext
Construct a ThemeContext (typically in your DI container) and pass it your theme instance. The constructor registers it as the resolver for all generated markup extensions.
using AngelSix.ThemeEngine;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddSingleton(new ThemeContext(new DefaultTheme()));
Not using a DI container — or need the theme to resolve in the Avalonia designer or a custom-control library? See Use outside dependency injection below.
Use outside dependency injection (design time & custom controls)
The generated {theme:...} extensions resolve through a single static hook,
ThemeContext.Resolver. Constructing a ThemeContext sets it — so in a normal app the
DI bootstrap above is all you need. But there are cases where no DI container ever runs,
and the extensions have nothing to resolve against:
- Design-time previews. The Avalonia previewer / designer instantiates your theme and
XAML directly; it never executes your app's startup, so
Resolveris never set and every{theme:...}binding fails (or the preview throws). - Custom-control libraries. A control or theme library that ships its own XAML can be
loaded ad-hoc — by the previewer, by a consumer that doesn't use DI, or before the host
app has bootstrapped its container. The library can't assume someone else has set up a
ThemeContext.
For these cases, set up a fallback ThemeContext once, before any {theme:...} extension
is evaluated. Guard on Resolver so you only create one when nothing else has — this keeps
a real DI-provided context untouched at runtime while still giving the designer (or an
undecorated host) a working theme to bind to:
if (ThemeContext.Resolver is null)
_ = new ThemeContext(new DefaultTheme());
The natural place is the constructor of whatever loads your theme's XAML — for example a
Styles-derived theme class, right before AvaloniaXamlLoader.Load:
public class MyTheme : Styles
{
public MyTheme(IServiceProvider? sp = null)
{
// Fallback for design time / non-DI hosts. If a DI bootstrap has already
// created a ThemeContext, Resolver is set and we leave it alone; otherwise
// we register the default theme so {theme:...} extensions still resolve.
if (ThemeContext.Resolver is null)
_ = new ThemeContext(new DefaultTheme());
AvaloniaXamlLoader.Load(sp, this);
}
}
You don't keep a reference to the ThemeContext — its constructor registers itself as
Resolver, which is the only thing the extensions need. At runtime your DI bootstrap wins:
the ThemeContext constructor it runs unconditionally reassigns Resolver to the
DI-provided context, so it overrides this fallback whether it runs before or after. At
design time, or in a host with no DI, the fallback is the only thing that ran, so it takes
over.
Use in XAML
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:theme="using:AngelSix.ThemeEngine.Generated">
<ControlTheme x:Key="{x:Type Button}" TargetType="Button">
<Setter Property="Background" Value="{theme:ThemeColor1Brush}" />
<Setter Property="Padding" Value="{theme:ButtonPadding}" />
<Setter Property="CornerRadius" Value="{theme:ControlCornerRadius}" />
</ControlTheme>
</ResourceDictionary>
Each extension returns a Binding whose source is ThemeContext.Active, so swapping ThemeContext.Active = new DarkTheme() re-themes every bound value instantly.
Compose paddings & margins — {theme:Edges}
Asymmetric paddings and margins don't need their own tokens. The Edges markup extension (shipped in this package, reached through the same theme namespace) composes a Thickness from whichever spacing tokens you place on each side, so per-control insets reuse your scale instead of hard-coded literals:
<Setter Property="Padding" Value="{theme:Edges Horizontal={theme:SpacingXl}, Vertical={theme:SpacingLg}}" />
<Setter Property="Padding" Value="{theme:Edges Left={theme:SpacingXxl}, Top={theme:SpacingLg}, Right={theme:SpacingLg}, Bottom={theme:SpacingMd}}" />
<Border Margin="{theme:Edges All={theme:SpacingMd}}" />
Each edge accepts any {theme:...} token — they're live bindings, so Edges weaves the four into a MultiBinding and the result stays reactive to theme / BaseSize changes. Precedence per edge: an explicit edge (Left etc.) beats Horizontal/Vertical, which beat All; anything unset is 0.
Verification
The generator emits a marker type AngelSix.ThemeEngine.Generated.__ThemeEngineGenerated. The ThemeContext constructor reflects for it and throws an actionable error if the analyzer didn't run, so wiring problems surface immediately rather than as obscure XAML errors.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Avalonia (>= 12.0.1)
- CommunityToolkit.Mvvm (>= 8.4.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.9)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.