3. The design of the user interface¶
The aim of the laboratory¶
The goal of this laboratory is to familiarize ourselves with the basics of thick-client application development using the declarative XAML markup technology. The foundational knowledge learned here applies to all XAML dialects (WinUI, WPF, UWP, Xamarin.Forms, MAUI) or can be applied in a very similar way. However, in today’s session, we will specifically use XAML through the WinAppSDK / WinUI 3 framework.
Prerequisites¶
Tools required for the lab:
- Windows 10 or Windows 11 operating system (Linux and macOS are not suitable)
A description of the necessary development environment can be found here.
Development environment for WinUI3
Compared to previous labs, additional components need to be installed. The above page mentions that the ".NET desktop development" Visual Studio Workload needs to be installed. Additionally, at the bottom of the same page, there is a "WinUI support" section where all specified steps must be completed!
Solution¶
Download the completed solution
It is essential to work following the instructor during the lab, it is forbidden (and pointless) to download the final solution in advance. However, during subsequent independent practice, it can be useful to review the final solution, so we make it available.
The solution is available on GitHub under the megoldas
branch. The easiest way to download it is by cloning the megoldas
branch using the git clone
command in the terminal:
git clone https://github.com/bmeviauab00/lab-xaml-kiindulo -b megoldas
For this, command-line Git must be installed on your computer. More information can be found here.
Starter project¶
In the first task, we will set up the environment where we will explore the XAML language and the WinUI framework. We could generate the initial project using Visual Studio (WinUI 3 project, Blank App, Packaged (WinUI 3 in Desktop) type), but for the sake of efficiency, we will use a pre-prepared project.
You can clone the project to your local machine by running the following command:
git clone https://github.com/bmeviauab00/lab-xaml-kiindulo.git
Open HelloXaml.sln
.
Let's review the files included in the project:
- App
- Contains two files:
App.xaml
andApp.xaml.cs
(we will clarify later why there are two files) - Application entry point:
OnLaunched
overridden method inApp.xaml.cs
- In this case, we initialize the application's only window,
MainWindow
, here
- Contains two files:
- MainWindow
- The
.xaml
and.xaml.cs
files for the main window of our application.
- The
Additional solution elements
The initial Visual Studio solution also contains the following elements:
- Dependencies
- Frameworks
Microsoft.AspNetCore.App
: .NET SDK metapackage (includes references to core Microsoft .NET and SDK components)- Windows-specific .NET SDK
- Packages
- Windows SDK Build Tools
- WindowsAppSDK
- Frameworks
- Assets
- Application logos
- app.manifest, Package.appxmanifest
- XML files containing application metadata, where we can specify logos and, similar to Android, request permissions for security-critical system resources.
Run the application!
Introduction to XAML¶
We will define the user interface using an XML-based markup language called XAML.
Graphical designer interface
In some XAML dialects (e.g. WPF), a graphical designer tool is available for UI design. However, it often generates less efficient XAML code. Moreover, Visual Studio now supports Hot Reload for XAML, so there is no need to stop the application while editing XAML, and changes can be seen immediately in the running application. Because of this, WinUI does not provide a designer tool in Visual Studio. However, there are limitations, 'more significant' changes may require restarting the application.
Xaml language basics¶
The XAML language:
- An object instantiation language
- Standard XML
- XML elements/tags instantiate objects, whose classes are standard .NET classes
- XML attributes set properties (dependency properties)
- Declarative
Let's examine the XAML generated by the project template (MainWindow.xaml
). We can see that every control is represented as an XML element/tag in XAML.
Each control's properties are set using attributes on its tag, e.g., HorizontalAlignment
, which aligns the control within its container (in this case, the window).
Controls can contain other controls, forming a hierarchical tree structure.
Let's analyze MainWindow.xaml
in more detail:
- Root tag namespaces: Define what tags and attributes can be used in the XML.
- Default namespace: Contains XAML elements/controls (e.g.,
Button
,TextBox
). x
namespace: The namespace for the XAML parser (e.g.,x:Class
,x:Name
).- Other custom namespaces can be referenced.
- Default namespace: Contains XAML elements/controls (e.g.,
Window
root tag:- A .NET class is generated based on our window/page, inheriting from the
Window
class. - The name of the derived class is defined by the
x:Class
attribute. For example,x:Class="HelloXaml.MainWindow"
means the class will beMainWindow
inside theHelloXaml
namespace. - This is a partial class, its "other half" is located in the so-called code-behind file (
MainWindow.xaml.cs
). See the next point.
- A .NET class is generated based on our window/page, inheriting from the
- Code-behind file (
MainWindow.xaml.cs
):- The other "half" of the partial class: check that the class name and namespace match what is defined in the
.xaml
file (partial class!). - Event handlers and helper functions are placed here (among others).
this.InitializeComponent();
: it must always be called in the constructor. It reads the XAML at runtime, instantiates, and initializes the window/page content (i.e., the controls defined in the XAML file with their specified properties).
- The other "half" of the partial class: check that the class name and namespace match what is defined in the
Let's delete the contents of the Window
and remove the event handler (myButton_Click
function) from the code-behind file.
Now, we will manually write XAML to define the user interface. Let's add a Grid
inside the Window
, which will later help us create a table-based layout:
<?xml version="1.0" encoding="utf-8"?>
<Window
x:Class="HelloXaml.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:HelloXaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
</Grid>
</Window>
Run the application (e.g., by pressing F5). The Grid
now fills the entire window, and its color matches the window's background, making it indistinguishable visually.
During the next tasks, keep the application running so that we can immediately see the changes we make to the UI.
Hot Reload limitations
Keep in mind the limitations of Hot Reload: if a change does not appear on the running application's UI, restart the application!
Object instances and their properties¶
Now, let's see how to instantiate objects and set their properties using XAML.
Add a Button
inside the Grid
. The Content
property allows us to specify the button's text, or more precisely, its content.
<Button Content="Hello WinUI App!"/>
At runtime, this declaration creates a Button
object at the specified location and sets its Content
property to "Hello WinUI App!". We could achieve the same result in the code-behind file using C#, but it would result in less readable code:
// For example, adding this at the end of the constructor:
Button b = new Button();
b.Content = "Hello WinUI App!";
rootGrid.Children.Add(b);
// To make the above line work, the Grid in the XAML file must have the attribute
// x:Name="rootGrid" so that it can be accessed in the code-behind file.
This example clearly illustrates that XAML is fundamentally an object instantiation language and supports setting object properties.
The Content
property is special, it can be specified not only as an XML attribute but also within a tag (XML element).
<Button>Hello WinUI App!</Button>
Moreover! We can place not only text on the button but also any other element. For example, let's add a red circle inside it. The circle is 10 pixels wide, 10 pixels high, and its color (Fill
) is red.
<Button>
<Ellipse Width="10" Height="10" Fill="Red" />
</Button>
In earlier .NET UI technologies (e.g., Windows Forms), this would not have been so easy to implement.
Now, let's place the Record label next to the red circle (so that the button has meaning). The button can only have one child, so we need to place the circle and the text (TextBlock
) inside a layout control (such as a StackPanel
). Let's also add a left margin to the TextBlock
so that it does not touch the circle.
<Button>
<StackPanel Orientation="Horizontal">
<Ellipse Width="10" Height="10" Fill="Red" />
<TextBlock Text="Record" Margin="10,0,0,0" />
</StackPanel>
</Button>
The StackPanel
is a simple layout panel used for arranging controls: it arranges the contained controls side by side when the Orientation
is set to Horizontal
, or one below the other when the Orientation
is set to Vertical
. In our example, it simply places the two controls next to each other.
The result is as follows:
XAML vector graphic controls
It's important to note that most XAML controls are vector-based. This button will appear sharp (without any "pixelation") regardless of the DPI or zoom level.
There are three ways to specify properties for controls instantiated in XAML (which we have already partially used):
- Property ATTRIBUTE syntax
- Property ELEMENT syntax
- Property CONTENT syntax
Let's now take a closer look at these options:
-
Property ATTRIBUTE syntax. We have already used this, specifically in our first example:
<Button Content="Hello WinUI App!"/>
The name comes from the fact that the property is specified in the form of an XML attribute. Since XML attributes can only be strings, it can only be used to access simple number/string/etc. values in string form, or member variables and event handlers defined in a code-behind file. However, using type converters, it is possible to specify "complex" objects as well. Although we won't go into much detail here, the built-in type converters are often used, almost "instinctively." Example:
Let's add a background color to the
Grid
:<Grid Background="Azure">
Or, we can also specify it in hexadecimal format:
<Grid Background="#FFF0FFFF">
The margin (
Margin
) is also a complex value, and the associated type converter expects the values for the four sides (left, top, right, bottom) separated by commas (or spaces). We have already used this for theTextBlock
with theRecord
label. Note: A single number can also be used for the margin, which will apply the same value to all four sides. -
Property ELEMENT syntax. This allows us to set a property to a complex object that is instantiated/parameterized without using type converters. Let's look at an example.
- In the previous example, when setting the
Background
property toAzure
, it actually creates aSolidColorBrush
and sets its color to light blue. This can be done without using a type converter as follows:
<Grid> <Grid.Background> <SolidColorBrush Color="Azure" /> </Grid.Background> ...
This sets the
Background
property of theGrid
to the specifiedSolidColorBrush
. This is known as "property element syntax" for setting a property.- The name comes from the fact that the property is set in the form of an XML element (rather than an XML attribute).
Here, the
<Grid.Background>
element does not create an object instance but rather sets the value of the specified property (Background
) to an instance of a given object (in this case, aSolidColorBrush
). This can be identified by the dot notation in the XML element name.- This is a more verbose way of setting properties, but it offers full flexibility.
Now, let's replace the
SolidColorBrush
with a gradient colorBrush
(LinearGradientBrush
):<Grid> <Grid.Background> <LinearGradientBrush> <LinearGradientBrush.GradientStops> <GradientStop Color="Black" Offset="0" /> <GradientStop Color="White" Offset="1" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Grid.Background> ...
A
LinearGradientBrush
doesn't have a type converter, so we could only specify it using the element syntax.The question is, how can we set both a
SolidColorBrush
and aLinearGradientBrush
for theGrid
control'sBackground
property? The answer is simple, polymorphism makes this possible:- Both
SolidColorBrush
andLinearGradientBrush
are subclasses of the built-inBrush
class. - The
Background
property is of typeBrush
, so polymorphism allows any subclass ofBrush
to be used.
Note
- In the examples above, when specifying the
Color
- e.g.Color="Azure"
- the wordAzure
is converted by the type converter into a blueColor
instance. Here’s how the previousSolidColorBrush
example would look fully expanded:
<Grid> <Grid.Background> <SolidColorBrush> <SolidColorBrush.Color> <Color>#FFF0FFFF</Color> </SolidColorBrush.Color> </SolidColorBrush> </Grid.Background> ...
- Where supported, it’s worth taking advantage of type converters and use attribute syntax to keep the XAML concise.
- For value types (
struct
), such asColor
, the value must be set at object instantiation ("constructor time"). Therefore, the properties cannot be set separately, and we must rely on a type converter.
- In the previous example, when setting the
-
Property CONTENT syntax. To better understand this, let’s look at the three ways we can set a
Content
property of a button to some text (you don’t need to do this in the lab; it's enough to look at it together in this guide):- Property attribute syntax (we’ve already used it):
<Button Content="Hello WinUI App!"/>
- Set it using the property element syntax learned in the previous section:
<Button> <Button.Content> Hello WinUI App! </Button.Content> </Button>
- Every control can define a dedicated "Content" property, which allows us to skip the opening and closing tags. So, the
<Button.Content>
opening and closing tags in the previous example can be omitted for this one property:Or written on one line:<Button> Hello WinUI App! </Button>
This is familiar — we saw it in our introductory example: this is the Property CONTENT syntax for setting properties. The name itself suggests that we can specify this one property directly in the control's "content" section. Not every control uses<Button>Hello WinUI App!</Button>
Content
as this dedicated property name: for example,StackPanel
andGrid
useChildren
. Let’s recall and check the code: we’ve already used them, but we didn’t write out theStackPanel.Children
orGrid.Children
XML elements when specifying the contents ofStackPanel
orGrid
(though we could have!).
- Property attribute syntax (we’ve already used it):
Now, let's either simplify the background of the Grid
to something simple or remove the background color specification entirely.
Event handling¶
XAML applications are event-driven applications. We get notified of every user interaction through events, and based on these, we can update the interface.
Now let's handle the button click event.
As a first step, let's give the TextBlock
control a name so that we can reference it from the code-behind file later:
<TextBlock x:Name="recordTextBlock" Text="Record" Margin="10,0,0,0" />
The x:Name
"speaks" to the XAML parser, it will create a member variable in our class with this name, which holds the reference to the corresponding control. Let's think about it: since it will be a member variable, we can access it from the code-behind file because it is a "partial part" of the same class!
Named controls
Don't give names to controls that you don't intend to reference. (We should get used to only referencing those that are absolutely necessary. Data binding will also help with this.)
Exception: If the control hierarchy is very complex, names can help make the code clearer, as they will appear in the Live Visual Tree window, and the generated event handler names will align with them.
Let's handle the button Click
event and then try out the code.
<Button Click="RecordButton_Click">
private void RecordButton_Click(object sender, RoutedEventArgs e)
{
recordTextBlock.Text = "Recording...";
}
Creating event handlers
If we don't select New Event Handler for event handlers, but instead manually type the desired name and then press F12 or right-click / Go to Definition, the event handler will be generated in the code-behind file.
An event handler has two parameters: the sender object (object sender
) and the parameter containing the event details/circumstances (EventArgs e
). Let's take a closer look at these:
object sender
: The object that triggered the event. In our case, it is the button itself, and we could cast it toButton
. We rarely use this parameter.- The second parameter is always of type
EventArgs
or one of its descendants (depending on the event type), which contains the event's details. In the case of theClick
event, this is of typeRoutedEventArgs
.
Event arguments
Some types of event arguments:
RoutedEventArgs
: Used for events like theClick
event, as shown in our example. TheOriginalSource
property gives us the control where the event was first triggered.- Note: In the above case, it is the button itself, but for example, if we were handling a mouse-down event (not the
Click
butPointerPressed
) on aStackPanel
, we might get one of its child elements if it was clicked.
- Note: In the above case, it is the button itself, but for example, if we were handling a mouse-down event (not the
KeyRoutedEventArgs
: Used for events likeKeyDown
(key press), and it provides the key that was pressed.PointerRoutedEventArgs
: Used for events likePointerPressed
(mouse/stylus press), through which we can query, among other things, the click coordinates.
XAML event handlers are entirely built on C# events (event
keyword, see previous practice):
For example:
<Button Click="RecordButton_Click">
is mapped to:
Button b = new Button();
b.Click += RecordButton_Click;
Layout, arrangement¶
The arrangement of controls is determined by two factors:
- Layout (panel) controls and their attached properties
- General positioning properties within the parent control (e.g., margin, vertical or horizontal alignment)
Built-in layout controls include:
StackPanel
: arranges elements vertically or horizontally.Grid
: allows defining a grid structure to align elements accordingly.Canvas
: enables explicit positioning of elements by specifying X and Y coordinates.RelativePanel
: defines the relationship between elements with constraints.
We will try out the Grid
control (as it is typically used to structure the main layout of a window or page). We will create an interface where users can add people to a list by specifying their name and age. The final layout should look like this:
Key behavioral constraints:
- When resizing the window, the form should have a fixed width and remain centered.
- In the Age row, the "+" button should increase the age, while the "-" button should decrease it.
- Clicking the Add button should add the specified person to the list below (in the image, two people have already been added to the list).
We define 4 rows and 2 columns in the root Grid
. The first column contains labels, while the second column holds input fields. We also move our existing button to the third row, change its text to Add, and replace the circle with a SymbolIcon
. The fourth row will contain a list that spans two columns.
<Grid x:Name="rootGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Name"/>
<TextBox Grid.Row="0" Grid.Column="1" x:Name="tbName"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Age"/>
<TextBox Grid.Row="1" Grid.Column="1" x:Name="tbAge"/>
<Button Grid.Row="2" Grid.Column="1">
<StackPanel Orientation="Horizontal">
<SymbolIcon Symbol="Add" />
<TextBlock Text="Add" Margin="5,0,0,0"/>
</StackPanel>
</Button>
<ListView Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2"/>
</Grid>
Row and column definitions can specify that a row/column should take the size of its content (Auto
), fill the remaining space (*
), or have a fixed size in pixels (using the Width
property). If multiple *
values are used in definitions, they can be proportioned. For example, *
and *
represent a 1:1 ratio, while *
and 3*
represent a 1:3 ratio.
The Grid.Row
and Grid.Column
properties are known as attached properties. This means that the control where they are applied does not have these properties, and the information is simply "attached" to it. In this case, this information is essential for the Grid
to position its child elements correctly. The default value of Grid.Row
and Grid.Column
is 0
, meaning that explicitly specifying them is not necessary.
Imperative UI description
In other UI frameworks where the UI is constructed imperatively, this is often handled with function parameters, such as myPanel.Add(new TextBox(), 0, 1)
.
Another property requiring explanation is Grid.ColumnSpan="2"
in the ListView
definition. The ColumnSpan
and RowSpan
properties determine how many columns or rows a control spans. In our example, the ListView
spans across both columns.
Run the application (if the code does not compile, remove the RecordButton_Click
event handler from the code-behind file).
At this stage, the Grid
stretches to fill the entire space both horizontally and vertically. Why is this happening? One of the key pillars of control arrangement is the HorizontalAlignment
and VerticalAlignment
properties. These determine where a given control is positioned within its parent container (or parent control) horizontally and vertically. Possible values:
VerticalAlignment
:Top
,Center
,Bottom
,Stretch
(aligns at the top, center, bottom, or stretches vertically).HorizontalAlignment
:Left
,Center
,Right
,Stretch
(aligns to the left, center, right, or stretches horizontally).
(Note: For Stretch
to work, the control must not have explicit Height
and Width
values.)
Since we did not set HorizontalAlignment
or VerticalAlignment
for our Grid
, so their value is the default Stretch
(in case of Grid
), causing our Grid
to fill the entire available space in both directions within the parent container, which is the window.
Our interface does not yet look as intended, so let’s refine its appearance with the following changes:
- The table should not fill the entire screen width but be centered horizontally:
HorizontalAlignment="Center"
- Set a fixed width of 300px:
Width="300"
- Make the distance between the rows 5px, between the columns 10px and keep 20px distance from the edge of the container:
RowSpacing="5" ColumnSpacing="10" Margin="20"
- Align labels (
TextBlock
) vertically centered:VerticalAlignment="Center"
- Align the button to the right:
HorizontalAlignment="Right"
- Make the list visually distinct:
BorderThickness="1"
andBorderBrush="DarkGray"
<Grid x:Name="rootGrid"
Width="300"
HorizontalAlignment="Center"
Margin="20"
RowSpacing="5"
ColumnSpacing="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Name" VerticalAlignment="Center"/>
<TextBox Grid.Row="0" Grid.Column="1" x:Name="tbName" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="Age" VerticalAlignment="Center"/>
<TextBox Grid.Row="1" Grid.Column="1" x:Name="tbAge"/>
<Button Grid.Row="2" Grid.Column="1" HorizontalAlignment="Right">
<StackPanel Orientation="Horizontal">
<SymbolIcon Symbol="Add"/>
<TextBlock Text="Add" Margin="5,0,0,0" />
</StackPanel>
</Button>
<ListView Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="2"
BorderThickness="1"
BorderBrush="DarkGray"/>
</Grid>
Let's extend our form with two more buttons (± buttons for age, see the previous animated screenshot):
-
: on the left side of theTextBox
+
: on the right side of theTextBox
For this, let's replace the following line:
<TextBox Grid.Row="1" Grid.Column="1" x:Name="tbAge"/>
with a Grid
that has 1 row and 3 columns:
<Grid Grid.Row="1" Grid.Column="1" ColumnSpacing="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Content="-" />
<TextBox Grid.Row="0" Grid.Column="1" x:Name="tbAge" />
<Button Grid.Row="0" Grid.Column="2" Content="+" />
</Grid>
Nesting layout controls
We might ask why we didn’t add extra columns and rows in the outer Grid
(by properly using ColumnSpan
on the existing controls).
Instead, we followed the principle of unit encapsulation: the newly introduced controls are fundamentally related elements, so we achieved a more transparent solution by placing them in a separate Grid
control. Expanding the outer Grid
would be justified if we wanted to save on creating controls, for performance reasons. In our case, this is not necessary.
We are now done with designing the layout of our simple form.
Data binding¶
Binding¶
In the next step, we will solve the problem of entering and modifying a person's data on the form we just created. For this, a Person
class is already prepared in the project's Models
folder, let's take a look at it.
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
We want to bind the two properties here to the TextBox
controls, for which we will use data binding.
In the code-behind file of our window, let's introduce a property that references a Person
object and set its initial value in the constructor:
public Person NewPerson { get; set; }
public MainWindow()
{
InitializeComponent();
NewPerson = new Person()
{
Name = "Eric Cartman",
Age = 8
};
}
In the next step, bind (using data binding) the following properties of the NewPerson
object:
- Bind the
Name
property to theText
property of thetbName
TextBox
- Bind the
Age
property to theText
property of thetbAge
TextBox
Text="{x:Bind NewPerson.Name}"
Text="{x:Bind NewPerson.Age}"
(Add the above property settings in the rows of tbName
and tbAge
TextBox
controls.)
Important
The essence of data binding is that we do not manually set the properties (in this case, the text) of controls appearing on the UI from the code-behind file. Instead, we link the properties using the platform's data binding mechanism. This way, when one property changes, the other changes automatically as well!
The Text="{x:Bind}"
syntax is a so-called markup extension: it has a special meaning for the XAML processor. This is why we use XAML instead of regular XML.
It is even possible to create our own Markup Extensions, but that is beyond the scope of this lesson.
Let's run it! As a result of data binding, the Text
properties of the two TextBox
controls automatically contain the name and age values given in the Name
and Age
properties of the NewPerson
object (used as a data source).
Change notification¶
Let's implement the Click
event handlers for the ± buttons.
<Button Grid.Row="1" Grid.Column="0" Content="-" Click="DecreaseButton_Click"/>
<!-- ... -->
<Button Grid.Row="1" Grid.Column="2" Content="+" Click="IncreaseButton_Click"/>
private void DecreaseButton_Click(object sender, RoutedEventArgs e)
{
NewPerson.Age--;
}
private void IncreaseButton_Click(object sender, RoutedEventArgs e)
{
NewPerson.Age++;
}
Due to the data binding introduced earlier, we would expect that if we change the Age
property of the NewPerson
data source in the above event handlers, the tbAge
TextBox
control in the UI should reflect this change. Let's try it! This doesn't work yet because the INotifyPropertyChanged
interface needs to be implemented as well.
-
Implement the
INotifyPropertyChanged
interface in thePerson
class. When binding to this class, the system will subscribe to thePropertyChanged
event. By triggering this event, we can notify the binding mechanism that a property has changed.public class Person : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private string name; public string Name { get { return name; } set { if (name != value) { name = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); } } } private int age; public int Age { get { return age; } set { if (age != value) { age = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Age))); } } } }
Is the code lengthy?
In the future, we could move this logic to a base class, but that would introduce the MVVM pattern, which belongs to a later topic. So don't be alarmed by this slightly messy code.
-
In the data binding, enable change notification by modifying the
Mode
toOneWay
, since the default mode forx:Bind
isOneTime
, which means a one-time data binding.Text="{x:Bind NewPerson.Age, Mode=OneWay}"
Let's try it! The event handlers modify the data source (NewPerson
), and now, as a result, the UI changes as well, thanks to the properly set up data binding.
Two-way data binding¶
As with Age
, set the data binding for the Name
property to one-way as well:
Text="{x:Bind NewPerson.Name, Mode=OneWay}"
Start the application, then set a breakpoint in the setter of the Name
property in the Person
class (if (name != value)
line), and let's check if the data binding works in the reverse direction: if we change the value in the TextBox
field, does the NewPerson
object's Name
property change as well? Type something into the Name
field, then click on another field: at this point, the content of the TextBox
should be "finalized", and it should be written back into the data source. However, this is not happening, and the code doesn't hit our breakpoint.
This happens because earlier we used OneWay
data binding, which only means data binding from the data source to the UI. If we want the data binding to work in the reverse direction as well (from control to data source), we need to set the binding mode to TwoWay
. This is called two-way data binding.
Text="{x:Bind Name, Mode=TwoWay}"
Text="{x:Bind Age, Mode=TwoWay}"
Try it out! Now the data binding works in both directions:
- If the source property (e.g.,
NewPerson.Name
) changes, the bound control property (e.g.,TextBox.Text
) stays in sync with it. - If the target (control) property changes (e.g.,
TextBox.Text
), the source property (e.g.,NewPerson.Name
) stays in sync with it.
Lists¶
Next, we will practice the use of list data binding.
Let's add a list of Person
objects to the code-behind file of the view, and assign it an initial value in the constructor.
public List<Person> People { get; set; }
public MainWindow()
{
InitializeComponent();
NewPerson = new Person()
{
Name = "Eric Cartman",
Age = 8
};
People = new List<Person>()
{
new Person() { Name = "Peter Griffin", Age = 40 },
new Person() { Name = "Homer Simpson", Age = 42 },
};
}
Let's set the ItemsSource
property of the ListView
control via data binding to specify the data source it should work with.
<ListView Grid.Row="3" Grid.ColumnSpan="2" ItemsSource="{x:Bind People}"/>
Let's try it out!
We see that two items have appeared in the list. Of course, it's not displaying what we want, but this can be easily fixed.
By default, the ListView
calls ToString()
on the list items, which, if not overridden, shows the FullName
property (i.e., the type name) of the class type .
Let's set the ItemTemplate
property of our ListView
(using the familiar property element syntax), which defines the appearance of the list item through a template. In our case, this will be a single-cell Grid
, where the TextBlock
s display the properties of Person
, with the name left-aligned and the age right-aligned.
<ListView Grid.Row="3" Grid.ColumnSpan="2" ItemsSource="{x:Bind People}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="model:Person">
<Grid>
<TextBlock Text="{x:Bind Name}" />
<TextBlock Text="{x:Bind Age}" HorizontalAlignment="Right" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
A DataTemplate
is a layout template that the ListView
will apply to every item for display when we set it in the ItemTemplate
property.
Since x:Bind
is a compile-time data binding, we also need to specify the data type in the data template using the x:DataType
attribute. In the above example, we specified model:Person
, meaning we want the model
prefix to be mapped to the HelloXaml.Models
namespace in our code (since it contains the Person
class). To do this, we must include the following namespace declaration in the attributes of the Window
tag at the beginning of our XAML file:
xmlns:model="using:HelloXaml.Models"
(after this, the model
prefix will be usable). We can do this manually or with Visual Studio's help: just click on the underlined (marked as erroneous) model:Person
text, then click on the lightbulb that appears at the beginning of the line (or press Ctrl
+ .
), and select the "Add xmlns using:HelloXaml.Models" option that appears.
Let's try it out! Now the items are correctly displayed in the list.
When the Add button is clicked, let's add a new Person
object to the list with the data from the form, then clear the form data in our NewPerson
object.
To do this, we will introduce a Click
event handler for the Add button:
<Button ... Click="AddButton_Click">
private void AddButton_Click(object sender, RoutedEventArgs e)
{
People.Add(new Person()
{
Name = NewPerson.Name,
Age = NewPerson.Age,
});
NewPerson.Name = string.Empty;
NewPerson.Age = 0;
}
The new item is not appearing in the list because the ListView
is not notified that a new item has been added to the list. This can be easily fixed: we can replace the List<Person>
with an ObservableCollection<Person>
:
public ObservableCollection<Person> People { get; set; }
ObservableCollection<T>
It is important to note that here the value of the People
property hasn't changed, but rather the content of the List<Person>
object has been altered, which is why INotifyPropertyChanged
is not the solution, but rather the INotifyCollectionChanged
interface, which is implemented by ObservableCollection
.
So, we are now familiar with and using two interfaces that support change notification using data binding: INotifyPropertyChanged
and INotifyCollectionChanged
.
Outlook: Classic Binding¶
The classic form of data binding is represented by the Binding
markup extension.
The main differences compared to x:Bind
are:
- The default mode of
Binding
isOneWay
, notOneTime
: It automatically observes changes, whereas withx:Bind
this needs to be explicitly set. Binding
by default works with theDataContext
, but you can change the data binding source. Whereasx:Bind
binds to the class of the view (xaml.cs) by default.Binding
works at runtime using reflection, so on one hand, it doesn't give us compile-time errors if something is misspelled, but on the other hand, a large number of bindings (order of 1000) can significantly slow down our application.x:Bind
is compile-time, so the compiler checks whether the specified properties exist. In data templates, we need to declare which data the template will work with by using thex:DataType
attribute.- In
x:Bind
, it's possible to bind methods, whereasBinding
can only use converters. When binding functions, the change notification works with changes in the parameters as well.
Recommendation
As a rule of thumb, it is recommended to prefer using x:Bind
because it is faster, and it provides compile-time errors. However, if you encounter issues with x:Bind
, it’s worth switching to Binding
.