Avalonia for the little ones

In the fresh preview of Rider, among other things, there was support for Avalonia. Avalonia is the largest .NET framework for developing cross-platform UI, and its support in the IDE is a great reason to finally figure out how to write desktop applications for any platform.

In this article, I will show you the example of a simple task of implementing a calculator:

  • how to manage the markup,
  • how to bind functionality to components,
  • how to manage styles.


Training

For work I used:

The only must-have tool on this list is the dotnet itself. You can choose the rest yourself: your favorite operating system and IDE (for example, the same Rider).
To initialize the project, we will use .NET Application Templates for Avalonia… To do this, we need to clone the template repository, and then install the downloaded templates:

git clone https://github.com/AvaloniaUI/avalonia-dotnet-templates.git
dotnet new --install /path/avalonia-dotnet-templates/

Types of Avalonia projects
Project types

Now that the templates are installed, we can create a new project based on the Avalonia MVVM template:

dotnet new avalonia.mvvm -o ACalc

Let’s go to the project directory and update all package versions to the newest ones (at the time of this writing):

dotnet add package Avalonia --version 0.10.0-preview6
dotnet add package Avalonia.Desktop --version 0.10.0-preview6
dotnet add package Avalonia.ReactiveUI --version 0.10.0-preview6

Let’s take a closer look at the project structure generated by the template:

image

  • In folder Assets the resources used by us in this project are stored. At the moment, there is the Avalonia logo, which is used as an application icon.
  • To folder Model we will be adding up all the common models used in our application. It is currently empty.
  • Folder ViewModels is intended to store the logic that will be used in each of the windows. Right now, this folder contains the main window ViewModel and the base class for all ViewModels.
  • In folder Views the layout of the windows is stored (as well as the code behind file, in which, although you can put logic, but it is better to use the ViewModel for these purposes). At the moment we only have the main window.
  • App.xaml – general application config. Despite the fact that it looks like another window, in fact, this file is used to set general settings for the application.
  • ViewLocator it won’t be useful to us this time, since it is used to create custom controls. You can read more about it in the documentation of Avalonia

Let’s start our application with the dotnet run command.

Everything is now ready for development.

Markup

Let’s start by creating some basic markup. Let’s go to the Views / MainWindow.xaml file – the layout of the main window of our calculator will be stored there.

At the moment, our markup consists of the basic parameters of the window (size, icon and title) and one block with the text. Let’s replace this block of text with Grid, which will serve as the skeleton of our markup. This control will sort all the elements in order, one by one.

So, let’s replace the TextBlock with an empty Grid:

<Grid></Grid>

Now let’s prepare the basis for our markup. First, let’s indicate how many lines our application needs and how high they should be:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="auto"></RowDefinition>
        <RowDefinition Height="auto"></RowDefinition>
        <RowDefinition Height="*"></RowDefinition>
    </Grid.RowDefinitions>
</Grid>

Now let’s fill in the markup with the main components – add a menu bar, a basic screen, and a nested Grid for the key block:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="auto"></RowDefinition>
        <RowDefinition Height="auto"></RowDefinition>
        <RowDefinition Height="*"></RowDefinition>
    </Grid.RowDefinitions>
    <!--строка меню-->
    <Menu>
    </Menu>
    <!--Импровизированный экран нашего калькулятора-->
    <TextBlock>
    </TextBlock>
    <!--Grid для клавиш-->
    <Grid></Grid>
</Grid>

Let us dwell separately on the arrangement of the keys in the grid.
First you need to describe the number of rows and columns in our Grid. And then – expand the buttons according to their corresponding rows and columns, indicating their coordinates.

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
         <ColumnDefinition></ColumnDefinition>
         <ColumnDefinition></ColumnDefinition>
         <ColumnDefinition></ColumnDefinition>
         <ColumnDefinition></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Button Grid.Row="0" Grid.Column="0">1</Button>
</Grid>

It’s worth noting that elements inside a Grid can span multiple cells. For this, the ColumnSpan and RowSpan parameters are used:

 <Button Grid.Row="3" Grid.Column="3" Grid.ColumnSpan="2">=</Button>

The rest of the buttons are added in the same way, so the finished markup can be viewed directly in project repositories

The last thing we need to do is set the window parameters. Let’s set the starting and minimum window sizes (they are set in the root Window element).

MinHeight="300"
MinWidth="250"
Height="300"
Width="250"

After adding all the markup elements, our calculator window will look like this:

Main functionality

With the markup finished, it’s time to implement the logic!

Let’s start by adding a new Enum to the Models folder that describes the possible operations:

public enum Operation
{
    Add,
    Subtract,
    Multiply,
    Divide,
    Result
}

Now let’s go to the ViewModel / MainWindowViewModel class. This is where the main functionality of our application will be stored.

Let’s add several private fields to the file, with which we will work:

private double _firstValue;
private double _secondValue;
private Operation _operation = Operation.Add;

Now let’s implement the main methods:

  • AddNumber – adds a new digit to the number.
  • ExecuteOperation – performs one of the operations described in the Operation name.
  • RemoveLastNumber – deletes the last entered digit.
  • ClearScreen – clears the calculator screen.

We will not dwell on the implementation of these methods, there is no specificity for Avalonia (you can look at the implementation in project repositories). The only thing that interests us is that in addition to the above private fields, these methods also operate on a public property ShownValue… About him – a little later.

Binding

Now that we have both the markup and the logic ready, it’s time to link them together.
Reactive UI is included by default in Avalonia – this is a framework designed just for linking View and Model when using MVVM. You can read more about it at official website and in documentation of Avalonia… Specifically, now we are interested in the ability of the framework to update the View when the data changes.

To store the actual value displayed on the screen, we implement the ShownValue property:

public double ShownValue
{
    get => _secondValue;
    set => this.RaiseAndSetIfChanged(ref _secondValue, value);
}

The value obtained from this property will be displayed on the display of our calculator, and the RaiseAndSetIfChanged method will take care of calling a notification when the property value changes.

Let’s bind this property to the text field created at the markup stage:

<TextBlock Grid.Row="1" Text="{Binding ShownValue}" />

Thanks to the Binding directive and the RaiseAndSetIfChanged method, the value of the Text property in this field will be updated whenever the value of the ShownValue property changes.

Now let’s add three more public properties for commands to MainWindowViewModel. Commands are wrappers around functions that will be called by specific actions on the UI.

public ReactiveCommand<int, Unit> AddNumberCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveLastNumberCommand { get; }
public ReactiveCommand<Operation, Unit> ExecuteOperationCommand { get; }

Commands need to be initialized in the class constructor, linking them to the appropriate methods:

public MainWindowViewModel()
{
    AddNumberCommand = ReactiveCommand.Create<int>(AddNumber);
    ExecuteOperationCommand = ReactiveCommand.Create<Operation>(ExecuteOperation);
    RemoveLastNumberCommand = ReactiveCommand.Create(RemoveLastNumber);
}

Now let’s update the button markup. For example, for the Backspace key, the new markup would look like this:

<Button Grid.Row="3" Grid.Column="2" Command="{Binding RemoveLastNumberCommand}">←</Button>

The situation is somewhat more complicated with number buttons and operation buttons. For them, we must pass the input digit or operation as a parameter. To do this, in the root Window tag, we need to add the System namespace:

xmlns:s="clr-namespace:System;assembly=mscorlib"

And then update the button markup by adding an associated method and parameter to them:

<Button Grid.Row="0" Grid.Column="0" Command="{Binding AddNumberCommand}">
    <Button.CommandParameter>
        <s:Int32>1</s:Int32>
    </Button.CommandParameter>
     1
</Button>

After we update all other buttons in the same way, the functionality of the calculator will be completely ready to work.

Styles

So, the logic of our calculator is fully implemented, but its visual side leaves much to be desired. It’s time to play with styles!

There are three ways to manage styles in Avalonia:

  • set up styles inside the component,
  • customize the styles within the window,
  • connect the style pack.

Let’s go through each of them.

Let’s start by setting up styles within a specific component. An obvious contender for pinpoint changes is our calculator screen. Let’s increase the font size for it and move the text to the right.

<TextBlock Grid.Row="1" Text="{Binding ShownValue}" TextAlignment="Right" FontSize="30" />

Now let’s play with the styles within the window. Here we can change the appearance of all components of a certain type. For example, you can move the buttons a little.

<Window.Styles>
    <Style Selector="Button">
        <Setter Property="Margin" Value="5"></Setter>
     </Style>
</Window.Styles>

As you can see, the specific components to which the style is applied can be selected using the selector. You can read more about selectors in documentation of Avalonia

After applying the changes above, our window will look like this

To make your life easier, you can use a ready-made style pack. Let’s, for example, include the Material style for our calculator. To do this, add the appropriate nuget package:

dotnet add package Material.Avalonia --version 0.10.3

Now let’s update the App.xaml file and specify the style pack to use and its parameters.

<Application ...
             xmlns:themes="clr-namespace:Material.Styles.Themes;assembly=Material.Styles"
             ...>
    <Application.Resources>
        <themes:BundledTheme BaseTheme="Dark" PrimaryColor="Purple" SecondaryColor="Amber"/>
    </Application.Resources>
    <Application.Styles>
        <StyleInclude Source="avares://Material.Avalonia/Material.Avalonia.Templates.xaml" />
    </Application.Styles>
</Application>

The installed package will update the visual style of our application, and now it will look like this:

You can create the same style packs yourself – they can be used inside your project or distributed as a package to nuget. More information about styles and how to manage them can be found in the documentation.

Conclusion

In this article, we have analyzed the simplest example of using Avalonia, but the functionality of this framework is much wider, and it is growing every day. In addition to the repeatedly mentioned by me documentationyou can also ask for advice at Russian-speaking chatdedicated to Avalonia, or right here in the comments.
And many more interesting things about Avalonia and .NET UI can be listened to at online meetup from Kontur, which will take place today, at five Moscow time.

All source codes of the project can be found in repositories on Github

That’s all! Stay tuned, we’ll be back with articles on more advanced features in Avalonia.

Similar Posts

2 Comments

  1. Hi! How can we modify this program to take decimal numbers as input? The tutorial is really good

Leave a Reply

Your email address will not be published. Required fields are marked *