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.
Note that the code has been moved over to NP.Ava.Demos repository and all the samples have been upgraded to work under Avalonia 11.0.6 (latest stable Avalonia version)
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 Avalonia 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.Ava.Demos/NP.Demos.XamlSamples within Github NP.Ava.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:
<my_namespace:MyClass Prop1="Hello World"
Prop2="123"/>
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:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/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
:
Compile and run the solution - here is what you are going to see:
There are five square buttons of different colors stacked vertically within a window.
Here is the relevant code of MainWindow.xaml file:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dep1="clr-namespace:Dependency1Proj;assembly=Dependency1Proj"
xmlns:dep1_sub_Folder="clr-namespace:Dependency1Proj.SubFolder;
assembly=Dependency1Proj"
xmlns:local="clr-namespace:NP.Demos.XamlNamespacesSample"
xmlns:dep2="https://avaloniademos.com/xaml"
...>
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center">
<dep1:RedButton/>
<dep1_sub_Folder:BlueButton/>
<local:BrownButton/>
<dep2:GreenButton/>
<dep2:GrayButton/>
</StackPanel>
</Window>
There are four custom namespaces defined by the following lines of the top XML tag:
xmlns:dep1="clr-namespace:Dependency1Proj;assembly=Dependency1Proj"
xmlns:dep1_test_Folder="clr-namespace:Dependency1Proj.SubFolder;assembly=Dependency1Proj"
xmlns:local="clr-namespace:NP.Demos.XamlNamespacesSample"
xmlns:dep2="https://avaloniademos.com/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:
namespace Dependency1Proj
{
public class RedButton : Button, IStyleable
{
Type IStyleable.StyleKey => typeof(Button);
public RedButton()
{
Background = new SolidColorBrush(Colors.Red);
Width = 30;
Height = 30;
}
}
}
The line...
Type IStyleable.StyleKey => typeof(Button);
...(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:
xmlns:dep1="clr-namespace:Dependency1Proj;assembly=Dependency1Proj"
The value of the namespace contains two parts divided by ';
' semicolon. The first part refers to the C# namespace:
clr-namespace:Dependency1Proj
and the second part refers to assembly name:
assembly=Dependency1Proj
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:
xmlns:dep1_sub_Folder="clr-namespace:Dependency1Proj.SubFolder;assembly=Dependency1Proj"
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:
xmlns:local="clr-namespace:NP.Demos.XamlNamespacesSample"
GreenButton
and GrayButton
are defined in two different namespaces of Dependency2Proj
. GreenButton
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:
[assembly: XmlnsDefinition("https://avaloniademos.com/xaml", "Dependency2Proj")]
[assembly: XmlnsDefinition("https://avaloniademos.com/xaml", "Dependency2Proj.SubFolder")]
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:
xmlns:dep2="https://avaloniademos.com/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...
xmlns:dep2="https://avaloniademos.com/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.:
<my_namespace:MyClass Prop1="Hello World"
Prop2="123"/>
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:
namespace NP.Demos.AccessPropertiesInXamlSample
{
public class Person
{
public int AgeInYears { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public override string ToString()
{
return $"Person: {FirstName} {LastName}, Age: {AgeInYears}";
}
}
}
We want to display it as the Window's content. Here is what we have within MainWindow.xaml file:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:NP.Demos.AccessPropertiesInXamlSample"
x:Class="NP.Demos.AccessPropertiesInXamlSample.MainWindow"
Width="300"
Height="200"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Title="AccessPropertiesInXamlSample">
<Window.Content>
<local:Person FirstName="Joe"
LastName="Doe"
AgeInYears="25"/>
</Window.Content>
</Window>
Note the way we assign Content
property of the Window
to a composite Type
:
<Window ...>
<Window.Content>
<local:Person FirstName="Joe"
LastName="Doe"
AgeInYears="25"/>
</Window.Content>
</Window>
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:
<Window ...>
<Window.Width>
<x:Double>300</x:Double>
</Window.Width>
</Window>
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:
xmlns:x="http://schemas.microsoft.com/winfx/2006/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:
var button = this.FindControl<button>("CloseWindowButton");</button>
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:
Pressing "Change Status Color" button will result in the third rectangle switching its color to red:
Here is the MainWindow.xaml file for the sample:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NP.Demos.StaticVsDynamicXamlResourcesSample.MainWindow"
Title="NP.Demos.StaticVsDynamicXamlResourcesSample"
Width="300"
Height="200">
<Window.Resources>
<ResourceDictionary>
<SolidColorBrush x:Key="StatusBrush"
Color="Green"/>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel x:Name="ElementsPanel"
Orientation="Vertical">
<Border x:Name="Border1"
Background="{StaticResource StatusBrush}"
Height="30"
Width="80"
Margin="0,5"/>
<Border x:Name="Border2"
Height="30"
Width="80"
Margin="0,5">
<Border.Background>
<StaticResource ResourceKey="StatusBrush"/>
</Border.Background>
</Border>
<Border x:Name="StatusChangingBorder"
Background="{DynamicResource StatusBrush}"
Height="30"
Width="80"
Margin="0,5"/>
</StackPanel>
<Button x:Name="ChangeStatusButton"
Grid.Row="1"
Width="160"
HorizontalAlignment="Right"
HorizontalContentAlignment="Center"
Content="Change Status Color"
Margin="10"/>
</Grid>
</Window>
We define the XAML resource within the same MainWindow.xaml file as a resource of the window:
<Window.Resources>
<ResourceDictionary>
<SolidColorBrush x:Key="StatusBrush"
Color="Green"/>
</ResourceDictionary>
</Window.Resources>
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:
<Border x:Name="Border1"
Background="{StaticResource StatusBrush}"
Height="30"
Width="80"
Margin="0,5"/>
For the second border, we use StaticResource
class, without markup extension (and you can see that the corresponding XAML is considerably more verbose):
<Border x:Name="Border2"
Height="30"
Width="80"
Margin="0,5">
<Border.Background>
<StaticResource ResourceKey="StatusBrush"/>
</Border.Background>
</Border>
Finally, the third border uses DynamicResource
markup extension:
<Border x:Name="StatusChangingBorder"
Background="{DynamicResource StatusBrush}"
Height="30"
Width="80"
Margin="0,5"/>
Button "StatusChangingBorder
" is hooked within MainWindow.xaml.cs file to change the "StatusBrush
" Resource from "Green
" to "Red
":
public MainWindow()
{
InitializeComponent();
Button button =
this.FindControl<Button>("ChangeStatusButton");
button.Click += Button_Click;
}
private void Button_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var statusBrush = this.FindResource("StatusBrush");
this.Resources["StatusBrush"] =
new SolidColorBrush(Colors.Red);
}
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 StaticResource
. DynamicResources
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:
The solution consists of two projects - the main project, NP.Demos.XamlResourcesInMultipleProjects
and another project which the main project depends on - Dependency1Proj
:
RedBrush
resource is defined within Themes/BrushResources.axaml file under Dependency1Proj
:
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="GreenBrush"
Color="Green"/>
</ResourceDictionary>
Note that the BrushResources.axaml file has "Avalonia XAML" build action (as any Avalonia XAML resource file should):
Such files are created by choosing "Resource Dictionary (Avalonia)" template for Visual Studio new item creation:
GreenBrush
Avalonia Resource is defined within Themes/LocalBrushResources.axaml file (this file is located in the main project):
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="GreenBrush"
Color="Green"/>
</ResourceDictionary>
Here is the content of MainWindow.axaml file:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NP.Demos.XamlResourcesInMultipleProjects.MainWindow"
Title="NP.Demos.XamlResourcesInMultipleProjects"
Width="100"
Height="180">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://Dependency1Proj/Themes/BrushResources.axaml"/>
<ResourceInclude Source="/Themes/LocalBrushResources.axaml"/>
</ResourceDictionary.MergedDictionaries>
<SolidColorBrush x:Key="BlueBrush"
Color="Blue"/>
</ResourceDictionary>
</Window.Resources>
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center">
<Border x:Name="RedBorder"
Width="70"
Height="30"
Background="{StaticResource RedBrush}"
Margin="5"/>
<Border x:Name="GreenBorder"
Width="70"
Height="30"
Background="{StaticResource GreenBrush}"
Margin="5"/>
<Border x:Name="BlueBorder"
Width="70"
Height="30"
Background="{StaticResource BlueBrush}"
Margin="5"/>
</StackPanel>
</Window>
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:
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://Dependency1Proj/Themes/BrushResources.axaml"/>
<ResourceInclude Source="/Themes/LocalBrushResources.axaml"/>
</ResourceDictionary.MergedDictionaries>
<SolidColorBrush x:Key="BlueBrush"
Color="Blue"/>
</ResourceDictionary>
</Window.Resources>
<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
.
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:
public class LocalProjectStaticBrushes
{
public static Brush GreenBrush { get; set; } =
new SolidColorBrush(Colors.Green);
}
Here is the content of DependencyProjectStaticBrushes
class:
public class DependencyProjectStaticBrushes
{
public static Brush RedBrush { get; set; } =
new SolidColorBrush(Colors.Red);
}
Running the project will create a window with two rectangles, red and green:
Here are the relevant parts of MainWindow.axaml file:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dep1="clr-namespace:Dependency1Proj;assembly=Dependency1Proj"
xmlns:local="clr-namespace:NP.Demos.XStaticMarkupExtensionSample"
...>
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center">
<Border Width="70"
Height="30"
Background="{x:Static dep1:DependencyProjectStaticBrushes.RedBrush}"
Margin="5" />
<Border Width="70"
Height="30"
Background="{x:Static local:LocalProjectStaticBrushes.GreenBrush}"
Margin="5" />
</StackPanel>
</Window>
There are two important namespaces defined at the Window
tag level:
xmlns:dep1="clr-namespace:Dependency1Proj;assembly=Dependency1Proj"
xmlns:local="clr-namespace:NP.Demos.XStaticMarkupExtensionSample"
"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:
Background="{x:Static dep1:DependencyProjectStaticBrushes.RedBrush}"
and:
Background="{x:Static local:LocalProjectStaticBrushes.GreenBrush}"
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:
public class ValuesContainer<TVal1, TVal2>
{
public TVal1? Value1 { get; set; }
public TVal2? Value2 { get; set; }
}
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:
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
Here is the code for this sample:
<Grid RowDefinitions="Auto, Auto">
<Grid.DataContext>
<local:ValuesContainer x:TypeArguments="x:Double, x:String"
Value1="300"
Value2="Hello 1"/>
</Grid.DataContext>
<TextBlock Text="ValuesContainer Generics Sample:"
FontWeight="Bold"/>
<Grid Background="Yellow"
Grid.Row="1"
RowDefinitions="Auto, Auto"
Width ="{Binding Path=Value1}"
HorizontalAlignment="Left">
<TextBlock Text="{Binding Path=Value1,
StringFormat='Width of Yellow Rectangle=Value1=\{0\}'}"
Margin="5"/>
<TextBlock Text="{Binding Path=Value2, StringFormat='Value2=\'\{0\}\''}"
Grid.Row="1"
Margin="5,0,5,5"/>
</Grid>
</Grid>
We define the ValuesContainer
object to be the DataContext
of the Grid
that contains the code for the sample:
<Grid.DataContext>
<local:ValuesContainer x:TypeArguments="x:Double, x:String"
Value1="300"
Value2="Hello 1"/>
</Grid.DataContext>
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
:
<Grid Background="Yellow"
...
Width ="{Binding Path=Value1}" ...>
So that the width of the yellow rectangle will be 300.
List of ValuesContainer Objects Sample
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:
<Grid RowDefinitions="Auto, Auto">
<Grid.DataContext>
<collections:List x:TypeArguments="local:ValuesContainer(x:Int32, x:String)">
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
Value1="1"
Value2="Hello 1"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
Value1="2"
Value2="Hello 2"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
Value1="3"
Value2="Hello 3"/>
</collections:List>
</Grid.DataContext>
<TextBlock Text="List of ValuesContainer Generics Sample:"
FontWeight="Bold"/>
<ItemsControl Items="{Binding}"
Grid.Row="1">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Value1,
StringFormat='Value1=\{0\}'}"/>
<TextBlock Text="{Binding Path=Value2,
StringFormat='Value2=\'\{0\}\''}"
Margin="10,0,0,0"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
This time, the DataContext
of the container is defined as a List<ValuesContainer<int, string>>
, i.e., we have two levels of type argument recursion:
<collections:List x:TypeArguments="local:ValuesContainer(x:Int32, x:String)">
...
</collections:List>
Then, since List<...>
has an Add
method, we can simply add the individual objects within the List<...>
:
<collections:List x:TypeArguments="local:ValuesContainer(x:Int32, x:String)">
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
Value1="1"
Value2="Hello 1"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
Value1="2"
Value2="Hello 2"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
Value1="3"
Value2="Hello 3"/>
</collections:List>
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 list and use the ItemTemplate
of the ItemsControl
to display Value1
and Value2
of each individual item:
<ItemsControl Items="{Binding}"
Grid.Row="1">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Value1,
StringFormat='Value1=\{0\}'}"/>
<TextBlock Text="{Binding Path=Value2,
StringFormat='Value2=\'\{0\}\''}"
Margin="10,0,0,0"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Dictionary of integer to ValuesContainer Objects Sample
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:
<Grid RowDefinitions="Auto, Auto">
<Grid.DataContext>
<collections:Dictionary x:TypeArguments="x:String, local:ValuesContainer(x:Int32, x:String)">
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
x:Key="Key1"
Value1="1"
Value2="Hello 1"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
x:Key="Key2"
Value1="2"
Value2="Hello 2"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
x:Key="Key3"
Value1="3"
Value2="Hello 3"/>
</collections:Dictionary>
</Grid.DataContext>
<TextBlock Text="Dictionary of ValuesContainer Generics Sample:"
FontWeight="Bold"/>
<ItemsControl Items="{Binding}"
Grid.Row="1">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Key,
StringFormat='Key=\'\{0\}\''}"/>
<TextBlock Text="{Binding Path=Value.Value1,
StringFormat='Value1=\{0\}'}"
Margin="10,0,0,0"/>
<TextBlock Text="{Binding Path=Value.Value2,
StringFormat='Value2=\'\{0\}\''}"
Margin="10,0,0,0"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
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:
<collections:Dictionary x:TypeArguments="x:String, local:ValuesContainer(x:Int32, x:String)">
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
x:Key="Key1"
Value1="1"
Value2="Hello 1"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
x:Key="Key2"
Value1="2"
Value2="Hello 2"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
x:Key="Key3"
Value1="3"
Value2="Hello 3"/>
</collections:Dictionary>
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:
[
{ Key="Key1"
Value = new ValuesContainer{ Value1=1, Value2="Hello 1"},
{ Key="Key2"
Value = new ValuesContainer{ Value1=2, Value2="Hello 2"},
{ Key="Key3"
Value = new ValuesContainer{ Value1=3, Value2="Hello 3"}
]
Here is how we bind our ItemsControl
to those objects and display them using its ItemTemplate
:
<ItemsControl Items="{Binding}"
Grid.Row="1">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Key,
StringFormat='Key=\'\{0\}\''}"/>
<TextBlock Text="{Binding Path=Value.Value1,
StringFormat='Value1=\{0\}'}"
Margin="10,0,0,0"/>
<TextBlock Text="{Binding Path=Value.Value2,
StringFormat='Value2=\'\{0\}\''}"
Margin="10,0,0,0"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
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:
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"):
Build and run the sample, here is what you'll see:
There are four vertically stacked images - here is the corresponding code:
<Image Source="/Assets/LinuxIcon.jpg"
Width="50"
Height="50"
Margin="5"/>
<Image Source="avares://Dependency1Proj/Assets/avalonia-32.png"
Width="50"
Height="50"
Margin="5"/>
<Image x:Name="LinuxIconImage2"
Width="50"
Height="50"
Margin="5"/>
<Image x:Name="AvaloniaIconImage2"
Width="50"
Height="50"/>
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:
Source="/Assets/LinuxIcon.jpg"
While the image located in a different project should be using a full version of the URL prepended with avares://.
Source="avares://Dependency1Proj/Assets/avalonia-32.png"
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:
public MainWindow()
{
InitializeComponent();
...
var assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
Image linuxIconImage2 = this.FindControl<Image>("LinuxIconImage2");
linuxIconImage2.Source =
new Bitmap
(
assetLoader.Open(
new Uri("avares://NP.Demos.ReferringToAssetsInXaml/Assets/LinuxIcon.jpg")));
Image avaloniaIconImage2 = this.FindControl<Image>("AvaloniaIconImage2");
avaloniaIconImage2.Source =
new Bitmap
(
assetLoader.Open(
new Uri("avares://Dependency1Proj/Assets/avalonia-32.png")));
}
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:
The main program is located under Program.cs file and is very simple:
public static void Main(string[] args)
{
Course course = new Course();
}
You can put a breakpoint after the line and investigate the content of the course
object:
Take a look at Course.axaml/Course.axaml.cs files. Here is the content of Course.axaml file:
<x:Object xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:NP.Demos.NonVisualXamlSample"
x:Class="NP.Demos.NonVisualXamlSample.Course"
NumberStudents="5">
<local:Person FirstName="Joe" LastName="Doe" Age="100" />
</x:Object>
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:
public partial class Course
{
public Course()
{
AvaloniaXamlLoader.Load(this);
}
public int NumberStudents { get; set; }
[Content]
public Person? Teacher { get; set; }
}
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:
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public double Age { get; set; }
}
Conclusion
This article gives a detailed explanation of basic XAML functionality with samples.
History
- 4th October, 2021: Initial version
- 22nd December, 2023: Article updated