30. Markup, XAML compiler, and extensibility

Goal

Why this matters

Prerequisites

1. The XAML asset pipeline

When you add .axaml files, the SDK-driven build uses two MSBuild tasks from Avalonia.Build.Tasks:

  1. GenerateAvaloniaResources (external/Avalonia/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs)
    • Runs before compilation. Packs every AvaloniaResource item into the *.axaml resource bundle (avares://).
    • Parses each XAML file with XamlFileInfo.Parse, records x:Class entries, and writes /!AvaloniaResourceXamlInfo metadata so runtime lookups can map CLR types to resource URIs.
    • Emits MSBuild diagnostics (BuildEngine.LogError) if it sees invalid XML or duplicate x:Class declarations.
  2. CompileAvaloniaXaml (external/Avalonia/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs)
    • Executes after C# compilation. Loads the produced assembly and references via Mono.Cecil.
    • Invokes XamlCompilerTaskExecutor.Compile, which runs the XamlIl compiler over each XAML resource, generates partial classes, compiled bindings, and lookup stubs under the CompiledAvaloniaXaml namespace, then rewrites the IL in-place.
    • Writes the updated assembly (and optional reference assembly) to $(IntermediateOutputPath).

Key metadata:

2. Inside the XamlIl compiler

XamlIl is Avalonia's LLVM-style pipeline built on XamlX:

  1. Parsing (XamlX.Parsers) transforms XAML into an AST (XamlDocument).
  2. Transform passes (Avalonia.Markup.Xaml.XamlIl.CompilerExtensions) rewrite the tree, resolve namespaces (XmlnsDefinitionAttribute), expand markup extensions, and inline templates.
  3. IL emission (XamlCompilerTaskExecutor) creates classes such as CompiledAvaloniaXaml.!XamlLoader, CompiledAvaloniaXaml.!AvaloniaResources, and compiled binding factories.
  4. Runtime helpers (external/Avalonia/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs) provide services for deferred templates, parent stacks, and resource resolution at runtime.

Every .axaml file with x:Class="Namespace.View" yields:

If you set <SkipXamlCompilation>true</SkipXamlCompilation>, the compiler bypasses IL generation; AvaloniaXamlLoader then falls back to runtime parsing for each load (slower and reflection-heavy, but useful during prototyping).

3. Runtime loading and hot reload

AvaloniaXamlLoader (external/Avalonia/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs) chooses between:

Runtime configuration knobs:

Use cases for runtime loading:

4. Namespaces, schemas, and lookup

Avalonia uses XmlnsDefinitionAttribute (external/Avalonia/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs) to map XML namespaces to CLR namespaces. Assemblies such as Avalonia.Markup.Xaml declare:

[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup.Xaml.MarkupExtensions")]

Guidelines:

IXamlTypeResolver is available through the service provider (Extensions.ResolveType). When you write custom markup extensions, you can resolve types that respect XmlnsDefinition mappings.

5. Markup extensions and service providers

All markup extensions inherit from Avalonia.Markup.Xaml.MarkupExtension (MarkupExtension.cs) and implement ProvideValue(IServiceProvider serviceProvider).

Avalonia supplies extensions such as StaticResourceExtension, DynamicResourceExtension, CompiledBindingExtension, and OnPlatformExtension (external/Avalonia/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/*). The service provider gives access to:

Custom markup extension example:

public class UppercaseExtension : MarkupExtension
{
    public string? Text { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var source = Text ?? serviceProvider.GetDefaultAnchor() as TextBlock;
        return source switch
        {
            string s => s.ToUpperInvariant(),
            TextBlock block => block.Text?.ToUpperInvariant() ?? string.Empty,
            _ => string.Empty
        };
    }
}

Usage in XAML:

<TextBlock Text="{local:Uppercase Text=hello}"/>

Tips:

6. Custom templates, resources, and compiled bindings

XamlIl optimises templates and bindings when you:

The compiler hoists resource dictionaries and template bodies into factory methods, reducing runtime allocations. When you inspect generated IL (use ilspy), you'll see new Func<IServiceProvider, object>(...) wrappers for control templates referencing XamlIlRuntimeHelpers.DeferredTransformationFactoryV2.

7. Debugging and diagnostics

8. Authoring workflow checklist

  1. Project file – confirm <UseCompiledBindingsByDefault> and <VerifyXamlIl> match your requirements.
  2. Namespaces – add [assembly: XmlnsDefinition] for every exported namespace; document the suggested prefix.
  3. Resources – place .axaml under the project root or set Link metadata so GenerateAvaloniaResources records the intended resource URI.
  4. InitializeComponent – always call it in partial classes; otherwise the compiled loader is never invoked.
  5. Testing – run unit tests with AvaloniaHeadless (Chapter 21) to exercise runtime loader paths without the full compositor.

9. Practice lab: extending the markup toolchain

  1. Inspect build output – build your project with dotnet build /bl. Open the MSBuild log and confirm GenerateAvaloniaResources and CompileAvaloniaXaml run with the expected inputs.
  2. Add XML namespace mappings – create a component library, decorate it with [assembly: XmlnsDefinition("https://schemas.myapp.com/ui", "MyApp.Controls")], and consume it from a separate app.
  3. Create a markup extension – implement {local:Uppercase} as above, inject IServiceProvider utilities, and write tests that call ProvideValue with a fake service provider.
  4. Toggle compiled bindings – set <AvaloniaUseCompiledBindingsByDefault>false>, then selectively enable compiled bindings in XAML with {x:CompileBindings} and observe the generated IL (via dotnet-monitor or ILSpy).
  5. Runtime loader experiment – register a custom IRuntimeXamlLoader in a test harness to load XAML from strings, flip UseCompiledBindingsByDefault, and log diagnostics through RuntimeXamlLoaderConfiguration.DiagnosticHandler.

10. Troubleshooting & best practices

Look under the hood (source bookmarks)

Check yourself

What's next