Skip to content

Instantly share code, notes, and snippets.

@StephaneDelcroix
Created January 27, 2026 09:28
Show Gist options
  • Select an option

  • Save StephaneDelcroix/b6807164a6c7182d8b225594a22471bb to your computer and use it in GitHub Desktop.

Select an option

Save StephaneDelcroix/b6807164a6c7182d8b225594a22471bb to your computer and use it in GitHub Desktop.
XAML C# Expressions Spec v2

XAML C# Expressions

Overview

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.

Motivation

Currently, XAML property values can be:

  1. Literal strings - Text="Hello World"
  2. Markup extensions - Text="{Binding Name}", Text="{StaticResource MyString}"
  3. Type converters - Color="Red" (converted via ColorTypeConverter)

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

Example

<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>

Expression Resolution

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:

Binding Expression

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.

Local Access

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.

Static Invocation

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)}" />

Event Handler

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

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.

Compound Expressions

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.

Expression Syntax

Expressions are standard C#. Any valid C# expression can be used, subject to XML escaping rules.

XML Escaping

XAML is XML, so certain C# characters must be escaped in attribute values:

C# XAML Attribute
&& &amp;&amp;
< &lt;
> &gt;
`
<Label IsVisible="{Count &gt; 0 &amp;&amp; Count &lt; 100}" />

String Quoting

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".

CDATA Alternative

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

Disambiguation

When a {...} value could be interpreted as either a markup extension or an expression, disambiguation rules apply.

Explicit Expression: {= ...}

Prefix with = to force expression parsing:

<Label Text="{= Binding}" />  <!-- Uses the 'Binding' property, not BindingExtension -->

Page Member: {this.Foo}

Use this. to explicitly reference a member on the page/view type:

<Label Text="{this.Title}" />

BindingContext Member: {.Foo}

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.

Member Conflict (MAUIX2008)

When {Foo} exists on both the page and BindingContext:

  • Error: MAUIX2008 is emitted
  • Resolution: Use {this.Foo} for page or {.Foo} for BindingContext

Explicit Markup Extension: {prefix:Name}

Use an xmlns prefix to explicitly invoke a markup extension:

<Label Text="{local:MyMarkup}" />

Ambiguity Warning (MAUIX2007)

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.

Two-Way Binding

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.

Diagnostics

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

Limitations

  • 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

Future Considerations

  • RelativeSource bindings{RelativeSource Self.Width}
  • Attached properties{grid1.(Grid.Row)}
  • Explicit two-way{= Name, Mode=TwoWay} or {Name, set: value => Name = value}

Syntax Cheat Sheet

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 &amp;&amp; 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}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment