Skip to content

Commit 1e1f7ee

Browse files
Merge pull request #812 from AvaloniaUI/788-documentation-on-custom-markup-extension-including-new-optionsmarkupextensions
788 documentation on custom markup extension including new optionsmarkupextensions
2 parents d046aa1 + 78ac33a commit 1e1f7ee

File tree

3 files changed

+157
-25
lines changed

3 files changed

+157
-25
lines changed
Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
---
2-
id: markupextensions
3-
title: Markup Extensions
2+
id: index
3+
title: Markup extensions
4+
description: Markup extensions are simple classes that provide values to XAML properties at runtime. They provide a convenient, reusable option for code-based customization of properties.
45
---
56

6-
A `MarkupExtension` allows code-based customization of setter logic to a target property in a convenient, reusable
7-
syntax within XAML. Curly braces are used to differentiate the usage from plain text.
7+
Markup extensions are simple classes that provide values to XAML properties at runtime. They provide a convenient, reusable option for code-based customization of properties.
88

9-
Avalonia provides the following:
9+
## About markup extensions
10+
11+
A classic markup extension is any class that:
12+
13+
- Implements `object? ProvideValue(IServiceProvider?)`
14+
- Optionally inherits from `MarkupExtension` (not required in Avalonia)
15+
- Is used from XAML via the `{ns:Extension ...}` syntax
16+
17+
In Avalonia, `ProvideValue` is allowed to return **any** type. This means the result can be strongly typed, as the returned value is assigned directly to the target property.
18+
19+
Avalonia provides the following markup extensions:
1020

1121
| MarkupExtension | Assigns to Property |
1222
|--------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
@@ -21,7 +31,7 @@ Avalonia provides the following:
2131

2232
## Compiler intrinsics
2333

24-
These technically fall outside of `MarkupExtension`s as part of the XAML compiler, but the XAML syntax is the same.
34+
These technically fall outside of `MarkupExtension` as part of the XAML compiler, but the XAML syntax is the same.
2535

2636
| Intrinsic | Assigns to Property |
2737
|-----------|-----------------------|
@@ -31,14 +41,13 @@ These technically fall outside of `MarkupExtension`s as part of the XAML compile
3141
| x:Static | Static member value |
3242
| x:Type | `System.Type` literal |
3343

34-
The `x:True` and `x:False` literals have use cases where the target binding property is `object` and you need
35-
to provide a boolean. In these scenarios that lack type information, providing "True" remains a `string`.
44+
The `x:True` and `x:False` literals have use cases where the target binding property is `object` and you need to provide a boolean. In these scenarios that lack type information, providing "True" remains a `string`.
3645

3746
```xml
3847
<Button Command="{Binding SetStateCommand}" CommandParameter="{x:True}" />
3948
```
4049

41-
## Creating MarkupExtensions
50+
## Creating markup extensions
4251

4352
Derive from `MarkupExtension` or add one of the following signatures which are supported via duck-typing:
4453

@@ -49,11 +58,44 @@ object ProvideValue();
4958
object ProvideValue(IServiceProvider provider);
5059
```
5160

52-
When strong types are used instead of `object`, you will receive compile-time errors when there is a mismatch in the
53-
XAML use of constructor parameters, properties, or the return value in `ProvideValue`. When returning `object`, the
54-
actual type returned must match the target property's type else an `InvalidCastException` is thrown at runtime.
61+
Here is a basic example with a markup extension used for localization:
62+
63+
```csharp
64+
public class LocExtension
65+
{
66+
public string Key { get; set; } = "";
67+
68+
public string ProvideValue(IServiceProvider serviceProvider)
69+
{
70+
// Simplified localization lookup
71+
return LocalizationService.GetString(Key) ?? Key;
72+
}
73+
}
74+
```
75+
76+
```xml
77+
<TextBlock Text="{local:Loc Key=WelcomeMessage}" />
78+
```
79+
80+
When strong types are used instead of `object`, you will receive compile-time errors when there is a mismatch in the XAML use of constructor parameters, properties, or the return value in `ProvideValue`. When returning `object`, the actual type returned must match the target property's type, else an `InvalidCastException` is thrown at runtime.
5581

56-
### Receiving Literal Parameters
82+
### Using `IServiceProvider`
83+
84+
The `IServiceProvider` passed to `ProvideValue` exposes XAML context services, enabling the extension to understand where it is used.
85+
86+
Common standard services include:
87+
88+
- **`IProvideValueTarget`** — gives access to the target object and property.
89+
- **`IRootObjectProvider`** — provides the XAML document’s root object.
90+
91+
Avalonia also provides additional, XAML-IL specific services:
92+
93+
- **`IAvaloniaXamlIlParentStackProvider`** — exposes the parent object stack during XAML parsing.
94+
- **`IAvaloniaXamlIlXmlNamespaceInfoProvider`** — provides namespace metadata.
95+
96+
These services are optional, but essential for more advanced or context-aware extensions.
97+
98+
### Receiving literal parameters
5799

58100
When parameters are required, use a constructor to receive each parameter in order.
59101

@@ -84,13 +126,11 @@ public class MultiplyLiteral
84126
<TextBlock Text="This has FontSize=40" FontSize="{namespace:MultiplyLiteral 10, 8, Third=0.5}" />
85127
```
86128

87-
### Receiving Parameters From Bindings
129+
### Receiving parameters from bindings
130+
131+
A common scenario is to transform data coming in from a binding and updating the target property. When all parameters come from bindings, this is straightforward by creating a `MultiBinding` with an `IMultiValueConverter`.
88132

89-
A common scenario is wanting to transform data coming in from a binding and updating the target property. When all parameters
90-
come from bindings, this is somewhat straightforward by creating a `MultiBinding` with an `IMultiValueConverter`. In the
91-
sample below, `MultiplyBinding` requires two bound parameters. If a mix of literal and bound parameters is necessary,
92-
creating an `IMultiValueConverter` would allow for passing of literals as constructor or `init` parameters. `BindingBase`
93-
allows for both `CompiledBinding` and `ReflectionBinding` to be used, but does not allow literals.
133+
In the sample below, `MultiplyBinding` requires two bound parameters. If a mix of literal and bound parameters is necessary, creating an `IMultiValueConverter` would allow for passing of literals as constructor or `init` parameters. `BindingBase` allows for both `CompiledBinding` and `ReflectionBinding` to be used, but does not allow literals.
94134

95135
```csharp
96136
public class MultiplyBinding
@@ -123,15 +163,24 @@ public class MultiplyBinding
123163
```
124164

125165
:::info
126-
127166
An alternate approach is to return an `IObservable<T>.ToBinding()` instead.
128-
129167
:::
130168

131-
### Returning Parameters
169+
### Returning parameters
170+
171+
Avalonia’s markup extension model is flexible: `ProvideValue` may return anything.
172+
173+
This includes:
174+
175+
- Static .NET object
176+
- Typed .NET object, which can be validated at compile time when assigned to a property
177+
- **Binding** instances
178+
- **Observables (`IObservable<T>`)** for dynamic, reactive values
179+
180+
Binding-returning or observable-returning markup extensions are supported and integrate with Avalonia’s property and data-binding systems.
181+
182+
To make a markup extension compatible with multiple target property types, you can set `ProvideValue` to return an `object` in its method signature, so that each type can be handled individually.
132183

133-
To make a `MarkupExtension` compatible with multiple target property types, return an `object` and handle each
134-
supported type individually.
135184

136185
```csharp
137186
public object ProvideValue(IServiceProvider provider)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
id: options-markup-extensions
3+
title: Options markup extensions
4+
description: An options markup extension is a special type of markup extension, specialized for switch-like expressions.
5+
---
6+
7+
`OptionsMarkupExtension` is a special type of markup extension, specialized for switch-like expressions. Its purpose is to provide optimization by removing branches that will never be used, allowing trimming by the compiler.
8+
9+
## `OnPlatform` markup extension
10+
11+
One example of an options markup extension is the built-in `OnPlatform` markup extension. This markup extension defines values per runtime platform (Windows, macOS, Linux, etc.) to optimize branches, selecting only those relevant to the platform being compiled for.
12+
13+
With `OnPlatform`, you can, for instance, use the `Markdown` control on Linux and the `WebView` control on other platforms. The unused control would be excluded, thus reducing the binary size.
14+
15+
## Creating custom options markup extensions
16+
17+
Here is an example of a custom implementation with `RuntimeInformation.ProcessArchitecture`. As shown in this example, we recommend using compiler flags or .NET runtime APIs that are effectively constant.
18+
19+
```csharp
20+
public class ArchitectureExtension : IAddChild<On<object>>
21+
{
22+
[MarkupExtensionOption(nameof(X86))] public object? X86 { get; set; }
23+
[MarkupExtensionOption(nameof(X64))] public object? X64 { get; set; }
24+
[MarkupExtensionOption(nameof(Arm))] public object? Arm { get; set; }
25+
[MarkupExtensionOption(nameof(Arm64))] public object? Arm64 { get; set; }
26+
[MarkupExtensionOption(nameof(Wasm))] public object? Wasm { get; set; }
27+
28+
[Content]
29+
[MarkupExtensionDefaultOption]
30+
public object? Default { get; set; }
31+
32+
public static bool ShouldProvideOption(string option)
33+
{
34+
var currentArch = RuntimeInformation.ProcessArchitecture;
35+
return option switch
36+
{
37+
nameof(X86) => currentArch == Architecture.X86,
38+
nameof(X64) => currentArch == Architecture.X64,
39+
nameof(Arm) => currentArch == Architecture.Arm,
40+
nameof(Arm64) => currentArch == Architecture.Arm64,
41+
nameof(Wasm) => currentArch == Architecture.Wasm,
42+
_ => false,
43+
};
44+
}
45+
46+
// Needed for the compiler.
47+
public void AddChild(On<object> child) {}
48+
public object? ProvideValue() => null;
49+
}
50+
```
51+
52+
This class defines several options that are selected through the `ShouldProvideOption` static method. You can then set the options in XAML, like so:
53+
54+
```xml
55+
<Border Background="{local:Architecture Default=White, X64=Green, Arm64=Red, Wasm=Blue}" />
56+
```
57+
58+
### What’s the difference?
59+
60+
The example above, in a non-optimized .NET build, is equivalent to the following code.
61+
62+
```csharp
63+
border.Background = ArchitectureExtension.ShouldProvideOption("X64") ? Brushes.Green
64+
: ArchitectureExtension.ShouldProvideOption("Arm64") ? Brushes.Red
65+
: ArchitectureExtension.ShouldProvideOption("Wasm") ? Brushes.Blue
66+
: Brushes.White;
67+
```
68+
69+
Once optimized and trimmed for specific platform architecture, it is reduced to the following instead.
70+
71+
```csharp
72+
border.Background = Brushes.Red; // assuming app was compiled with dotnet publish -r win-arm64;
73+
```

sidebars.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,17 @@ const sidebars = {
461461
'concepts/the-mvvm-pattern/avalonia-ui-and-mvvm',
462462
],
463463
},
464-
'concepts/markupextensions',
464+
{
465+
'type': 'category',
466+
'label': 'Markup extensions',
467+
'link': {
468+
'type': 'doc',
469+
'id': 'concepts/markupextensions/index',
470+
},
471+
'items': [
472+
'concepts/markupextensions/options-markup-extensions',
473+
],
474+
},
465475
{
466476
'type': 'category',
467477
'label': 'ReactiveUI',

0 commit comments

Comments
 (0)