系列教程 · 2023年12月8日

Basics of XAML in Easy Samples for Multiplatform Avalonia .NET Framework

In this article, I present a simple tutorial for Avalonia XAML providing easy to understand samples. It covers important topics concerning using XAML for representing classes and properties, XAML namespace, simple markup extensions, XAML resource and using XAML for non-visual projects.

Introduction

This article is the third instalment of Avalonia articles following Multiplatform UI Coding with AvaloniaUI in Easy Samples. Part 1 – AvaloniaUI Building Blocks and Multiplatform Avalonia .NET Framework Programming Basic Concepts in Easy Samples.

The article is a guide to basic XAML functionality. You do not need to read the previous two articles in order to understand it aside from instructions on setting up your Visual Studio and creating an Avalonia project.

It is assumed that the readers of this article have some basic familiarity with XML and C#.

Avalonia is a great new open source package which closely resembles WPF but, unlike WPF or UWP, works on most platforms – Windows, MacOS and various flavors of Linux and is in many respects, is more powerful than WPF.

Avalonia is also considerably more powerful and flexible for building multiplatform desktop applications than Node.js or Xamarin.

The source code for Avalonia is available at Avalonia Source Code.

The material in this article covers the basics of Avalonia XAML like namespaces, types, properties, resources, generics and basic markup extensions for beginners. Here, we will not go into covering how the attached properties or bindings are represented in XAML (this has already been covered in Multiplatform Avalonia .NET Framework Programming Basic Concepts in Easy Samples) or into Templates and Styles (this will be covered in future articles).

Even though all the samples here are dealing with Avalonia, much of it applies also to other XAML frameworks like WPF, UWP and Xamarin.

Note, that I recently found out (from the following excellent demo TypeArgumentsDemo) that Avalonia extension for Visual Studio 2019 supports generics for compiled XAML – something that WPF team promised to add long ago, but to the best of my knowledge, never did. I devote a section in this article to Generics in Avalonia XAML.

I also used much of the material and samples for this article for the new Avalonia documentation to be available soon at Avalonia Documentation.

All the code for this article is located under NP.Avalonia.Demos/NP.Demos.XamlSamples within Github NP.Avalonia.Demos repository.

What is XAML

XAML is XML used for building C# (mostly visual) objects.

C# classes are displayed as XML tags, while class properties are usually displayed as XML attributes:

XAML

The XAML code in the example above creates an object of type MyClass from XML namespace my_namespace and sets its properties Prop1 to string “Hello World” and Prop2 to value 123. Note that the properties will be resolved to their C# type, e.g., if Prop2 is of int type, it will be resolved to 123 integer value, while if it is of string type, it will be resolved to “123” string. If the property or type mentioned in XAML file do not exist, the compilation of the project that contains that XAML will fail and often Visual Studio will detect an error even before the compilation and will underscore the missing type or property with a red broken line.

The namespace (in our sample, it is “my_namespace“) should usually be defined above or within the XML tag where it is used. It can point to a C# namespace or a set of C# namespaces (as will be explained below with an appropriate example).

XAML file can be associated with a C# file called “code-behind” to define the same class using “partial class” declarations. The C# code-behind usually contains definitions of methods that serve as event handlers for the events fired by elements defined in XAML file. This way of associating events fired by XAML elements and C# event handlers is the easiest and most straightforward and was already explained in the previous article in Creating and Running a Simple Avalonia Project using Visual Studio 2019. However, it is also the worst, since it breaks the important MVVM pattern (as will be shown in future articles) and should almost never be used.

XAML Namespace Sample

XAML namespace is a string usually defined at the top level element of the XAML file (even though it can be defined on any tag) and pointing to some C# namespace(s) within some .NET assembly or assemblies that the project containing the current XAML file is dependent on.

Take a look at the top two lines of MainWindow.xaml file of our introductory sample of the first article:

XAML

These two lines define two XAML namespaces for the whole file. One of these namespaces which does not require any prefix (it has an empty prefix) and the other has prefix “x“. Both namespaces refer to the types defined in Avalonia packages. You can define, many elements, (say a button) in Avalonia without any prefix (e.g., as <Button .../> ) because those elements are located in the default Avalonia namespace referred to by “https://github.com/avaloniaui” URL. The namespace that is referred to by prefix “x” contains various types that are used slightly less frequently. For example – many built-in C# types, e.g., string and object can be referred to in XAML as <x:String>...</x:String>and <x:Object>...</x:Object> (they are contained in the Avalonia namespace referred to by “http://schemas.microsoft.com/winfx/2006/xaml” url).

Important Note: The so called XAML namespace URLs do not have to refer to any valid URL that really exists on a web and the computer does not have to be online in order for them to work.

The example showing various ways to define custom XAML namespace is located under NP.Demos.XamlNamespacesSample.sln solution. Download its code from Github and open the solution in Visual Studio (or Rider).

You can see that the solution consists of three projects: the main project NP.Demos.XamlNamespacesSample and two projects on which the main project depends: Dependency1Proj and Dependency2Proj:

Image 1

Compile and run the solution – here is what you are going to see:

Image 2

There are five square buttons of different colors stacked vertically within a window.

Here is the relevant code of MainWindow.xaml file:

XAML

There are four custom namespaces defined by the following lines of the top XML tag:

XAML

Different buttons are referred to by the corresponding XAML namespace prefixes. Let us take a look at <RedButton/><RedButton/> class is defined as a C# class under Dependency1Proj project. Here is its code:

C#

The line…

C#

…(and the fact that RedButton class implements IStyleable interface) ensures that the default button Styles from the main theme will also be applied to the class RedButton which derives from Avalonia Button class. The constructor of the button assigns the button color to red and sets the button to have height and width of 30 generic pixels.

Note that the code for each of the buttons of the sample is exactly the same as that for RedButton aside from the button class name, C# namespace and the color assigned to the button.

Now take a look at the line that defines the XAML namespace prefix dep1 by which we refer to this button inside the XAML file:

XAML

The value of the namespace contains two parts divided by ‘;‘ semicolon. The first part refers to the C# namespace:

XAML

and the second part refers to assembly name:

XAML

In case of RedButton, both the namespace and assembly name have the same name: Dependency1Proj.

BlueButton is defined within the same project (Dependency1Proj), but within SubFolder folder. Its C# namespace is not Dependency1Proj (as for the RedButton) but Dependency1Proj.SubFolder.

Here is the line that defines the XAML namespace prefix dep1_sub_Folder by which BlueButton is referred to in MainWindow.xaml file:

XAML

The clr-namespace changed to be Dependency1Proj.SubFolder while the assembly part stated the same since BlueButton was defined in the same Dependency1Proj assembly.

Now take a look at <local:BrownButton\>. C# code for BrownButton is defined in the main project NP.Demost.XamlNamespacesSample – the same project that our MainWindow.xaml file is located in. Because of that, we can skip the assembly name when defining the prefix “local” (by which our BrownButton is referred to) and only specify the clr-namespace part:

XAML

GreenButton and GrayButton are defined in two different namespaces of Dependency2ProjGreenButton is defined under the project’s main namespace – Dependency2Proj while GrayButton is defined under Dependency2Proj.SubFolder namespace. However, Dependency2Proj also has file AssemblyInfo.cs which defines assembly metadata. In this file, we added a couple of lines at the bottom:

C#

These two lines combine the two namespaces of the assembly: Dependency2Proj and Dependency2Proj.SubFolder into the same URL: “https://avaloniademos.com/xaml”. As was mentioned above, it does not matter at all if that URL exists or of the computer is online. It is good if your URL would carry some meaning corresponding to the projects that contain this functionality.

Now the XAML prefix dep2 by which we refer to both GreenButton and GrayButton is defined by referring to that URL:

XAML

There is an important extra Avalonia feature in comparison to WPF. In WPF, one can refer by URL only to the functionality that is not located in the same project as the XAML file that wants to refer to it, while in Avalonia, there is no such restriction – e.g., if we have a XAML file in the same Dependency2Proj project, we still could put the line…

XAML

…at its top element and refer to the our GreenButton and GrayButton defined within the same project by dep2: prefix.

Accessing C# Composite Properties in XAML

We already mentioned above that C# built-in properties can be accessed as XML attributes of the corresponding element, e.g.:

XAML

Prop1 and Prop2 are simple C# properties defined on class MyClass which can be found in the C# namespace referred to by my_namespace prefix of that XAML file. Prop1 is likely of string type, while Prop2 can be either of any numeric type or a string (the XAML will automatically convert string “123” to the correct type).

What will happen, however, if the property itself is of some complex type that contains several properties of its own?

C# solution NP.Demos.AccessPropertiesInXamlSample.sln shows how to create such property in XAML.

There is a class Person defined within the project:

C#

We want to display it as the Window’s content. Here is what we have within MainWindow.xaml file:

XAML

Note the way we assign Content property of the Window to a composite Type:

XAML

We use Window.Content property tag with a period separating the name of the class from the name of the property.

Note that in the same way as we assign properties of composite types, we can also assign primitive type properties, e.g., we can set the Window’s Width by the following code:

XAML

instead of using XML attributes. Of course, such notations are much bulkier than XAML attribute notations are rarely used for properties of primitive types.

Note: Because Window.Content is a special property marked by ContentAttribte, we did not have to add <Window.Content> at all and could have placed <local:Person .../> object straight under <Window...> tag. There is only one property per class that can be marked with the ContentAttribute so in many cases, we are forced to use the <Class.Property notations anyway.

XAML Special Properties

There are several special properties marked by prefix “x:“, provided of course that we have “x” namespace prefix defined at the top of the file as:

XAML

The most important of them are x:Name and x:Key.

x:Name is used for elements within the XAML tree in order to be able to easily find an element in C# and also (by some people) in order provide some self documentation for XAML and in order to be able to easily identify the element within Avalonia Development Tool.

We already showed how to find an x:Name‘d element in C# code in Multiplatform UI Coding with AvaloniaUI in Easy Samples. “Creating and Running a Simple AvaloniaUI Project using Visual Studio 2019” section: one can use the FindControl(...) method, e.g., for a button defined in XAML and x:Named “CloseWindowButton“, we can use the following method in the code-behind to find it:

C#

x:Key is used for finding the Avalonia XAML resources and we are going to explain it in the section dedicated to them.

Very Brief Introduction to Markup Extensions

Markup up extensions are some C# classes that can significantly simplify XAML. They are used for setting some XAML properties using one liner notations that have curly brackets (‘{‘ and ‘}‘) in them. There are some very important built-in Avalonia markup extensions – the most important are the following:

  • StaticResource
  • DynamicResource
  • x:Static
  • Binding

We are going to provide examples for all of them aside from the Binding (which has already been explained in a Bindings.

One can also create custom markup extensions, but this is rarely used and will not be touched upon in this guide. We shall explain it in one of the future guides.

Avalonia XAML Resources

XAML resources is one of the most important methods of re-using XAML code and of placing some generic XAML code in generic Visual projects to be used in multiple applications.

StaticResource vs DynamicResource Sample

This sample shows the main difference between static and dynamic resources: static resource target value will not update when the resource itself is updated, while dynamic – will.

The sample is located under NP.Demos.StaticVsDynamicXamlResourcesSample solution.

Open the solution and run it, here is what you will see:

Image 3

Pressing “Change Status Color” button will result in the third rectangle switching its color to red:

Image 4

Here is the MainWindow.xaml file for the sample:

XAML

We define the XAML resource within the same MainWindow.xaml file as a resource of the window:

XAML

x:Key of the XAML resource can be used by StaticResource and DynamicResource to refer to the particular resource.

We then use StaticResource to set the background of two first borders and DynamicResource to the third border within a vertical border stack.

For the first border within the stack, we use StaticResource markup extension:

XAML

For the second border, we use StaticResource class, without markup extension (and you can see that the corresponding XAML is considerably more verbose):

XAML

Finally, the third border uses DynamicResource markup extension:

XAML

Button “StatusChangingBorder” is hooked within MainWindow.xaml.cs file to change the “StatusBrush” Resource from “Green” to “Red“:

C#

Even though the resource is the same for all three borders, only last border’s background changes – the one that uses DynamicResource.

Other important differences between the static and dynamic resource are the following:

  • DynamicResource can refer to a XAML resource defined in XAML below the DynamicResource expression, while StaticResource should refer to a resource above it.
  • StaticResource can be used to assign simple C# properties on various objects used in XAML, while the target of a DynamicResource statement should always be a special Avalonia Property on AvaloniaObject (special properties were explained in Attached Properties).
  • Since DynamicResource is more powerful (provides change notification) it takes considerably more memory resources than StaticResource. Because of that, when you do not need the change notification (the property stays the same for the duration of the program), you should always use StaticResourceDynamicResources are very useful when you want to dynamically change the themes or the colors of your application, e.g., allow the user to switch the theme or to change the colors depending on the time of the day.

Referring to XAML Resources Defined in Different XAML Files and Projects Sample

In this sample, we show how to refer to XAML resources located in a different file within the same or different project.

The sample is located under NP.Demos.XamlResourcesInMultipleProjects Visual Studio solution. After running the sample, you will see three rectangles of different colors – red, green and blue:

Image 5

The solution consists of two projects – the main project, NP.Demos.XamlResourcesInMultipleProjects and another project which the main project depends on – Dependency1Proj:

Image 6

RedBrush resource is defined within Themes/BrushResources.axaml file under Dependency1Proj:

XAML

Note that the BrushResources.axaml file has “Avalonia XAML” build action (as any Avalonia XAML resource file should):

Image 7

Such files are created by choosing “Resource Dictionary (Avalonia)” template for Visual Studio new item creation:

Image 8

GreenBrush Avalonia Resource is defined within Themes/LocalBrushResources.axaml file (this file is located in the main project):

XAML

Here is the content of MainWindow.axaml file:

XAML

We have three borders stacked vertically – first border’s background is getting its value from RedBrush resource, second border’s – from GreenBrush and third border from BlueBrush.

Take a look at the Resources section of the window at the top of the file:

XAML

<ResourceInclude .../> tags within <ResourceDictionary.MergedDictionary> tag means that we are merging the Resource Dictionaries defined externally to the current dictionary – the way we get all their key-value pairs. Those who know WPF can notice the difference – in WPF, we use <ResourceDictionary Source="..."/> tag and not <ResourceInclude Source="..."/>. Also note that for a dependent project, we do not separate the assembly from the rest of the URL and we are not using the cryptic “Component/” prefix for the URL. These are purely notational (not conceptual) differences, but still need to be remembered.

Note the Avalonia XAML urls for the merged files:

  • avares://Dependency1Proj/Themes/BrushResources.axaml” – the url of the Avalonia XAML Resource file defined in a different project should start with the magic work “avares://” followed by the assembly name, followed by the path to the file: “avares://<assembly-name>/<path-to-the-avalonia-resource_file>.
  • /Themes/LocalBrushResources.axaml” – the url of the Avalonia XAML Resource file defined in the same project in which it is used, should only consist of a forward slash followed by the path to avalonia resource file from the root of the current project.

At the end of the resource section, we define the BlueBrush resource – local to MainWindow.axaml file.

x:Static Markup Extension

x:Static markup extension allows to refer to static properties defined in the same project or in some dependent projects. The sample code is located under NP.Demos.XStaticMarkupExtensionSample solution. It contains two projects – the NP.Demos.XStaticMarkupExtensionSample (main project) and the dependency project Dependency1Proj. Main project contains class LocalProjectStaticBrushes while the dependency project contains DependencyProjectStaticBrushes.

Image 9

The contents of both C# files are very simple – each defines and sets value for a single static property. Here is the content of LocalProjectStaticBrushes class:

C#

Here is the content of DependencyProjectStaticBrushes class:

C#

Running the project will create a window with two rectangles, red and green:

Image 10

Here are the relevant parts of MainWindow.axaml file:

XAML

There are two important namespaces defined at the Window tag level:

XAML

dep1” corresponds to the dependency project and “local” corresponds to the project local to the MainWindow.xaml file (the main project).

Using these namespace prefixes and x:Static markup extension, we can set the Background properties on the two borders:

XAML

and:

XAML

Generics in Avalonia XAML

As I mentioned above, I learned that Visual Studio 2019 compiled Avalonia XAML supports generics from the following excellent demo TypeArgumentsDemo. As far as I know Microsoft never added such functionality to WPF, even though at one point they intended to do it.

Generics demo is located under XamlGenericsSamples project.

There is a ValuesContainer class with two generic type arguments:

C#

ValuesContainer defines two values Value1 of generic type TVal1 and Value2 of generic type TVal2.

The rest of the interesting code is all located within the MainWindow.axaml file.

Run the sample, and here is what you’ll see:

Image 11

There are three samples – first explains creating a single ValuesContainer object, second – a list of ValuesContainer objects and the third one – a Dictionary that maps integers into ValuesContainer objects. Let us explain the samples one by one.

Single ValuesContainer Object Sample

Image 12

Here is the code for this sample:

XAML

We define the ValuesContainer object to be the DataContext of the Grid that contains the code for the sample:

XAML

x:TypeArguments property of ValuesContainer object define a comma separated list of generic type arguments – we define the first argument as double and the second as string. Then we set Value1="300" and Value2="Hello 1". Note that the XML string “300” will be automatically converted to a double. Since DataContext is a special property that propagates down the visual tree, the same DataContext will be defined on all the descendants of the Grid. We can bind the descendant TextBlocks‘ Text properties to Value1 and Value2 to display those values. Also to prove that Value1 is indeed of double type (and not a string), we bind the width of the internal grid (with yellow background) to Value1 property of the DataContext:

XAML

So that the width of the yellow rectangle will be 300.

List of ValuesContainer Objects Sample

Image 13

Before with look into XAML code of the sample, note that we define a namespace collections at the top of the XAML file: xmlns:collections="clr-namespace:System.Collections.Generic;assembly=System.Collections". This namespace points to the C# namespace and assembly that defines generic collections such as List<...> and Dictionary<...>.

Here is the corresponding XAML code:

XAML

This time the DataContextof the container is defined as a List<ValuesContainer<int, string>>, ie we have two levels of type argument recursion:

XAML

Then, since List<...> has an Add method, we can simply add the individual objects within the List<...>:

XAML

Note that for each ValuesContainer object, the Value1 will be converted to an int automatically.

Then we bind the Items property of the ItemsControl to the to the list and use the ItemTemplate of the ItemsControl to display Value1 and Value2 of each individual item:

XAML

Dictionary of integer to ValuesContainer Objects Sample

Image 14

Our last sample is even more fun. We show how to create and display context of a generic Dictionary<string, ValuesContainer<int, string>>.

Here is the relevant code for the sample:

XAML

Here is how we define the Dictionary<string, ValuesContainer<int, string>>:   <collections:Dictionary x:TypeArguments="x:String, local:ValuesContainer(x:Int32, x:String)">.

Here is how the dictionary is populated:

XAML

Note that we simply create our ValuesContainer objects within the dictionary, but each of the objects has an x:Key property set to a unique value. This x:Key property specifies the key for the dictionary, while the ValuesContainer object becomes a value. Note that in our case, the dictionary’s key is of type string, but if it was of some other well know type, e.g. int, the Avalonia XAML compiler would have converted the key values to integers.

The XAML code above populates our dictionary with three KeyValuePair<string, ValuesContainer<int, string>> objects whose json representation is the following:

JSON

Here is how we bind our ItemsControl to those objects and display them using its ItemTemplate:

XAML

First TextBlock's Text property is bound to the Key of the KeyValuePair<...>, second to Value1 and third to Value2.

Referencing Assets in XAML

In Avalonia Lingo – Assets are usually binary image (e.g., png or jpg) files. In this section, we shall show how to refer to such files from Image controls within XAML.

The sample’s code is located under NP.Demos.ReferringToAssetsInXaml NP.Demos.ReferringToAssetsInXaml solution. Here is the solution’s code:

Image 15

We have Themes/avalonia-32.png file under the dependent project Dependency1Proj and Themes/LinuxIcon.jpg file under the main project.

Note that the Build Action for the asset files should be “AvaloniaResource” (unlike for XAML resource files where as we saw, it was set to “Avalonia XAML”):

Image 16

Build and run the sample, here is what you’ll see:

Image 17

There are four vertically stacked images – here is the corresponding code:

XAML

For the first two images – the Source is set in XAML, for the last – in C# code behind.

Note that the image defined as an asset local to the same project which contains our MainWindow.axaml file that uses it can use a simplified version of the source URL:

XAML

While the image located in a different project should be using a full version of the URL prepended with avares://.

XAML

Note, that just the same as in case of the XAML resource dictionary files and differently from WPF, the assembly name (“Dependency1Proj” – in our case) is a part of the URL and there is no Component prefix.

The Source property of the last two images is being set within MainWindow.axaml.cs code-behind file. Here is the relevant code:

C#

Note that even for the local file “LinuxIcon.jpg” (file defined in the same project as the MainWindow.xaml.cs file that uses it), we need to provide the full URL with “avares://<assembly-name>/” prefix.

Non-Visual XAML Code

The last sample will demonstrate that potentially one can use XAML even for a completely non-visual code. The sample is located under NP.Demos.NonVisualXamlSample solution. Unlike the previous samples – it is a console application referencing only one (not three) Avalonia nuget packages:

Image 18

The main program is located under Program.cs file and is very simple:

C#

You can put a breakpoint after the line and investigate the content of the course object:

Image 19

Take a look at Course.axaml/Course.axaml.cs files. Here is the content of Course.axaml file:

XAML

The top tag of type Course contains a single object of type Person. The NumberStudents property of the top object is set to 5, while the Person's properties are set FirstName="Joe"LastName="Doe" and Age="100".

Course.axaml.cs file defines the properties for the Course class:

C#

Note that its constructor also defines the method for loading the XAML file and that the class is marked as “partial” (the other part is generated from XAML). Also note that Teacher property has ContentAttribute – this is why we do not need to use <local:Course.Teacher> tag to place the Person object into.

Finally, here is the code for Person class:

C#

Conclusion

This article gives a detailed explanation of basic XAML functionality with samples.