Post

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

commit

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

commit

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:

Avalonia folder structure

  • 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

commit

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

default avalonia window

That’s nice, but for our purposes we need more than just a welcome text.

To start we need to organize the project.

  1. create a Main folder under Views and move both CatApp/MainWindow.axaml and CatApp/MainWindow.axaml.cs to it.
  2. change the namespace of the .cs file so it represents the new folder structure in our case it’s: CatApp.Views.Main
  3. change the x:Class attribute of the .axaml file so it also represents the new folder structure: CatApp.Views.Main.MainWindow
  4. 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 blazor server home page

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).

Abstract layout of blazor's home page

1. Creating the layout

commit

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.

  1. defining the boundaries inside the tag

    1
    2
    3
    
     <Grid RowDefinitions="70, *" ColumnDefinitions="250, *">
    
     </Grid>
    
  2. 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)

commit

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)

commit

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)

commit

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)

commit

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

commit

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

commit

blazor 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

blazor 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

  1. Create a new class in the Models folder CatModel.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;
     }
    
  2. Create a new folder Services and inside of it create a class CatsService.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

  1. Add the model’s namespace in the UserControl tag as a property

    1
    
         xmlns:mdl="using:CatApp.Models"
    

    xmlns stands for “xml namespace” then we append a name for the namespace to reference it later which is mdl in this case.

  2. Add the class to the dataGrid with the x:DataType property

    1
    
         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.

This post is licensed under CC BY 4.0 by the author.