-
Notifications
You must be signed in to change notification settings - Fork 0
XAML
Cursorial lets you author your UI declaratively in XAML — the same shapes WPF and Avalonia use
({Binding}, ControlTemplate, ResourceDictionary, x:Name/x:Class). You get it two ways: a runtime
loader that parses and instantiates markup at runtime, and a Roslyn source generator that validates your
XAML at build time, emits typed code-behind, and produces an AOT-clean (reflection-free) load path. Both run the
same parser, so what compiles in the generator loads identically at runtime.
Reach for XAML when you'd rather express a control tree as markup than as constructor soup, when you want MVVM-style data binding, or when you want compiler-checked bindings and trim/AOT-friendly publishing.
Runtime loader (Cursorial.UI.Xaml) |
Source generator (Cursorial.UI.Xaml.Generator) |
|
|---|---|---|
| When errors surface | At Load(...) time, as XamlParseException
|
At build time, as CUR#### compiler diagnostics |
| Code-behind | Hand-wired (FindName, manual field assignment) |
Generated typed x:Name fields + InitializeComponent()
|
| Binding checks | Path resolved reflectively at runtime |
x:DataType paths checked at build; compiled bindings |
| Reflection | Reflective metadata provider by default | Emits an AOT-clean provider — no runtime reflection |
| Setup | Reference the package, call the loader | Add the generator, give the .xaml an x:Class partial |
Both share the netstandard2.0 frontend (Cursorial.UI.Xaml.Frontend) — the parser, the structure-of-arrays
document model, the markup-extension grammar, and the diagnostics. The generator runs that frontend over Roslyn
symbols; the loader runs it over text.
XamlLoader has a process-wide default instance, XamlLoader.Shared. The simplest path parses and instantiates
in one call:
using Cursorial.UI.Xaml;
const string Xaml = """
<StackPanel xmlns="https://cursorial.dev/ui" Spacing="1" Margin="2,1">
<TextBlock Text="{Binding Title}" />
<Button Content="_Save"
Command="{Binding SaveCommand}"
Background="{StaticResource AccentBrush}" />
</StackPanel>
""";
var root = XamlLoader.Shared.Load<StackPanel>(Xaml);
root.DataContext = viewModel;Load<T>(string) casts the root to T and throws a descriptive InvalidOperationException if it isn't that
type. The non-generic Load(string) returns object. Both have Stream and TextReader overloads.
Parsing and instantiation are split. Parse(...) returns an immutable, thread-safe XamlDocument you can parse
off the UI thread and reuse; Load(XamlDocument) instantiates a fresh object graph on the UI thread:
XamlDocument doc = XamlLoader.Shared.Parse(Xaml); // pure, thread-safe, host-free
var a = XamlLoader.Shared.Load<StackPanel>(doc); // independent instances
var b = XamlLoader.Shared.Load<StackPanel>(doc);GetOrParse(uri, xml) memoizes the document per loader, keyed by URI — handy for templates loaded repeatedly.
LoadComponent(component, document) (or the (component, sourceXaml, source) overload) sets members on an object
you already created instead of producing a new root — the mechanism behind generated code-behind. The
parameterless LoadComponent(object) (the WPF embedded-resource convention) is not wired up; use the explicit
overloads.
The default UI namespace is https://cursorial.dev/ui. Declared on the root, it maps unprefixed elements to
the Cursorial UI assembly's public namespaces (Cursorial.UI, Cursorial.UI.Controls, Cursorial.UI.Data, and
the drawing media types). Reach your own types with a using: (or clr-namespace:) prefix:
<UserControl xmlns="https://cursorial.dev/ui"
xmlns:x="https://cursorial.dev/xaml"
xmlns:vm="using:MyApp.ViewModels">
...
</UserControl>The x: directive namespace is https://cursorial.dev/xaml. Namespace declarations are root-only (Avalonia
parity) — a non-root xmlns is a CUR2004 error.
The loader ships the four extensions you reach for most, plus a base class (MarkupExtension) for your own:
-
{Binding}— paths, indexers, attached-property segments, converters, modes,RelativeSource,ElementName, plusTemplatedParent. Honorsx:DataTypefor typed paths. See Data binding. -
{StaticResource Key}— resolved eagerly against the lexical resource scope at load time. -
{DynamicResource Key}— a live producer; re-resolves when the resource (or theme) changes. -
{TemplateBinding Property}— the fast path from aControlTemplatepart to its templated parent.
<Border Background="{DynamicResource SurfaceBrush}"
BorderPen="{StaticResource Hairline}">
<TextBlock Text="{Binding User.Name}" Foreground="{TemplateBinding Foreground}" />
</Border>-
ControlTemplate— captured as a deferred subtree and expanded per target, each with its own name scope;{TemplateBinding}and template-scoped names resolve against the templated parent. See Controls. -
ResourceDictionary— keys can be strings or types (for implicitDataTemplates); supportsMergedDictionaries(own entries beat later-merged) andThemeDictionaries(light/dark variants). See Styling & themes. - Themes ship as code or as embedded XAML; the built-in control themes are authored in a box-drawing idiom and are overridable per app.
A Style carries a selector grammar (type / .class / #name / :pseudo-class / descendant & child / :is()).
The namespace-qualified type token uses a prefix|Type form (CSS / Avalonia style), resolved against the document's
root xmlns:
<Styles xmlns="https://cursorial.dev/ui" xmlns:c="using:MyApp.Controls">
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="{DynamicResource AccentBrush}" />
</Style>
<Style Selector=":is(c|CardBase).highlighted">
<Setter Property="BorderPen" Value="{StaticResource AccentPen}" />
</Style>
</Styles>A Style's selector is built at activation (not folded at parse), so prefix|Type tokens bind the document
namespace. A TargetType builds an exact-type selector; an explicit Selector wins when both are present.
A leading underscore in content marks an access key — Content="_Save" registers Alt+S.
Folding happens on genuinely access-text-typed members; an object-typed Content="_Save" stays the raw string and
the control folds it on demand, so the underscore always means the same thing whether you read it as text or as
content.
x:Name registers an element in the document's name scope; after loading, find it with UIElement.FindName. On a
parse or resolution error the loader throws XamlParseException, which carries the failing Line and
Column (1-based) and the full Diagnostics list. Diagnostic codes are banded:
-
CUR1xxx— parse / grammar errors (malformed XML, unterminated markup extension, bad intrinsic). -
CUR2xxx— resolution errors (type not found — with a "did you mean?" suggestion — unknown member, undeclared prefix,xmlnsnot on root). -
CUR3xxx— instantiation errors (activation failure, duplicate name).
Add Cursorial.UI.Xaml.Generator to your project and the same XAML is validated and compiled at build time. Give
the markup an x:Class directive and the generator emits a matching partial class.
The generator resolves types and members purely from Roslyn symbols — it never loads Cursorial.UI into the
compiler — and runs the frontend parser over those symbols. Every parse and resolution diagnostic surfaces as a
CUR#### build error at the .xaml file's line and column, so an unknown type or a typo'd property fails the
build instead of the first Load(...):
MyView.xaml(7,18): error CUR2002: The type 'Buttom' was not found. Did you mean 'Button'?
For each x:Class document the generator emits a partial class with a typed field per document-scope
x:Name and an InitializeComponent() that loads the markup and assigns the fields. (Names inside resource
dictionaries or template content live in their own scopes and don't become fields.)
<!-- MyView.xaml -->
<StackPanel xmlns="https://cursorial.dev/ui"
xmlns:x="https://cursorial.dev/xaml"
x:Class="MyApp.Views.MyView"
Spacing="1">
<TextBlock x:Name="Heading" Text="{Binding Title}" />
<Button x:Name="SaveButton" Content="_Save" Command="{Binding SaveCommand}" />
</StackPanel>// MyView.xaml.cs — your half of the partial
namespace MyApp.Views;
public partial class MyView : StackPanel
{
public MyView()
{
InitializeComponent(); // generated: loads the XAML, populates the fields
Heading.Foreground = Colors.Teal; // SaveButton / Heading are typed fields
SaveButton.Click += (_, _) => Heading.Text = "Saved!";
}
}The generated InitializeComponent() loads a once-parsed, cached XamlDocument through a loader bound to the
assembly's own generated metadata provider, then assigns each typed field from the document name scope.
Declare an x:DataType and the generator validates each DataContext-relative {Binding} path against that type's
members at build time — a bad path is a build error, not a silent runtime no-op. The scope is lexical, so a
binding inside a DataTemplate is checked against that template's x:DataType, not an outer one:
<DataTemplate x:DataType="vm:FileItem">
<TextBlock Text="{Binding Name}" /> <!-- checked against FileItem.Name at build -->
</DataTemplate>Compiled bindings are typed and avoid per-update boxing — a single-hop binding's steady-state push allocates
nothing. (You can also opt into compiled bindings from C# via Binding.Compiled(...); see
Data binding.)
The generator emits one IXamlTypeMetadataProvider per compilation, installed via a module initializer as the
loader's default. With a generated provider plus standard markup, the load path uses no runtime reflection —
the reflective default provider can be dropped for NativeAOT / trimmed publishing. A dual-run gate asserts the
generated provider renders byte-identically to the reflective one, so moving to the generator changes nothing
about how your UI looks.
The build-integration MSBuild props/targets and a verified AOT-publish demo are still in progress; the generator core — symbol-backed parse,
CUR####diagnostics, typed code-behind, the emitted provider, andx:DataTypepath checks — is in place and exercised in-repo.
- UI Overview — the framework the markup builds on
-
UI Data Binding —
{Binding}, converters, compiled bindings - UI Styling and Themes — selectors, resources, theme variants
-
UI Controls —
ControlTemplateand the control catalog
Cursorial.Core
Cursorial.Rendering
Drawing & Animation
Cursorial.UI
- Overview
- Layout & panels
- Controls
- Styling & themes
- Data binding
- Input & focus
- Windowing
- Animation & transitions
Declarative