Akbura can evaluate markup extension classes in regular attributes and in
AKCSS utility attributes. A markup extension is a class with a public instance
ProvideValue() or ProvideValue(IServiceProvider) method.
The Extension suffix is optional:
using Demo.Markup;
<Border p-${GalleryPadding 4} />
Both GalleryPadding and GalleryPaddingExtension resolve this class:
namespace Demo.Markup;
public sealed class GalleryPaddingExtension
{
public GalleryPaddingExtension(double value)
{
Value = value;
}
public double Value { get; }
public double ProvideValue(IServiceProvider services)
{
return Value;
}
}
The extension namespace must be imported explicitly. Akbura.Markup is not
added implicitly either:
using Akbura.Markup;
<Border p-1 ${md}:p-3 />
Binding objects in holder properties​
Normal markup properties apply binding results to their target. A CLR property
marked with Avalonia's [AssignBinding] instead stores the binding object:
using Avalonia.Controls;
using Avalonia.Styling;
using Akbura.Markup;
<Setter Property="Button.Background" Value=${Binding AccentBrush} />
Akbura still calls ProvideValue and supplies the normal extension services.
The delivery policy applies both to a statically known binding result and to an
extension whose ProvideValue returns object containing a binding. It does
not bind Setter.Value itself or subscribe merely to assign the current brush.
Avalonia later applies the stored binding to the styled target.
The same metadata works for custom holders with different property names.
[DependsOn] controls assignment ordering independently; it does not create
reactive subscriptions. See Avalonia Styles for custom
[Content]/[AssignBinding] assignments and contextual value types.
Utility arguments​
A markup extension can supply an argument to a parameterized utility:
@using Avalonia;
@using Avalonia.Controls;
@utilities {
Decorator.p-(double value) {
Padding: new Thickness(value);
}
}
using Demo.Markup;
state double spacing = 12;
<Border p-${GalleryPadding {spacing}} />
For a utility parameter of type T, ProvideValue may return:
| Result | Behavior |
|---|---|
T |
Applies the value immediately. |
IObservable<T> |
Reapplies the utility whenever the observable publishes a value. |
IObservable<object> |
Converts each published value to T at runtime. |
BindingBase |
Binds a hidden attached property on the actual utility target and reapplies the utility when the binding changes. |
An observable that has not published a value does not participate in the utility cascade. This lets an earlier utility with the same conflict key act as a fallback. Completion keeps the last value. An observable error is reported immediately.
The IObservable<object> form is useful when an extension cannot expose a
generic result type. Every published value still has to be compatible with the
utility parameter type.
Dynamic arguments​
If an extension contains a component expression, Akbura recreates it during each component update:
<Border p-${GalleryPadding {spacing + 1}} />
Expressions inside the extension are bound as normal C# expressions. They are
included in GetCSharpSymbolReferences and preserve source locations through
generated #line directives.
Literal or otherwise component-independent extensions are created when their runtime source is attached. They are not recreated by every component update.
Utility variants​
A markup extension before a colon controls whether a utility candidate is active:
using Akbura.Markup;
<Border p-1
${sm}:p-2
${md}:p-3 />
A prefix is not a special kind of markup extension. Existing markup extensions can be used directly when their resolved value supplies a boolean condition:
<ToggleSwitch x.Name="MyToggle" />
<Border ${DynamicResource MyKey}:p-5
${StaticResource MyBoolValue}:p-7
${Binding #MyToggle.IsChecked}:p-10 />
This means resource lookup and Avalonia bindings can control utilities without
a custom extension class. A direct extension may return bool or
IObservable<bool>. Extensions such as DynamicResource and Binding can
also provide the condition through an Avalonia binding. The observable and
binding forms reevaluate only the affected element and conflicting property.
UtilityVariantAttribute is optional. It does not make an extension usable as
a prefix. It only supplies Order, ConflictGroup, and
UnprefixedPrecedence for resolving active candidates that write the same
property. Without the attribute, prefixed candidates use normal source order.
The older form:
<Border md:p-3 />
is parsed only for error recovery and produces a diagnostic. Use:
<Border ${md}:p-3 />
Custom ordering and unprefixed fallback behavior can be configured with
UtilityVariantAttribute.
See Utility Variants for the complete property-level
conflict-resolution algorithm, ConflictGroup, Order, built-in breakpoint
implementation, and UnprefixedUtilityPrecedence modes.
Utility binding priority​
A prefix extension can select the Avalonia binding layer used after AKCSS has selected a winning property operation:
using Akbura.Markup;
using Avalonia.Data;
[UtilityBindingPriority(
Priority = BindingPriority.Animation)]
public sealed class importantExtension
{
public BindingBase ProvideValue(IServiceProvider services)
{
return new Binding("IsChecked")
{
ElementName = "ImportantToggle"
};
}
}
<ToggleSwitch x.Name="ImportantToggle" />
<Border Margin="10"
${important}:m-12 />
The binding controls whether m-12 participates. While it is active, the
winning Margin operation is installed at BindingPriority.Animation.
When it becomes inactive, Akbura disposes only that contribution and Avalonia
reveals the previous Margin="10" value.
Use exactly one priority source:
[UtilityBindingPriority(
Priority = BindingPriority.Template)]
public sealed class fixedPriorityExtension
{
}
[UtilityBindingPriority(
PriorityMember = nameof(Priority))]
public sealed class priorityExtension
{
public BindingPriority Priority { get; set; }
}
PriorityMember may name an accessible instance field or readable instance
property whose type is exactly BindingPriority. Akbura creates one extension
instance for each prefix invocation, applies its constructor and named
properties, calls ProvideValue, and reads the priority from that same
instance. A refresh first disposes the old property contributions and then
creates the next instance.
The supported priorities are:
BindingPriority.AnimationBindingPriority.StyleTriggerBindingPriority.TemplateBindingPriority.Style
LocalValue, Inherited, Unset, and unknown enum values are rejected.
Priority-aware utilities may write only StyledProperty and
AttachedProperty values. Dynamic resources are bound at the requested
priority; ordinary values are installed as disposable Avalonia contributions.
This attribute does not participate in conflict resolution. It can be combined
with UtilityVariantAttribute, but Order, ConflictGroup, and
UnprefixedPrecedence still select the AKCSS winner independently.
Lifecycle​
Akbura owns every subscription and binding created for utility markup extensions.
- Sources are attached to the actual target control.
- A new observable or binding value reevaluates only its utility conflict.
- A component update performs the complete AKCSS cascade and recreates extensions that contain C# expressions.
- Detaching the target disposes subscriptions and bindings.
- Attaching it again evaluates the extensions and subscribes again.
BindingBase may temporarily produce no value while DataContext is being
inherited. During that period the utility candidate is excluded and its
fallback can apply.
AKCSS resource helpers​
AKCSS also provides two compiler-supported resource methods:
Amx.StaticResource<T>(object? key);
Amx.DynamicResource<T>(object? key);
They provide typed access to resources from AKCSS expressions.
| Method | Behavior |
|---|---|
Amx.StaticResource<T>(key) |
Resolves the resource as T without creating a dynamic resource binding. |
Amx.DynamicResource<T>(key) |
Observes the resource and updates the target property when its value changes. |
These methods are compiler interception points. They throw if called directly from ordinary C# code.
Static resources​
Use StaticResource<T> when the resource does not need to update after it has
been resolved:
@using Avalonia.Media;
Button.brand {
Background: Amx.StaticResource<IBrush>("BrandBrush");
}
Dynamic resources​
Use DynamicResource<T> when changing the resource should update the styled
property:
Control.w-(double width) {
Width: width * Amx.DynamicResource<double>("--spacing");
}
With the default --spacing value of 4, w-10 produces a width of 40.
The generated utility obtains the resource observable, converts every new
value using the original expression, binds the result, and tracks the binding
for disposal.
Amx.Extend<T> remains an interception placeholder. Utility markup extensions
use ${...} and do not call it.
Diagnostics​
Akbura reports diagnostics before code generation when:
- the extension type is missing or ambiguous;
- a public constructor cannot accept the positional arguments;
- a named extension property is missing or inaccessible;
ProvideValueis missing;- a utility argument cannot produce its declared parameter type;
- a utility variant does not produce
boolorIObservable<bool>; - a nested C# expression or its conversion is invalid.