XAML Fundamentals in WinUI 3
From Markup to Objects
XAML is an XML-based language for creating objects and setting their properties. Each element in a XAML file declares an object. When WinUI loads <Button>, it creates an instance of the Button class and sets the properties the markup names. An element nested inside another becomes part of its parent, so a page’s markup describes a tree of objects with the page at the root. Every XAML file has exactly one root element.
Each XAML page is paired with a C# code-behind file, such as MainPage.xaml.cs, that holds the page’s hand-written logic. Three pieces connect the two:
x:Classon the root element names the partial class that the code-behind file defines, such asMyApp.MainPage. Without it there is no code-behind class for the markup.x:Nameon an element gives it a name. The build generates a field with that name in the other half of the partial class, so code-behind can reach the element directly.InitializeComponent(), called from the code-behind constructor, loads the markup, creates the objects, and assigns the named fields. It is generated by the build too, which is why a page’s constructor calls a method that appears nowhere in the file you edit. Until it runs, the named fields are null, so constructor code that touches a named element belongs after the call.
From a XAML File to Objects
A page's markup, its two partial-class halves, and the objects they create.
Markup can also wire events. <Button Click="SaveButton_Click" /> attaches the code-behind method SaveButton_Click to the button’s Click event, and the build fails if no method with that name and the event’s delegate signature exists.
The build compiles the markup and can catch some mistakes there, such as a missing event handler. Others surface only when InitializeComponent runs, as a XamlParseException that names the file’s line number.
XAML is case-sensitive, and names in x:Name must be unique within their namescope. The page root creates one namescope, and every template creates its own. A template is a definition WinUI uses to build a tree of elements on demand. A control template builds a control’s visuals, such as the border and content area a Button draws itself with. A data template builds the visuals for one data item in a list. The control a control template is building is its templated parent, and names inside a template don’t collide with names on the page.
Setting Properties
Attribute and Property Element Syntax
The compact form sets a property as an XML attribute on the element:
<Button Content="Click me" Width="120" HorizontalAlignment="Center" />
An attribute value is a string. When the value is an object that a string can’t describe, property element syntax sets it instead. A child element named TypeName.PropertyName holds the value as a nested object element:
<Button>
<Button.Content>
<StackPanel Orientation="Horizontal">
<Image Source="icon.png" Width="16" />
<TextBlock Text="Click me" />
</StackPanel>
</Button.Content>
</Button>
Attribute order doesn’t matter, and many properties accept only one of the two forms. Each property’s reference page shows which XAML usages it supports.
Content Properties and Collections
A class can mark one property as its content property with [ContentProperty(Name = "...")]. Child elements or inner text with no property element around them go to that property. Button’s content property is Content, so the example above could drop the <Button.Content> wrapper and nest the StackPanel directly inside the Button. Border’s is Child. TextBlock’s is Inlines, its collection of formatted text runs, not Text, so the inner text of <TextBlock>Hello</TextBlock> becomes the text block’s inline content.
When the content property is a collection, as Children is for every panel, the children are added to it:
<StackPanel>
<TextBlock Text="Hello" />
<TextBlock Text="World" />
</StackPanel>
This looks like it assigns the read-only Children property, but it doesn’t. The parser creates each child and calls the existing collection’s Add method, in document order. Writing the collection out as an explicit object element (<StackPanel.Children><UIElementCollection>...) tries to create a new collection instead, and can throw a parse exception on a read-only property.
The content can come before or after the element’s other property elements, but not on both sides of them. A StackPanel with buttons, then <StackPanel.Resources>, then more buttons is invalid XAML.
How Attribute Strings Become Typed Values
Because every attribute value starts as a string, the parser has to convert it to the property’s type. For primitives (numbers, Booleans, strings) the conversion is built into the parser. Several other types have their own string grammar:
| Written as | Becomes |
|---|---|
Margin="8,4,8,4" |
A Thickness with left, top, right, bottom of 8, 4, 8, 4 |
Margin="20,10" |
A Thickness of 20 left and right, 10 top and bottom |
Background="#FF0078D4" or Background="Transparent" |
A new SolidColorBrush of that color, for any property of type Brush |
Visibility="Collapsed" |
The Visibility.Collapsed enum member |
ManipulationMode="TranslateX,TranslateY" |
A combination of flags enum members, comma-separated with no spaces |
Enum values use the member’s unqualified name. Visibility="Visibility.Collapsed" is invalid, and the member’s integer value might appear to work but depends on an implementation detail.
WinUI has no TypeConverter as WPF does. A type you define can opt into string conversion with the Windows.Foundation.Metadata.CreateFromString attribute, whose MethodName gives the fully qualified name of a static method that parses the string. A property whose type has no string conversion takes its value from property element syntax or from a markup extension, described below.
XAML Namespaces
A namespace declaration applies to the element that carries it and everything inside that element, so files declare them on the root:
<Page
x:Class="MyApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyApp">
- The default namespace maps to WinUI’s own types in the
Microsoft.UI.Xaml.*namespaces, so<Grid>and<TextBox>need no prefix. The URI is the same one WPF, Silverlight, and UWP used, which makes markup easier to carry between them. - The
x:namespace holds XAML language features rather than types:x:Class,x:Name,x:Keyfor resources,x:Uidfor localization, andx:Bind, among others. - A
using:mapping makes a code namespace available under a prefix. Afterxmlns:local="using:MyApp",<local:RatingControl />resolves toMyApp.RatingControl. The mapping names only the namespace, never the assembly. Any assembly the project references can supply types, which differs from WPF’sclr-namespace:...;assembly=...form.
Visual Studio’s templates often add d: and mc: as well. The d: namespace holds design-time attributes, and mc:Ignorable="d" tells the runtime parser to skip them.
A custom type used from XAML can’t be a nested type. The parser reads Outer.Inner as a namespace path followed by a type name, so it has no way to tell a nested class from a namespace.
Markup Extensions
A markup extension sets a property to something a literal string can’t express, such as a resource or a binding. A resource is an object, such as a brush or a style, stored under an x:Key in a ResourceDictionary so that many elements can share it. An extension is written in curly braces inside an attribute value:
<Button Style="{StaticResource AccentButtonStyle}" />
WinUI supports a fixed set of built-in extensions:
| Extension | What it supplies |
|---|---|
{StaticResource} |
A resource looked up by key once, when the element loads |
{ThemeResource} |
A resource looked up by key, and looked up again when the app switches between light, dark, and high-contrast themes |
{x:Bind} |
A binding compiled into generated code at build time |
{Binding} |
A binding resolved at runtime against the element’s DataContext, the data object it inherits from its parent |
{TemplateBinding} |
Inside a control template, the value of a property on the templated parent |
{RelativeSource} |
A binding source relative to the target, such as the templated parent or the element itself |
{CustomResource} |
A resource from a custom lookup the app registers |
{x:Null} |
A null value, for properties that accept one |
Extensions can be nested, and the innermost is evaluated first. WPF’s {x:Type}, {x:Static}, and {DynamicResource} aren’t in the list, so markup copied from WPF can fail on them.
An app can also define its own extension by deriving from Microsoft.UI.Xaml.Markup.MarkupExtension and overriding ProvideValue. The overload that takes an IXamlServiceProvider can ask for the target object and property the extension is being applied to.
Because { starts an extension, an attribute value that should begin with a literal brace is escaped with {}. Text="{}{0} items" sets the text to {0} items.
Dependency Properties
Many properties on WinUI elements aren’t backed by a field. They are dependency properties, stored and computed by a property system that lives in the DependencyObject base class. A single property can be given values from several places at once:
- A style, a reusable set of property values, each one a
Setter, that can be applied to many elements. - A template, which sets properties on the elements it builds.
- A storyboard, which animates a property’s value over time. Controls use storyboards to switch between visual states such as pressed and disabled.
- A binding, or code that sets the property directly.
The property system keeps all of them and decides which one wins. Styles, storyboard animations, and {Binding} targets all require a dependency property, and a type has to derive from DependencyObject to define one.
Value Precedence
When several sources supply a value, the highest in this order wins:
- Animated values. A running storyboard, including a visual state’s, or a finished one set to hold its end value (
FillBehavior.HoldEnd). - Local value. Set in code, as an attribute or property element in XAML, or through a binding or
{StaticResource}. - Templated properties. Values set on an element that a template created.
- Style setters. Values from a
Setterin a style. - Default value. The value registered with the property, shown in the next section.
The order explains behavior that otherwise looks like a bug. Setting Background on a button overrides the brush its style supplies, because a local value outranks a style setter. An animation overrides that local value while it runs, and the local value shows again when the animation is removed.
A binding counts as a local value, so assigning the property in code replaces the {Binding} entirely rather than changing its current value. {x:Bind} generated code also sets a local value, so a value assigned in code is overwritten the next time the binding updates. To remove a local value and let the style or default apply again, call ClearValue rather than assigning a value.
Defining a Dependency Property
A custom control or UserControl defines a dependency property with a static identifier, registered once, and an ordinary C# property that wraps GetValue and SetValue:
public static readonly DependencyProperty HeaderTextProperty =
DependencyProperty.Register(
nameof(HeaderText),
typeof(string),
typeof(MyCard),
new PropertyMetadata(string.Empty, OnHeaderTextChanged));
public string HeaderText
{
get => (string)GetValue(HeaderTextProperty);
set => SetValue(HeaderTextProperty, value);
}
private static void OnHeaderTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var card = (MyCard)d;
// respond to e.OldValue and e.NewValue
}
PropertyMetadata carries the default value and an optional property-changed callback. The callback runs when the property’s effective value, the one that wins under precedence, changes, whether the change came from a binding, a style, or code. For enum and struct properties it can also run when the value didn’t change, so a callback shouldn’t assume OldValue and NewValue differ.
Put logic in the callback rather than the wrapper’s setter. The XAML parser, bindings, and styles set the value through the property system without calling the wrapper, so code in the setter would be skipped. To observe a dependency property on a control you didn’t write, call RegisterPropertyChangedCallback on the instance.
Setting a dependency property in the control’s constructor gives it a local value, which then outranks any style an app applies. A default for a number, string, enum, or other immutable value belongs in the metadata instead. A mutable reference type, such as a collection, is the exception. The metadata default is one object shared by every instance, so a list registered there becomes a single list that every control adds to. Create a collection per instance in the constructor.
Not every property needs to be a dependency property. One that no binding targets, no style sets, and no animation drives works as an ordinary C# property.
A DependencyObject belongs to the UI thread it was created on, and code on another thread can’t read or write its dependency properties directly.
Attached Properties
An attached property is a dependency property that one class defines and other elements carry. It lets an element hold a setting that means something to a different class, usually its parent panel, without the element’s own type knowing about it.
Grid.Row="1" on a TextBox is the common case. Grid defines the RowProperty attached property, and the XAML sets it on the text box, which has the same effect as calling Grid.SetRow(textBox, 1). The value is stored on the TextBox, and the Grid reads it back with Grid.GetRow(child) when it decides where each child goes. Canvas.Left, ScrollViewer.VerticalScrollBarVisibility, ToolTipService.ToolTip, and AutomationProperties.Name work the same way.
A custom attached property uses RegisterAttached and a pair of static Get and Set methods in place of an instance property:
public static readonly DependencyProperty BadgeCountProperty =
DependencyProperty.RegisterAttached(
"BadgeCount",
typeof(int),
typeof(BadgeHelper),
new PropertyMetadata(0));
public static int GetBadgeCount(DependencyObject obj) =>
(int)obj.GetValue(BadgeCountProperty);
public static void SetBadgeCount(DependencyObject obj, int value) =>
obj.SetValue(BadgeCountProperty, value);
After mapping the namespace, markup sets it on any element as local:BadgeHelper.BadgeCount="3". A property-changed callback in the metadata can then react to the value, which makes attached properties a common way to add behavior to existing controls without subclassing them. Unlike WinUI’s own attached properties, a custom one can’t be the target of a storyboard animation.
Found this guide helpful? Share it with your team:
Share on LinkedIn