Avalonia Ui for beginners
1. Introduction
Avalonia UI is a cross-platform UI framework for .NET. it allows you to easily make cross platform apps with one code base.
Avalonia is based on WPF but with a different rendering engine, which makes them very similar in syntax.
In this guide (for the lack of ideas) we’ll recreate the blazor’s default template in avalonia.
*Note: this guide does not follow all best practices and is meant as a simple way for beginners to get started with Avalonia
2. Prerequisites
- Basic C# knowledge
- Any .Net version that implements .NET Standard 2.0
- Some understanding of HTML or WPF will help a lot but is not necessary
3. Making the app
1. Creating the project
Since i’m on linux i only have access to VScode and the terminal so everything will be in the terminal, but if you’re on windows/mac and using visual studio/rider then you can use their UI tools.
First we need to install the Avalonia templates to create a project
1
dotnet new install Avalonia.Templates
then to create a project (or you can use VS / rider UI)
1
dotnet new avalonia.app -o CatApp
This will generate a new folder called CatApp
that contains the template code for out project.
*Note: there’re other templates but this is the most basic one
Now open the folder in your preferred IDE, I’ll be using VScode, then run the project to make sure everything works fine.
2. Preparing the project
Choosing the correct SDK
When creating a new avalonia project using the provided templates it will default to .Net 6 no matter which .Net version you have installed, If that’s not the version you want to use then you can change it by editing the csproj file. I’ll be using .Net 7 because that’s the latest one as of the time of me writing this.
*Note: Avalonia only works on .net standard 2.0 supported SDKs
Choosing a pattern
For this guide we’ll be Using the MVC pattern since it’s easy to understand and use.
First we need to make some folders to organize the project, under your project’s folder make 3 folders Models
, Views
and Controllers
so that your folder structure looks like this:
The
Models
folder will hold our object classes.The
Views
folder will hold our UIs.And the
Controllers
folder will contain the logic for each Ui.
(ignore the .vscode
folder, that just holds my settings)
Preparing our main window
By default the template gives us a main window directly under the root folder so in our case it’s CatApp/MainWindow.axaml
and CatApp/MainWindow.axaml.cs
and if we run the project
1
dotnet run
We should get this
That’s nice, but for our purposes we need more than just a welcome text.
To start we need to organize the project.
- create a
Main
folder underViews
and move bothCatApp/MainWindow.axaml
andCatApp/MainWindow.axaml.cs
to it. - change the
namespace
of the .cs file so it represents the new folder structure in our case it’s:CatApp.Views.Main
- change the
x:Class
attribute of the .axaml file so it also represents the new folder structure:CatApp.Views.Main.MainWindow
- in
App.axaml.cs
add the using for the MainWindow’s namespace:using CatApp.Views.Main;
Now you should try running the project again to make sure everything works.
3. Creating the first window
As i said in the introduction we’ll be recreating the blazor default template which is this
To make it easy I’ll divide the work into small sections.
Thankfully the default blazor template has a very simple layout; a sidebar (blue) with a header (red) and a main content part (green) with a header (yellow).
1. Creating the layout
So first we’ll create the regions of our layout as shown in the previous image, for that we’ll be using a Grid
element.
Open MainWindow.axaml
and remove the welcome text in it (don’t worry we’ll add it back later).
Then to create a grid, just like in html, we open a <Grid>
tag and close it with </Grid>
.
After that we need to define our rows and columns, to do that we can write them as properties to the <Grid>
tag or as their own tags inside of the <Grid>
tag.
For our case since the Ui is very simple I will write them inside of the tag, but i’ll also show you how to write them on their own just in case.
defining the boundaries inside the tag
1 2 3
<Grid RowDefinitions="70, *" ColumnDefinitions="250, *"> </Grid>
defining the boundaries in their own tags
1 2 3 4 5 6 7 8 9 10
<Grid> <Grid.RowDefinitions> <RowDefinition Height="70" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="250" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> </Grid>
So what does any of that mean? well you see when creating a grid you need to define 2 properties, rows and columns, and you need to define how many do you have of each and their size, in the first example we define how many there are by separating between their sizes with a column, in the second example we define them by how many tags we used, as for the size you can either use a constant value (I used 70 for the headers and 250 for the sidebar) or a relative value which is described by the *
*Note: If you write a *
on it’s own then it will take whatever is left of the space / how much the elements inside of it need, if you type it with a number before it, for example 10*
, then it will take 10% of the available space of the window.
Well let’s run the app and see how it looks :)
…Empty.
Well that’s actually expected since rows and columns on their own are hypothetical properties that do not create any viewable objects and are only meant as boundaries for the elements inside of the grid.
But thankfully we can enable the outlines to see the areas of the grid, to do that we only need to add ShowGridLines="true"
to the Grid tag, now if we run it we can clearly see where the boundaries are drawn.
Tho the window is very small so let’s give it a minimum size, for that we need to add the MinWidth
and MinHeight
properties to the <Window>
tag. I’ll be using 960 for the width and 420 for the height.
*Note: you can use MaxWidth
and MaxHeight
to limit the size of your app.
*Note: you can use Width
and Height
to define the size your app should start with.
Now Let’s create the basic elements of the app and stylize them a bit
2. Creating The side bar’s header (red)
For the header we can use a simple panel with a dark background. To create it simply add a panel tag inside the grid tag and give it the proper position on the grid like this:
1
2
<Panel Background="#051130" Grid.Row="0" Grid.Column="0">
</Panel>
Tags explanation:
- Background: the background color for the panel tag.
- Grid.Row: the row’s index of the grid (starts from 0).
- Grid.Column: the column’s index of the grid (starts from 0).
Then we add the title of the app inside the panel
1
<TextBlock Foreground="white" TextAlignment="Center" FontSize="18" FontWeight="Bold" Padding="0, 20"> Cat App </TextBlock>
Tags explanation:
- Foreground: the color of the text in the TextBlock.
- TextAlignment: vertical alignment of the element inside it’s parent.
- FontSize: the size of the text.
- FontWeight: bold, thin etc…
- Padding: How much empty space to have between the element and it’s own borders.
3. Creating The main content’s header (yellow)
For this we need a way to align our elements horizontally so we’ll use a <StackPanel>
which automatically stacks the elements inside of it the way you tell it to. We’ll also wrap it in a panel so we can have a nice background.
1
2
3
4
<Panel Background="#222" Grid.Column="1" Grid.Row="0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10" Margin="25, 20">
</StackPanel>
</Panel>
Tags explanation:
- Orientation: How to stack the elements inside of the stack panel (default is vertical).
- HorizontalAlignment: the direction to align the elements horizontally (Stretch, Left (default), Center, Right).
- Spacing: How much space to put between the elements.
- Margin: how much space between the stackPanel and the elements outside of it.
Now let’s add some buttons that link to some websites, and since this is a guide i’ll be adding a link to the guide and a link to the source code of this app.
To do that i’ll just add these 2 elements inside the stack panel
1
2
3
<Button Name="GithubButton" Foreground="#2c4cf1" FontSize="15" Background="Transparent" Tag="https://github.com/cabiste69/CatAppTuto"> Github</Button>
<Button Name="AboutButton" Foreground="#2c4cf1" FontSize="15" Background="Transparent" Tag="https://github.com/cabiste69/Guides"> About</Button>
Tags explanation:
- Name: A name to refer to the element with (can be used to call the element in the code behind).
- Tag: Can be anything you want but doesn’t effect the element on it’s own.
Now this doesn’t actually open the url in the browser, and i’m not aware of a native way in avalonia to open links in the browser, so we’ll need to do some c#
*Note: I’ll be using a view-controller pattern as i find it better for organization, if you don’t want that then you can write all the code i’m gonna write in the controller directly in the code behind of the main window.
First create a new file under the Controllers
folder and name it MainController.cs
then inside of it we create 2 functions, first for opening links and the second to configure the buttons:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class MainController
{
// get a reference to the main view
private readonly MainWindow _view;
public MainController(MainWindow view)
{
_view = view;
ConfigureMainHeader();
}
// subscribe to the OpenLink function on click
private void ConfigureMainHeader()
{
_view.GithubButton.Click += OpenLink;
_view.AboutButton.Click += OpenLink;
}
// opens a link in the default browser
private void OpenLink(object? sender, RoutedEventArgs e)
{
// cast the sender to a button and get the Tag as a string
string url = ((Button)sender).Tag.ToString();
// Define a process with the url as the file name
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = url,
UseShellExecute = true
};
// start a process with the defined info
Process.Start(startInfo);
}
}
(i honestly have no idea how it’s done under the hood but it’s probably advanced magic)
*Note: if you get an error the Name 'x' does not exist in the current context
then just build the app for it to generate the button names so you can call them
Now we need to link that function to the buttons, for that open the code behind for the main window, in my case it’s Views/Main/MainWindow.axaml.cs
then add a new field above the constructor that refers to the controller
1
private readonly MainController _ctrl;
then we initialize it inside the constructor
1
2
3
4
5
public MainWindow()
{
InitializeComponent();
_ctrl = new(this);
}
And that’s it.
4. Creating The side bar (blue)
For this we again need a combination of a panel and a stack panel.
1
2
3
4
<Panel Grid.Column="0" Grid.Row="1">
<StackPanel Name="SideBar" Spacing="10" Margin="20, 20">
</StackPanel>
</Panel>
Now we need to give it a gradient color by creating a separate element for the background inside the Panel and defining the properties of the gradient like so
1
2
3
4
5
6
<Panel.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Offset="0" Color="#0d194c" />
<GradientStop Offset="1" Color="#2e0539" />
</LinearGradientBrush>
</Panel.Background>
Tags explanation:
(i genuinely have no idea)
Now we need to add some buttons to the side bar, how else are we gonna navigate the app?
SO inside the side stack panel we write
1
2
3
4
5
<Button Foreground="white" Background="Transparent" Width="200" FontSize="16" Padding="30, 7" Tag="Home">Home</Button>
<Button Foreground="white" Background="Transparent" Width="200" FontSize="16" Padding="30, 7" Tag="Counter">Counter</Button>
<Button Foreground="white" Background="Transparent" Width="200" FontSize="16" Padding="30, 7" Tag="FetchData">Fetch data</Button>
Tags Explanation:
- Width: the width of the button
Now to link them we’re gonna use a different approach.
In the controller we’re gonna add a new function ConfigureSideBar
that will find all the buttons and link them.
1
2
3
4
5
6
7
8
9
10
11
private void ConfigureSideBar()
{
// look through all the children of the sidebar
foreach (var child in _view.SideBar.Children)
{
// we only need buttons
if(child is Button)
// cast the variable to a button to get intelliSense
((Button)child).Click += ChangeView;
}
}
You’ll get an error here because the ChangeView
function doesn’t exist yet so let’s create it.
Again inside of the controller’s file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void ChangeView(object? sender, RoutedEventArgs e)
{
// get the tag of the button
string ViewName = ((Button)sender).Tag.ToString();
// we'll add the logic to switch views after we create them
switch (ViewName)
{
case "Home":
Console.WriteLine("Clicked on the 'home' button.");
break;
case "Counter":
Console.WriteLine("Clicked on the 'counter' button.");
break;
case "FetchData":
Console.WriteLine("Clicked on the 'fetch data' button.");
break;
default:
Console.WriteLine($"The view {ViewName} does not exist!");
break;
}
}
Finally we call the ConfigureSideBar
function in the constructor
1
2
3
4
5
6
public MainController(MainWindow view)
{
_view = view;
ConfigureMainHeader();
ConfigureSideBar();
}
5. The main content (green)
Now this is a bit complicated, since we want the content in this area to change depending on what button we press we can’t hard code it like the other areas, instead we’ll be creating a new file for each page we have (home, counter and fetch data) then set this area’s content to the page we need.
So for now we just add an empty UserControl
to the main grid
1
2
<UserControl Name="MainContentArea" Background="#181a1b" Grid.Column="1" Grid.Row="1">
</UserControl>
This type of element works as an empty container.
4. Creating the main content
1. Home page
1. Creating the files
we can use a dotnet command to create the template for the view (if you’re on VS/rider you can use the avalonia extension)
1
dotnet new avalonia.usercontrol -n HomeView -o Views/Home
That will create a new folder Home
under the views folder and 2 template files.
First thing (if you used the command to create the files) is to change the namespaces
for HomeView.axaml.cs
it’s: namespace CatApp.Views.Home;
and for HomeView.axaml
it’s: x:Class="CatApp.Views.Home.HomeView"
2. Creating the UI
this is a simple page as it only has text blocks so we only need to modify the axaml file
1
2
3
4
5
6
7
8
9
10
11
<StackPanel Margin="22" Spacing="15">
<TextBlock FontSize="50">Hello, EveryNya!</TextBlock>
<TextBlock FontSize="16">Welcome to AvaloNya!</TextBlock>
<Border CornerRadius="4" Background="#282b2c" HorizontalAlignment="Left">
<TextBlock FontSize="16" Padding="20">
Did you know? The average cat can jump 8 feet (2.4 meters) in a single bound!
</TextBlock>
</Border>
</StackPanel>
3. writing the logic
Now that we have a UI we need to show it to the user, for that all we need is to set it as the “content” for MainContentArea
that we declared previously.
So go back to the ChangeView
function then add the logic in the home case like so:
1
2
3
case "Home":
_view.MainContentArea.Content = new HomeView();
break;
If we run the app now and click on the home button, we should see the home page in the main content’s area, but we probably want it to be visible from the start for that we’ll create a new function in Controllers/MainController.cs
, called InitializeMainContent
and call it in the constructor
1
2
3
4
private void InitializeMainContent()
{
_view.MainContentArea.Content = new HomeView();
}
2. Counter page
1. Creating the files
Just as before
1
dotnet new avalonia.usercontrol -n CounterView -o Views/Counter
And don’t forget to change the namespaces
CounterView.axaml.cs
->namespace CatApp.Views.Counter;
CounterView.axaml
->x:Class="CatApp.Views.Counter.CounterView"
2. Creating the UI
This one is also an easy one to make (thankfully 🙏🏻)
1
2
3
4
5
<StackPanel Margin="30,22" Spacing="15">
<TextBlock FontSize="40">Cat Counter</TextBlock>
<TextBlock Name="CountMessage" FontSize="16">Current cats count: 0</TextBlock>
<Button Background="#16589b" Padding="10" Click="IncrementCount">Add a cat</Button>
</StackPanel>
3. writing the logic
Since this is a simple view I’ll just write the logic in the code behind, but if you want you can create a controller just like we did in the main window
so in CounterView.axaml.cs
we add this function:
1
2
3
4
5
6
7
8
// define a private variable to hold the count
private int _count = 0;
private void IncrementCount(object? sender, RoutedEventArgs e)
{
_count++;
CountMessage.Text = $"Current cats count: {_count}";
}
Finally we add it in the ChangeView
function
1
2
3
case "Counter":
_view.MainContentArea.Content = new CounterView();
break;
3. Fetch data page
This one is a bit complicated since we need a “table” like in html but that doesn’t exist here, instead we need a DataGrid
but for some reasons this is a separate nuget package so we need to install it and configure it before we use it.
1. Installing DataGrid
To install it we can use this command (or use the nuget manager of your IDE)
1
dotnet add package Avalonia.Controls.DataGrid
then inside App.axaml
we add this
1
2
3
<Application.Styles>
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
</Application.Styles>
2. Creating the files
1
dotnet new avalonia.usercontrol -n FetchDataView -o Views/FetchData
FetchDataView.axaml.cs
->namespace CatApp.Views.FetchData;
FetchDataView.axaml
->x:Class="CatApp.Views.FetchData.FetchDataView"
3. Writing the forecast service
It will be easier to explain if i start with the logic part
a huge chunk of this part will be copied from the blazor template :p
Create a new class in the
Models
folderCatModel.cs
1 2 3 4 5 6 7 8 9 10 11 12
namespace CatApp.Models; public class CatModel { public string Name { get; set; } public int Weight { get; set; } public DateOnly BirthDate { get; set; } public int Age => DateTime.Now.Year - BirthDate.Year; }
Create a new folder
Services
and inside of it create a classCatsService.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
namespace CatApp.Services; public class CatsService { // first google result private static readonly string[] Names = new[] { "Luna", "Charlie", "Shadow", "Oliver", "Simba", "Smokey", "Bella", "Tiger", "Pepper", "Milo", "Nala" }; // Generates a random array of Cats public static CatModel[] GetCatsAsync() { DateOnly today = DateOnly.FromDateTime(DateTime.Now); return Enumerable.Range(1, 30).Select(index => new CatModel { Name = Names[Random.Shared.Next(Names.Length)], Weight = Random.Shared.Next(0, 10), // roughly how many days in 20 years BirthDate = today.AddDays(Random.Shared.Next(-7300, 0)) }).ToArray(); } }
4. Creating the UI
First We’ll add 2 text blocks and an empty data grid inside a stack panel in Views/FetchData/FetchDataView.axaml
1
2
3
4
5
6
<StackPanel Spacing="15" Margin="30, 22">
<TextBlock FontSize="40">Cat list</TextBlock>
<TextBlock FontSize="16">these cats are still waiting to be adopted</TextBlock>
<DataGrid Name="CatsGrid" ColumnWidth="150" Width="600" HorizontalAlignment="Left" GridLinesVisibility="Horizontal">
</DataGrid>
</StackPanel>
Next we need to define the columns and bind them to the CatModel
class.
To bind a model we need 2 things
Add the model’s namespace in the
UserControl
tag as a property1
xmlns:mdl="using:CatApp.Models"
xmlns
stands for “xml namespace” then we append a name for the namespace to reference it later which ismdl
in this case.Add the class to the dataGrid with the
x:DataType
property1
x:DataType="mdl:CatModel"
This makes the dataGrid recognize the class.
Now that the dataGrid knows the class, we can define the headers and link them. We do that by adding this inside the dataGrid
1
2
3
4
5
6
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<DataGridTextColumn Header="Age (years)" Binding="{Binding Age}"/>
<DataGridTextColumn Header="Weight (Kg)" Binding="{Binding Weight}"/>
<DataGridTextColumn Header="Birth date" Binding="{Binding BirthDate}"/>
</DataGrid.Columns>
We define a header, give it a name and link it to a property inside the class.
Lastly for some customization I’m gonna wrap all the elements in a ScrollViewer
element then add a couple of properties to the dataGrid
1
VerticalScrollBarVisibility="Disabled" IsReadOnly="True"
Tags explanation:
- VerticalScrollBarVisibility: enable / disable the built-in scroll for the data grid (default enabled)
- IsReadOnly: enable / disable editing rows by the user (default enabled)
5. Linking the UI with the logic
Now that all the UI is done all we need to do is generate some data and fill the dataGrid with.
So as usual, we create a controller for the view
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
namespace CatApp.Controllers;
public class FetchDataController
{
// to modify the view from the controller
private readonly FetchDataView _view;
public FetchDataController(FetchDataView view)
{
_view = view;
InitializeDataGrid();
}
// populates the DataGrid with cats on class initialization
private void InitializeDataGrid()
{
var cats = CatsService.GetCatsAsync();
_view.CatsGrid.ItemsSource = cats;
// set a custom row height in pixels (default is 35)
_view.CatsGrid.RowHeight = 40;
// calculate the height of the dataGrid
// this is not necessary but without it the external scroll will break
_view.CatsGrid.Height = (cats.Length + 2) * _view.CatsGrid.RowHeight;
}
}
We initialize the controller in the code behind like so
Views/FetchData/FetchDataView.axaml.cs
1
2
3
4
5
6
private readonly FetchDataController _ctrl;
public FetchDataView()
{
InitializeComponent();
_ctrl = new(this);
}
And Finally we add it to the change view function in the main controller
1
2
3
case "FetchData":
_view.MainContentArea.Content = new FetchDataView();
break;
4. Results
And that’s it, the app is done and it only took 750 lines (guide length) to finish!
Here’s a preview of how the app looks like
If yours is slightly different then that might be because of different avalonia versions
Conclusion
Avalonia is a very cool library that allows us (.net devs) to create cross platform applications using a single codebase. It’s easy, fast and looks good by default.