CHAPTER 3
Building the User Interface with XAML
Avalonia UI is, at its core, a library that allows you to create native user interfaces from a single C# codebase by sharing code. This chapter provides the foundations for building the user interface in an Avalonia UI solution. Then, in the next three chapters, you will learn in more detail about layouts, controls, windows, pages, and navigation.
The structure of the user interface in Avalonia UI
Whether you build applications for the desktop or cross-platform solutions, in Avalonia UI you define the user interface via the eXtensible Application Markup Language (XAML). As its name implies, XAML is a markup language that you can use to write the user interface definition in a declarative fashion.
XAML was first introduced more than 15 years ago with Windows Presentation Foundation, and it has always been available in platforms such as Silverlight, Windows Phone, UWP, Xamarin, and .NET MAUI.
XAML derives from XML and offers the following benefits, among others:
· XAML makes it easy to represent structures of elements in a hierarchical way, where pages, layouts, and controls are represented with XML elements and properties with XML attributes.
· It provides clean separation between the user interface definition and the C# logic.
· Being a declarative language separated from the logic, it allows professional designers to work on the user interface without interfering with the imperative code.
The way you define the user interface with XAML is unified across platforms, meaning that you design the user interface once, and it will run on iOS, Android, and Windows.
Note: XAML in Avalonia UI adheres to Microsoft’s XAML 2009 specifications, and the differences with WPF are few. Remember that XAML is case-sensitive for object names and their properties and members.
For example, when you create an Avalonia UI desktop solution, you can find a file in the project called MainPage.axaml, whose markup is represented in Code Listing 1.
Code Listing 1
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="AvaloniaApplication1.MainWindow" Title="AvaloniaApplication1"> Welcome to Avalonia! </Window> |
XAML files in Avalonia UI normally contain the definition for a window or a custom view. The root element is a Window object, which represents its C# class counterpart and is rendered as an individual window for desktop apps and as a page in mobile apps.
The content of a window (or panel) goes inside the Window.Content property. However, in XAML the Content property is implicit, meaning you do not need to write a Window.Content element. The compiler assumes that the visual elements you enclose between the Window tags are assigned to the Window.Content property. In the case of an empty project, the content for the window is the Welcome to Avalonia! text.
XAML allows for better organization and visual representation of the structure of the user interface. If you look at the root element, you can see a number of attributes whose definition starts with xmlns. These are referred to as XML namespaces and are important because they make it possible to declare visual elements defined inside specific namespaces or XML schemas.
For example, xmlns points to the root XAML namespace defined inside a specific XML schema and allows for adding to the UI definition all the visual elements defined by Avalonia UI; xmlns:x points to an XML schema that exposes built-in types; and xmlns:d points to a namespace that simplifies the design-time definition of the visual elements.
As you will learn in Chapter 4, visual elements are arranged inside panels (also referred to as layouts). Each window or panel can only contain one visual element. In the case of the autogenerated MainPage.axaml page, you cannot add other visual elements to the page unless you organize them into a panel. Let’s provide an example with code that declares some text and a button. This requires you to have a Button below a TextBlock, and both must be wrapped inside a container such as the StackPanel, as demonstrated in Code Listing 2.
Tip: IntelliSense will help you add visual elements faster by showing element names and properties as you type. You can then simply press Tab or double-click to quickly insert an element.
Code Listing 2
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="AvaloniaApplication1.MainWindow" Title="AvaloniaApplication1"> <StackPanel Orientation="Vertical" Margin="10"> <TextBlock Text="Welcome to Avalonia UI!" /> <Button Name="Button1" Content="Click here!" Margin="0,10,0,0"/> </StackPanel> </Window> |
If you did not include both controls inside the panel, Visual Studio will raise an error. You can nest other layouts inside a parent layout and create complex hierarchies of visual elements. Notice the Name assignment for the Button. Generally speaking, with Name, you can assign an identifier to any visual element so that you can interact with it in C# code, for example, if you need to retrieve a property value.
If you have never seen XAML before, you might wonder how you can interact with visual elements in C# at this point. In the Solution Explorer, if you expand the MainWindow.axaml file, you will see a nested file called MainWindow.axaml.cs. This is the so-called code-behind file, and it contains all the imperative code for the current page. In this case, the simplest form of a code-behind file, the code contains the definition of the MainWindow class, which inherits from Window, and the page constructor, which makes an invocation to the InitializeComponent method of the base class and initializes the window. You will access the code-behind file often from Solution Explorer, especially when you need to respond to events raised by the user interface.
Limitations of the XAML editor and productivity tools
At the moment, the XAML editor for Avalonia UI and the related productivity tools do not offer the same features and power that you can expect in other platforms like WPF or .NET MAUI. For example, tools like quick actions, Go to Definition, and Peek Definition are not available in Avalonia UI. Also, when you type an event name, IntelliSense will not give you the possibility to quickly generate a new handler by pressing Tab, as you would do in any other XAML project type.
With regard to events, you will first need to manually generate an event handler in the code-behind file, and then you will be able to assign the handler to the event declaration in XAML; otherwise, the Avalonia editor will not be able to recognize the event handler. It is reasonable to expect improvements in the near future, but this is how it works today.
Responding to events
Events are fundamental for the interaction between the user and the application, and controls in Avalonia UI raise events, as normally happens in any platform. Events are handled in the C# code-behind file. For instance, suppose you want to perform an action when the user taps the button defined in the previous code. The Button control exposes an event called Click that you assign the name of an event handler, but you first need to declare an event handler as follows:
private void Button1_Click(object sender, RoutedEventArgs eventArgs)
{
// Do actions here...
}
Then, assign the handler to the event in XAML, like in the following example:
<Button Name="Button1" Text="Click here!" Margin="0,10,0,0"
Click="Button1_Click"/>
Tip: As mentioned in the previous paragraph, the way you declare and assign events in Avalonia UI has some limitations if compared to other platforms.
About visual elements, their event handlers’ signatures require two parameters: one of type object representing the control that raised the event, and one object of type RoutedEventArgs containing information about the event. In many cases, event handlers work with derived versions of RoutedEventArgs, but these will be highlighted when appropriate.
Understanding routed events
Avalonia UI implements routed events, exactly like WPF. To understand routed events, you need to understand that Avalonia UI handles events differently due to its hierarchical structure. When an event is triggered, it passes through the entire visual tree, and each element in the visual tree re-raises the event until it reaches its destination. The Avalonia UI runtime will figure out which element triggered the event, and which element is the recipient of the event itself. For a better understanding, consider the following code snippet:
<Button Text="Click here!" Margin="0,10,0,0" Click="Button_Click"/>
The Button has no name, but it has an event handler assigned, which looks like the following:
private void Button_Click(object sender, RoutedEventArgs eventArgs)
{
// Do actions here...
}
Though the button has no name, the routed event handler is still able to understand if a click event has been raised from that button. And even if you had the following situation:
<Button Text="Click here!" Margin="0,10,0,0" Click="Button_Click"/>
<Button Text="Do not click here!" Margin="0,10,0,0" Click="Button_Click"/>
The runtime could understand which control raised the event without specifying a name. One of the benefits of this approach is that you can have one common event handler for multiple views. You would need code like the following to cast the sender to a typed object and determine which control has actually raised the event:
private void Button_Click(object sender, RoutedEventArgs eventArgs)
{
Button senderObject = sender as Button;
if(senderObject.Content == "Click here!")
{
}
else
{
}
}
Understanding the routing strategies
Routed events can go through three different directions across the tree of visual elements. Such directions are referred to as routing strategies and are represented by values from the RoutingStrategy enumeration. Not only are they important for your general knowledge of the platform, but also because you can provide a strategy when creating custom routed events. Routing strategies are defined as follows:
· Tunnel: Events are raised from the visual element and pass through the entire visual tree until they arrive at the event recipient. This is the most common strategy.
· Bubble: Events start from the event recipient and go back through the entire visual tree until they arrive at the object that raised the action.
· Direct: Events are directly raised against the recipient. You can compare this strategy to the way events are raised in Windows Forms, and in general, platforms that are not based on XAML.
The previous code snippet, related to the Click event handler for a button, is based on the Tunnel strategy: the user clicks on the button and the event passes through the visual tree (the Window, the container, and finally the button).
Tunneling is certainly the most common strategy that you will see across the book, but don’t forget to bookmark the documentation page about routed events—it is a very important topic.
Understanding type converters
If you look at Code Listing 2, you will see that the Orientation property of the StackPanel is assigned the Vertical value, and it is of type Orientation, whereas the Margin property assigned to the Button is of type Thickness. However, these properties are assigned values passed in the form of strings in XAML.
Avalonia UI (and all the other XAML-based platforms) implements the so-called type converters, which automatically convert a string into the appropriate value for a number of known types. Another common example of a type converter is about assigning an individual Color to control properties such as Foreground or Background, which are used instead of type IBrush. In this case, the built-in type converter converts the color into a SolidColorBrush (brushes will be discussed in Chapter 9, “Brushes, Graphics, and Animations”).
Summarizing all the available type converters and known target types is neither possible nor necessary at this point; you simply need to remember that, in most cases, strings you assign as property values are automatically converted into the appropriate type on your behalf.
Coding the user interface in C#
In Avalonia UI, you can also create the user interface of an application in C# code. For instance, Code Listing 3 demonstrates how to create a page with a layout that arranges controls in a stack containing a label and a button. For now, do not focus on element names and their properties (they will be explained in the next chapter). Rather, focus on the hierarchy of visual elements that the code introduces.
var newWindow = new Window(); newWindow.Title = "New window";
var newLayout = new StackPanel(); newLayout.Orientation = Avalonia.Layout.Orientation.Vertical; var newTextBlock = new TextBlock(); newTextBlock.Text = "Welcome to Avalonia UI!";
var newButton = new Button(); newButton.Content = "Click here!"; newButton.Margin = new Thickness(0, 10, 0, 0);
newLayout.Children.Add(newTextBlock); newLayout.Children.Add(newButton);
newWindow.Content = newLayout; |
Here you have full IntelliSense support. However, as you can imagine, creating a complex user interface entirely in C# can be challenging for at least the following reasons:
· Representing a visual hierarchy made of tons of elements in C# code is extremely difficult.
· You must write the code in a way that allows you to distinguish between user interface definition and other imperative code.
· As a consequence, your C# becomes much more complex and difficult to maintain.
This should clarify why using XAML to declare the user interface is the most convenient way and that coding visual elements in C# should only be done when you need to generate new visual elements at runtime.
Chapter summary
This chapter provided a high-level overview of how you define the user interface with XAML, based on a hierarchy of visual elements. You have seen how to add visual elements and how to assign their properties; you have seen how type converters allow for passing string values in XAML and how the compiler converts them into the appropriate types. You also learned the important concept of routed events and how they can be used to create common event handlers.
After this overview of how the user interface is defined in Avalonia UI, it is time to discuss important concepts in more detail, and we will start by organizing the user interface with panels.
- 1800+ high-performance UI components.
- Includes popular controls such as Grid, Chart, Scheduler, and more.
- 24x5 unlimited support by developers.