Avalonia.Markup.Declarative

repository·master·Indexed 19 days ago

https://github.com/avaloniaui/avalonia.markup.declarative

A C#-first authoring layer for Avalonia UI that uses source generators and compiled bindings to enable type-safe, declarative UI markup in C#. It supports MVVM via ViewBase, self-contained declarative components (SFC), and hot reload. Additionally, the Declarative.Avalonia.AgentTools package provides an MCP server for AI agents to inspect the visual tree, perform layout audits, and interact with the application during debug builds.

Tokens
14.1K
Snippets
41
Records
59
Agent score
67%

What's inside Avalonia.Markup.Declarative

  1. Understand the coordinate frame for Agent Tools

    master

    All tools use a single coordinate frame: absolute client-DIP coordinates of the top-level.

    • The client-area origin is (0,0).
    • Screenshots render at 96 DPI, so one unit equals one screenshot pixel.
    • get_visual_tree reports each node's abs=[x y w h] and center=(x,y) in this frame.
    • hit_test reports coordinates in this frame.
    • click_at, tap, drag, and pointer_* consume these coordinates.

    Note on Popups: Content in a PopupRoot is reported relative to its own top-level. To target it, you must use that window's windowId.

  2. Use the Declarative Component pattern (SFC)

    master

    For features where the view and its reactive state are tightly coupled, use a self-contained declarative component. This pattern involves inheriting from ViewBase<TState> and defining a nested State class (typically using CommunityToolkit.Mvvm.ComponentModel.ObservableObject).

    To compose views that require constructor injection, use ViewFactory.Create<T>(). If you are using Dependency Injection, register UseComponentControlFactory(...) on your AppBuilder.

    using Avalonia.Data;
    using CommunityToolkit.Mvvm.ComponentModel;
    
    public class CounterComponent() : ViewBase<CounterComponent.State>(new State())
    {
        public sealed partial class State : ObservableObject
        {
            [ObservableProperty]
            [NotifyPropertyChangedFor(nameof(CounterLabel))]
            public partial decimal? Counter { get; set; } = 0;
    
            [ObservableProperty]
            public partial string StatusText { get; set; } = "Hello world";
    
            public string CounterLabel => $"Counter: {Counter}";
        }
    
        protected override object Build(State state) =>
            new StackPanel()
                .Children(
                    new TextBlock()
                        .Text(state, x => x.StatusText),
                    new TextBlock()
                        .Text(state, x => x.CounterLabel),
                    new NumericUpDown()
                        .Value(state, x => x.Counter, BindingMode.TwoWay),
                    new Button()
                        .Content("Increment")
                        .OnClick(_ => state.Counter++)
                );
    }
  3. Use compiled-binding setters with automatic conversion

    master

    Compiled-binding setters in Avalonia.Markup.Declarative support automatic conversion for common primitive and nullable mismatches.

    • Recommended approach: Use plain member access like x => x.Property. This avoids manual casts for types like int -> double or bool -> bool?.
    • Unsupported: Value-converting casts such as x => (double)x.Counter are rejected by the expression parser.
    • Supported: Type casts that reach a member of a derived type, such as x => ((DerivedType)x).Property, are supported.
    • Note on TwoWay bindings: For lossy numeric TwoWay bindings, the convert-back operation truncates toward zero.
    // Prefer this (automatic conversion works for int -> double)
    Button().Bind(Button.CommandProperty, x => x.MyCommand)
    
    // This is supported (casting to derived type)
    Button().Bind(Button.CommandProperty, x => ((DerivedViewModel)x).MyCommand)
    
    // This is REJECTED (direct value cast)
    // Button().Bind(Button.CommandProperty, x => (double)x.Counter)
  4. Resolve selectors for inspection tools

    master

    When using inspection tools (like get_layout or get_properties), selectors are resolved using the following priority order:

    1. Control Name: The name assigned via the .Name(...) extension.
    2. UI Automation Name: The visible label (e.g., the text on a Button or the header of a TabItem).
    3. Type Name: The class name (e.g., Button).

    Note: Name and label always take precedence over type. If a selector matches multiple controls, get_layout returns the first one and lists the others, while highlight frames all matches. Selectors also work for content inside open popups (dropdowns, menus, etc.).

  5. Configure compiled bindings and automatic type conversion

    master

    The library's generated setters support compiled bindings and automatic conversion for common primitive and nullable mismatches (e.g., int to double for Slider.Value or bool for CheckBox.IsChecked).

    Important Rules:

    • Use plain member access like x => x.Counter.
    • Do not use numeric-conversion casts like x => (double)x.Counter; the auto-converter handles this, and Avalonia's expression parser will reject the Convert node.
    • Type casts that navigate to a member of a derived type (e.g., x => ((DerivedType)x).Property) are supported.
    • For lossy TwoWay numeric conversions, the conversion back to the source type truncates toward zero.
  6. Understand error capture mechanisms in Agent Tools

    master

    The inspector captures several types of errors to provide actionable feedback to agents:

    • Build errors: ViewBuildingException (including rich binding/setter error messages) are stored in a thread-safe ring buffer.
    • Binding & Converter errors: Captured via a custom ILogSink decorator on Avalonia's Logger.Sink, specifically filtering for LogArea.Binding and LogArea.Property.
    • Runtime errors: Unhandled exceptions on the UI dispatcher and unobserved task exceptions are captured by RuntimeErrorSink. This is a pure observer; it records the exception (including the throw site) but does not mark it as handled, so the application's original error behavior remains unchanged.
    • Structured ViewModel errors: When set_view_model or invoke_command fails due to a bad path, the error message includes the failing segment, the runtime type of the object, available members, and a "did you mean" suggestion.
  7. Implement custom MCP tools for your application

    master

    To expose application-specific logic (like sprite editing or state inspection) to an agent, implement custom tool classes and register them using WithTools<T>() during inspector setup.

    Implementation Steps:

    1. Define a Tool Class: Mark the class with [McpServerToolType]. Use [McpServerTool] on methods to expose them.
    2. Provide Documentation: Use the [Description] attribute on methods. This is the only documentation the agent receives to decide how to call the tool.
    3. Dependency Injection: Tool instances are built once from your application's Services. You can inject your app's state or services into the constructor.
    4. Gating Interaction: To prevent accidental state changes, mark state-modifying tool classes with [AgentInteractionTools]. These will only be registered if EnableInteraction is set to true in the inspector configuration.

    Example Implementation

    [McpServerToolType]
    public sealed class SpriteTools
    {
        private readonly AppState _state;
    
        public SpriteTools(AppState state) => _state = state;
    
        [McpServerTool(Name = "get_sprite_info", ReadOnly = true), Description(
            "Returns the open sprite's size, frame count and the selected layer.")]
        public string GetSpriteInfo() =>
            $"{_state.Sprite.Size}, {_state.Sprite.Frames.Count} frame(s), layer '{_state.SelectedLayer.Name}'";
    }
    
    // Registration
    .UseAgentInspector(o =>
    {
        o.EnableInteraction = true;
        o.Services = serviceProvider;
        o.WithTools<SpriteTools>();
    })
    [McpServerToolType]
    public sealed class SpriteTools
    {
        private readonly AppState _state;
    
        public SpriteTools(AppState state) => _state = state;
    
        [McpServerTool(Name = "get_sprite_info", ReadOnly = true), Description(
            "Returns the open sprite's size, frame count and the selected layer.")]
        public string GetSpriteInfo() =>
            $"{_state.Sprite.Size}, {_state.Sprite.Frames.Count} frame(s), layer '{_state.SelectedLayer.Name}'";
    }
    
    .UseAgentInspector(o =>
    {
        o.EnableInteraction = true;
        o.Services = serviceProvider;
        o.WithTools<SpriteTools>();
    })
  8. Configure Claude Code to use the Agent Inspector

    master

    You can connect Claude Code to your running Avalonia app using the HTTP transport. Run the following command in your terminal, or add a .mcp.json file to your project root.

    claude mcp add --transport http avalonia-agent-inspector http://127.0.0.1:5599
    // .mcp.json (project root)
    {
      "mcpServers": {
        "avalonia-agent-inspector": {
          "type": "http",
          "url": "http://127.0.0.1:5599"
        }
      }
    }
  9. Implement the MVVM pattern with ViewBase

    master

    For classic MVVM architectures, inherit from ViewBase<TViewModel>. The generated setters provide compiled-binding overloads that allow you to bind directly to properties of the ViewModel using lambda expressions.

    using Avalonia.Data;
    
    public class MainView() : ViewBase<MainViewModel>(new MainViewModel())
    {
        protected override object Build(MainViewModel vm) =>
            new StackPanel()
                .Children(
                    new TextBox()
                        .Text(vm, x => x.Message, BindingMode.TwoWay),
                    new TextBlock()
                        .Text(vm, x => x.Message),
                    new Button()
                        .Content("Reset")
                        .OnClick(_ => vm.Message = string.Empty)
                );
    }
  10. Generate Markup Extensions for External Assemblies

    master

    The standalone AvaloniaExtensionGenerator CLI tool is deprecated. Extensions for third-party or external assemblies are now handled by the integrated source generator.

    To enable markup extensions for an external assembly, add the GenerateMarkupExtensionsForAssembly attribute to your assembly.

    using Avalonia.Markup.Declarative;
    
    [assembly: GenerateMarkupExtensionsForAssembly(typeof(SomeTypeFromExternalAssembly))]
  11. Connect Codex to the Agent Inspector

    master

    Add an [mcp_servers.avalonia-agent-inspector] table to your ~/.codex/config.toml. When using a url for a streamable-HTTP server, you must enable the experimental RMCP client in the [features] section.

    # ~/.codex/config.toml
    [mcp_servers.avalonia-agent-inspector]
    url = "http://127.0.0.1:5599"
    
    [features]
    experimental_use_rmcp_client = true
  12. Use Compiled Bindings instead of Reflection or Callbacks

    master

    The library has moved away from reflection-based Bind(...) and custom onChanged callbacks. You should now use compiled bindings which provide better type safety and performance.

    Patterns:

    • Two-way binding: Instead of .Value(() => property, val => property = val), use .Value(state, x => x.Property, BindingMode.TwoWay).
    • Standard binding: Instead of .Text(Bind(Message)), use .Text(state, x => x.Message).
    • Styles: Instead of string-based or Binding object styles, use compiled style setters: .IsEnabled(default(TViewModel)!, x => x.IsPropertyEnabled).
    // Two-way input
    new NumericUpDown()
        .Value(state, x => x.Counter, BindingMode.TwoWay);
    
    // Compiled binding
    new TextBox()
        .Text(state, x => x.Message, BindingMode.TwoWay);
    
    // Compiled style binding
    new Style<TabItem>()
        .IsEnabled(default(TabVm)!, x => x.Enabled)
        .Foreground(Brushes.YellowGreen);