XAML is based on Extensible Markup Language (XML). This would seem like a great thing as XML is a flexible markup language familiar to most developers. It is indeed flexible and powerful, but it has some drawbacks.
The primary problem with Microsoft's implementations of XAML is that there have been so many variations of the XAML language created for different development platforms over the years. Currently, UWP, Windows Presentation Foundation (WPF), and Xamarin.Forms applications all use XAML as their UI markup language. However, each of these uses a different XAML implementation or schema, and the markup cannot be shared across the platforms. In the past, Windows 8, Silverlight, and Windows Phone apps also had other different XAML schemas.
If you have never worked with XAML before, you're probably ready to see an example of some UI markup. The following XAML is a fragment that defines a Grid
containing several other of the basic WinUI controls (you can download the code for this chapter from GitHub here: https://github.com/PacktPublishing/-Learn-WinUI-3.0/tree/master/Chapter01):
<Grid Width="400" Height="250" Padding="2"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Grid.RowDefinitions>
<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:"
Margin="0,0,2,0"
VerticalAlignment="Center"/>
<TextBox Grid.Row="0" Grid.Column="1"
Text=""/>
<Button Grid.Row="1" Grid.Column="1" Margin="0,4,0,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Content="Submit"/>
</Grid>
Let's break down the XAML here. The top level of a UWP window is Page
. UWP app navigation is page-based, and this top-level navigation happens within a root Frame
container in the App.xaml
file in the project. You will learn more about page navigation in Chapter 4, Advanced MVVM Concepts. A Page
must contain only one child, usually some type of layout panel such as a Grid
or StackPanel
. By default, a Grid
is inserted as that child. We will discuss other types of panels that serve as a good parent container in the next chapter. I made a few modifications to the Grid
.
Height
and Width
properties provide a static size for the example, and HorizontalAlignment
and VerticalAlignment
properties will center the Grid
on the Page
. Fixed sizes are uncommon at this level of the XAML and limit the flexibility of the layout, but they illustrate some of the available attributes.
A Grid
is a layout panel that allows developers to define rows and columns to arrange its elements. The rows and columns can have their sizes defined as fixed, relative to each other, or auto-sized based on their contents. For more information, you can read the Microsoft Docs article Responsive layouts with XAML: https://docs.microsoft.com/en-us/windows/uwp/design/layout/layouts-with-xaml.
The Grid.RowDefinitions
block defines the number and behavior of the grid's rows. Our grid will have two rows. The first one has Height="Auto"
, which means it will resize itself to fit its contents, provided enough space is available. The second row has Height="*"
, which means the rest of the grid's vertical space will be allocated to this row. If multiple rows have their height defined like this, they will evenly split the available space. We will discuss additional sizing options in the next chapter.
The Grid.ColumnDefinitions
block does for the grid's columns what RowDefinitions
did for the rows. Our grid has two columns defined. The first ColumnDefinition
has its Height
set to Auto
, and the second has Height="*"
.
TextBlock
defines a label in the first Grid.Row
and Grid.Column
. When working with XAML, all indexes are 0-based. In this case, the first Row
and Column
are both at position 0
. The Text
property conveniently defines the text to display, and the VerticalAlignment
in this case will vertically center the text for us. The default VerticalAlignment
for a TextBlock
is Top
. The Margin
property adds some padding around the outside of the control. A margin with the same amount of padding on all sides can be set as a single numeric value. In our case, we only want to add a couple of pixels to the right side of the control to separate it from TextBox
. The format for entering these numeric values is "<LEFT>,<TOP>,<RIGHT>,<BOTTOM>"
, or "0,0,2,0"
here.
The TextBox
is a text entry field defined in the second column of the grid's first row.
Finally, we've added a Button
control to the second column of the grid's second row. A few pixels of top margin are added to separate it from the controls above. The VerticalAlignment
is set to Top
(the default is Center
) and HorizontalAlignment
is set to Right
(the default is Center
). To set the text of the Button
, you don't use the Text
property like we did with the TextBlock
, as you might think. In fact, there is no Text
property. The Content
property of the Button
is used here. Content
is a special property that we will discuss in more detail in the next chapter. For now, just know that a Content
property can contain any other control: text, an Image
, or even a Grid
control containing multiple other children. The possibilities are virtually endless.
Here is the UI that gets rendered by the preceding markup:
Figure 1.3 – WinUI XAML rendered
This is a very simple example to give you a first taste of what can be created with XAML. As we move ahead, you will learn how powerful the language can be.
Creating an adaptive UI for any device
In the previous example, the Grid
had fixed Height
and Width
properties. I mentioned that setting fixed sizes can limit a UI's flexibility. Let's remove the fixed size properties and use the alignment properties to guide the UI elements, to render how we want them to at different sizes and aspect ratios, as follows:
<Grid VerticalAlignment="Top" HorizontalAlignment="Stretch" Padding="2">
The rest of the markup remains unchanged. The result is a TextBox
that resizes to fit the width of the window, and the Button
remains anchored to the right of the window as it resizes. See the window resized a couple of different ways here:
Figure 1.4 – Resized windows
If you were using this app on a tablet PC, the contents would resize themselves to fit in the available space. That is the power of XAML's adaptive nature. When building a UI, you will usually want to choose relative and adaptive properties such as alignment to fixed sizes and positions.
It's this adaptive layout that makes XAML work so well on mobile devices with Xamarin, and this is why WPF developers have loved using it since its launch with Windows Vista.
Powerful data binding
Another reason why UWP and other XAML-based frameworks are so popular is the ease and power of their data-binding capabilities. Nearly all properties on UWP controls can be data-bound. The source of the data can be an object or a list of objects on the data source. In most cases, that source will be a ViewModel
class. Let's have a very quick look at using UWP's Binding
syntax for data binding to a property on a ViewModel
class, as follows:
- First, we will create a simple
MainViewModel
class with a Name
property, like this:public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler
PropertyChanged;
private string _name;
public MainViewModel()
{
_name = "Bob Jones";
}
public string Name
{
get
{
return _name;
}
set
{
if (_name == value) return;
_name = value;
PropertyChanged?.Invoke(this, new
PropertyChangedEventArgs(nameof(Name)));
}
}
}
The MainViewModel
class implements an interface called INotifyPropertyChanged
. This interface is key to the UI receiving updates when data-bound properties have changed. This interface implementation is typically wrapped either by a Model-View-ViewModel (MVVM) framework, such as Prism or MvvmCross, or with your own ViewModelBase
class. For now, we will directly invoke a PropertyChanged
event inside the Name
property's setter. We will learn more about ViewModels
and the INotifyPropertyChanged
interface in Chapter 3, MVVM for Maintainability and Testability.
- The next step is to create an instance of the
MainViewModel
class and set it as the ViewModel
for our MainPage
. This happens in the code-behind file for the page, MainPage.xaml.cs
, as illustrated in the following code snippet:public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
this.ViewModel = new MainViewModel();
}
public MainViewModel ViewModel { get; set; }
}
We have added a ViewModel
property to the MainPage
and set it to a new instance of our MainViewModel
class in the constructor.
Tip
Any code added to a page's constructor should be added after the call to InitializeComponent()
.
- Now, it's time to add the data-binding code to the XAML markup for the
TextBox
, as follows:<TextBox Grid.Row="0" Grid.Column="1" Text="{x:Bind
Path=ViewModel.Name, Mode=TwoWay}"/>
Some markup has been added to set the Text
property using the x.Bind
markup extension. The data-binding Path
is set to the Name
property on the ViewModel
, which has been assigned in the code-behind file in the preceding Step 2. By setting the data-binding mode to TwoWay
, updates in the ViewModel
will display in the UI, and any updates by the user in the UI will also be persisted in the Name
property of the MainViewModel
class. Now, running the app will automatically populate the name that was set in the constructor of the ViewModel
, as illustrated in the following screenshot:
Figure 1.5 – Data binding the TextBox
- To illustrate data binding to another property on another UI element on the page, we will first modify the grid to add a name, as follows:
<Grid x:Name="ParentGrid"
VerticalAlignment="Top"
HorizontalAlignment="Stretch"
Padding="2">
- Now add another
RowDefinition
to the Grid
to fit the new UI element in the page:<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
- Next, add a
TextBlock
element and use the Binding
markup extension to bind its Text
property to the ActualWidth
of the ElementName
set to ParentGrid
. We are also adding a TextBlock
to label this as the Actual Width:<TextBlock Grid.Row="1" Grid.Column="0"
Text="Actual Width:"
Margin="0,0,2,0"
VerticalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="1"
Text="{Binding ElementName=ParentGrid,
Path=ActualWidth}"/>
- Next, update the Submit
Button
to appear in Grid.Row
2.
- Now the new
TextBlock
control displays the width of the ParentGrid
when the page is loaded. Note that it will not update the value if you resize the window. The ActualWidth
property does not raise a property change notification. This is documented in the FrameworkElement.ActualWidth
docs: https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.frameworkelement.actualwidth:
Figure 1.6 – Data binding to another element
The Submit button does not function yet. You will learn how to work with Events and Commands with MVVM in Chapter 5, Exploring WinUI Controls and Libraries.
Styling your UI with XAML
When working with XAML, styles can be defined and applied at almost any scope, global to the application in App.xaml
, in the current Page
inside a Page.Resources
declaration, or inside any level or nested control on the page. The Style
property specifies a TargetType
property, which is the data type of the elements to be targeted by the style. It can optionally have a Key
property defined as a unique identifier, similar to a class identifier in Cascading Style Sheets (CSS). That Key
property can be used to apply the style to only selected elements of that type. Only one Key
property can be assigned to an element, unlike with CSS classes.
In the next example, we will modify the page to define a Style
property for all buttons on the page, as follows:
- Start by moving the Submit button to be nested inside a
StackPanel
element. A StackPanel
element stacks all child elements in a horizontal or vertical orientation, with vertical being the default orientation. Some of the button's properties will need to be moved to the StackPanel
element, as it is now the direct child of the Grid
. After adding a second button to the StackPanel
element to act as a Cancel button, the code for the StackPanel
and Button
elements should look like this:<StackPanel Grid.Row="1" Grid.Column="1"
Margin="0,4,0,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Orientation="Horizontal">
<Button Content="Submit" Margin="0,0,4,0"/>
<Button Content="Cancel"/>
</StackPanel>
A new margin has been added to the first button to add some space between the elements.
- Next, we will add a
Style
block to the Page.Resources
section to style the buttons. Because no Key
is assigned to the Style
block, it will apply to all Button
elements that do not have their styles overridden in an inner scope. This is known as an implicit style. The code for this is shown here:<Page.Resources>
<Style TargetType="Button">
<Setter Property="BorderThickness"
Value="2" />
<Setter Property="Foreground"
Value="LightGray" />
<Setter Property="BorderBrush"
Value="GhostWhite"/>
<Setter Property="Background"
Value="DarkBlue" />
</Style>
</Page.Resources>
- Now, when you run the app, you will see that the new style has been applied to both the Submit and Cancel buttons without adding any styling directly to each control, as illustrated in the following screenshot:
Figure 1.7 – Styled buttons
If we moved the Style
block to the Application.Resources
section, the defined style would get applied to every button in the entire app unless the developer had individually overridden some of the properties in the style. For instance, if the Submit button had a Background
property set to DarkGreen
, only the Cancel button would appear as dark blue.
We will spend more time on styles and design in Chapter 7, Windows Fluent UI Design.
Separating presentation from business logic
We looked briefly at the MVVM pattern in the earlier section on data binding. MVVM is key to the separation of presentation logic from business logic in UWP application development. The XAML elements only need to know that there is a property with a particular name somewhere in its data context. The ViewModel
classes have no knowledge of the View
(our XAML file).
This separation provides several benefits. First, ViewModels
can be unit tested independently of the UI. If any UWP elements are referenced by the system under test, the UI thread is needed. This will cause tests to fail when they're running on background threads locally or on a Continuous Integration (CI) server. See Chapter 3, MVVM for Maintainability and Testability for more information on unit testing WinUI applications.
The next benefit of View
/ViewModel
separation is that businesses with dedicated user experience (UX) experts will sometimes work on designing the XAML markup for an app while other developers are building the ViewModels
. When it is time to sync up the two, the developer can add in the necessary data-binding properties to the XAML, or perhaps the UX designer and developer have already agreed upon the names of the properties in the shared data context. Visual Studio includes another tool geared toward designers in this workflow, called Blend for Visual Studio. Blend was first released by Microsoft in 2006 as Microsoft Expression Blend, as a tool for designers to create UIs for WPF. Support was later added for other XAML languages such as Silverlight and UWP. Blend is still included with the UWP development workload when installing Visual Studio.
A final benefit we will discuss here is that a good separation of concerns between any layers of your application will always lead to better maintainability. If there are multiple components involved in a single responsibility or if logic is duplicated in multiple places, this leads to buggy code and unreliable applications. Follow good design patterns, and you will save yourself a lot of work down the road.
Now that you have a good understanding of the history of UWP applications, it's time to look at WinUI: what it is, and why it was created.