XAML C# Expressions allow writing C# expressions directly in XAML attribute values. The source generator parses these expressions and generates appropriate bindings or event handlers.
Currently, XAML property values can be:
- Literal strings -
Text="Hello World" - Markup extensions -
Text="{Binding Name}",Text="{StaticResource MyString}" - Type converters -
Color="Red"(converted viaColorTypeConverter)
However, there's no way to directly invoke C# code (methods, properties, expressions) from XAML without creating a binding or code-behind event handler. This leads to:
- Boilerplate for simple computed values
- Unnecessary runtime overhead for static computations
- Reduced readability when simple expressions are wrapped in bindings
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:DataType="local:MainBindingContext">
<StackLayout>
<Label Text="{$'{FirstName} {LastName}'}" />
<Label Text="{Items.Count}" />
<Button Clicked="{(s, e) => Count++}" />
</StackLayout>
</ContentPage>When the source generator encounters a {...} value that cannot be matched to a markup extension, it treats the content as a C# expression. Resolution follows this order:
If the target property is bindable and the expression can be matched as a BindingContext property path, the generator creates a Binding:
<Label Text="{Username}" />
<Label Text="{User.DisplayName}" />This is equivalent to {Binding Username} but with compile-time type checking. The path is resolved against the x:DataType.
If the expression cannot be converted to a binding, the generator checks for instance properties or methods on the page/view type:
<Label Text="{Title}" />
<Label Text="{GetFormattedDate()}" />The value is captured once at initialization time.
If no instance member matches, the generator checks for static methods and properties. Global usings are respected:
<Label Text="{DateTime.Now}" />
<Label Text="{Math.Max(A, B)}" />For event properties, lambda expressions generate event handler delegates:
<Button Clicked="{(s, e) => Count++}" />
<Button Clicked="{async (s, e) => await SaveAsync()}" />The generator creates a method with the appropriate signature and wires it to the event.
String interpolation expressions generate formatted strings with embedded values:
<Label Text="{$'{FirstName} {LastName}'}" />
<Label Text="{$'{Quantity}x {ProductName}'}" />
<Label Text="{$'Total: {Price:C2}'}" />Each interpolation hole is analyzed and bound appropriately.
Expressions can combine multiple binding paths, local members, method calls, and static members:
<!-- Arithmetic: binding + method -->
<Label Text="{Price * GetTaxRate()}" />
<!-- Boolean: two bindings -->
<Label IsVisible="{IsAdmin || IsPrivileged}" />
<!-- Comparison: binding + local -->
<Label IsVisible="{Count > MinimumCount}" />
<!-- Mixed: string interpolation, binding, static -->
<Label Text="{$'{Name} - {DateTime.Now:d}'}" />The generator analyzes the expression and creates appropriate bindings or captured values for each part.
Expressions are standard C#. Any valid C# expression can be used, subject to XML escaping rules.
XAML is XML, so certain C# characters must be escaped in attribute values:
| C# | XAML Attribute |
|---|---|
&& |
&& |
< |
< |
> |
> |
| ` |
<Label IsVisible="{Count > 0 && Count < 100}" />Since XAML attributes use double quotes, C# strings use single quotes:
<Label Text="{Name ?? 'Unknown'}" />
<Label Text="{IsVip ? 'Gold' : 'Standard'}" />Single quotes are converted to double quotes in the generated C#. Escape single quotes with backslash: 'it\'s' becomes "it's".
For complex expressions, use element syntax with CDATA to avoid escaping entirely:
<Label>
<Label.IsVisible><![CDATA[{Count > 0 && Count < 100}]]></Label.IsVisible>
</Label>
<Label>
<Label.Text><![CDATA[{Value ?? "Default"}]]></Label.Text>
</Label>Use CDATA when:
- Expression has multiple
&&or comparison operators - You need double-quoted strings
- The escaped version is hard to read
When a {...} value could be interpreted as either a markup extension or an expression, disambiguation rules apply.
Prefix with = to force expression parsing:
<Label Text="{= Binding}" /> <!-- Uses the 'Binding' property, not BindingExtension -->Use this. to explicitly reference a member on the page/view type:
<Label Text="{this.Title}" />Use . prefix to explicitly reference a member on the BindingContext:
<Label Text="{.Title}" />This is useful when both page and BindingContext have a property with the same name.
When {Foo} exists on both the page and BindingContext:
- Error: MAUIX2008 is emitted
- Resolution: Use
{this.Foo}for page or{.Foo}for BindingContext
Use an xmlns prefix to explicitly invoke a markup extension:
<Label Text="{local:MyMarkup}" />When {Foo} matches both a markup extension (FooExtension) and a property (Foo):
- Behavior: Defaults to markup extension (backward compatible)
- Warning: MAUIX2007 is emitted
- Resolution: Use
{= Foo}for expression or{local:Foo}for explicit markup
This is extremely unlikely in practice—it requires a property name that exactly matches a markup extension name without its Extension suffix.
Simple property paths support two-way binding automatically:
| Expression | Two-Way? |
|---|---|
{Name} |
✅ |
{User.Name} |
✅ |
{Price * Qty} |
❌ |
{Name.ToUpper()} |
❌ |
Complex expressions (operators, method calls) cannot generate a setter and are one-way only. Binding a complex expression to a two-way property (like Entry.Text) emits warning MAUIX2010.
| Code | Severity | Description |
|---|---|---|
| MAUIX2007 | Warning | Bare identifier matches both markup extension and property — defaults to markup |
| MAUIX2008 | Error | Member exists on both page and BindingContext — use this. or . to disambiguate |
| MAUIX2009 | Error | Member not found on page or BindingContext |
| MAUIX2010 | Warning | Complex expression cannot generate setter — two-way binding unavailable |
- SourceGen only — not available in XamlC or Runtime inflation
- Single expressions — no multi-statement blocks or control flow
- Event lambdas require parameters —
{(s, e) => ...}not{() => ...} - Static types need qualification — use full path or ensure global usings are in place
- RelativeSource bindings —
{RelativeSource Self.Width} - Attached properties —
{grid1.(Grid.Row)} - Explicit two-way —
{= Name, Mode=TwoWay}or{Name, set: value => Name = value}
| Want to... | Write |
|---|---|
| Bind to property | {Name} |
| Bind to nested property | {User.Email} |
| Call local method | {GetDisplayText()} |
| Call static method | {Math.Max(A, B)} |
| Compute value | {A * B} |
| Negate bool | {!Flag} |
| Combine bools | {A && B} or {A || B} |
| Ternary | {IsVip ? 'Gold' : 'Standard'} |
| Null-coalesce | {Value ?? 'Default'} |
| Format string | {$'{Value:F2}'} |
| Handle event | {(s, e) => Action()} |
| Async event | {async (s, e) => await ActionAsync()} |
| Force expression | {= Foo} |
| Force page member | {this.Foo} |
| Force BindingContext | {.Foo} |