Avalonia UI – Cross-Platform WPF application development with .NET Core
If you search for the term “Cross-Platform” – especially in the context of desktop applications – you will quickly find different providers. One of the most prominent ones is Electron. With Electron, many useful tools such as Visual Studio Code, Slack, Microsoft Teams or even Discord have been implemented. A big advantage is that development is done in a Chromium browser environment with Javascript while having access to native parts of the host system. However, electron apps are said to consume a lot of memory compared to native applications. In short: Electron is certainly a good solution, but not a panacea. This is true for many other solutions, of course, and although I have tried several solutions, the market for cross-platform development is too big to know all the vendors and possibilities. Recently, for example, one of my colleagues described how to use Flutter to create an app for Android and iOS on a common code base.¹ It was quite simple and elegant.
.NET Core 3.0 – Desktop Applications and WPF
Of course, you have been able to develop cross-platform applications with .NET Core since the beginning. However, these are primarily used on servers in the form of web or console applications. With the new version 3.0 Microsoft has now officially delivered support for WinForms and WPF with .NET Core. WPF is the acronym for Windows Presentation Foundation; a framework that enables the development of desktop applications on Windows with hardware-accelerated graphics and vector graphics. An essential feature of the framework is the strict separation of user interfaces and business logic, which is realised by so-called bindings. Thus MVVM or MVC patterns can be implemented very well.
From my point of view, .NET Core 3.0 is a big step to use the advantages of .NET Core for existing applications, but at the same time to let the desktop application itself run only under Windows.
What is Avalonia UI?
Avalonia UI combines the platform independence of .NET Core with a new development strongly based on WPF. It is a true platform-independent desktop application with an interface that can be used on Windows, Mac and Linux.
If you are familiar with WPF, you will immediately feel at home. Currently, Avalonia UI version 0.9 is available as nuget package and works so well that some developers are already using it for larger applications. The project is officially supported by the .NET Foundation and is completely open source and available on Github. Since I was allowed to implement WPF applications in different sizes and variants in the past, I took a closer look at the library and tried out how it “feels”. Let’s go!
Create and start project
Currently the easiest way to create a project is to use Visual Studio. You install the Avalonia UI Visual Studio Extension and then create a project of type “Avalonia Application” directly. If you start the created project afterwards, an empty window appears. The xaml code looks very familiar except for the xml-namespaces:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Button>Hello World!</Button>
</Window>
The structure of the programme has changed somewhat compared to WPF. Now there is startup code similar to ASP.NET Core, where you register an app class similar to the startup class:
class Program
{
// Main-Methode, welche eine Destkop Anwendung startet
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Anwendung konfigurieren.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToDebug();
}
Create and bind ViewModel
Not much has changed here. Without additional libraries, a class implements the INotifyPropertyChanged() interface, provides properties for the bindings and can react to commands by implementing the ICommand interface on the interface, such as pressing a button:
public class RelayCommand : ViewModel, ICommand
{
private readonly Action _execute;
public RelayCommand(Action execute)
{
_execute = execute;
}
public bool CanExecute(object parameter) => true;
public void Execute(object parameter) => _execute();
public event EventHandler CanExecuteChanged;
}
public class MainWindowViewModel : INotifyPropertyChanged
{
private int _counter;
public event PropertyChangedEventHandler PropertyChanged;
public MainWindowViewModel()
{
IncrementCommand = new RelayCommand(() => Counter++);
}
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public int Counter
{
get => _counter;
set
{
if (_counter == value) return;
_counter = value;
OnPropertyChanged();
}
}
public ICommand IncrementCommand { get; }
}
The corresponding code in MainWindow.xaml looks like this:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:Class="Test.MainWindow"
Title="TestWindow">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<Button Content="Increase Value" Command="{Binding IncrementCommand}" />
<TextBlock Text="{Binding Counter}" />
</StackPanel>
</Window>
And in the code-behind of the window you set the created ViewModel as DataContext:
namespace Test
{
public class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
DataContext = new MainWindowViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
If you now start the application and click the button, the value of the variable is increased by one each time and the interface is updated accordingly.
DataTemplates
If you want to implement the MVVM pattern well, I recommend DataTemplates. They are a good way to provide a view by data type for a ViewModel. All you have to do is provide a DataTemplate and specify the data type:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:Class="Test.MainWindow"
Title="TestWindow">
<Window.DataTemplates>
<DataTemplate DataType="{x:Type MainWindowViewModel">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<Button Content="Increase Value" Command="{Binding IncrementCommand}" />
<TextBlock Text="{Binding Counter}" />
</StackPanel>
</DataTemplate>
</Window.DataTemplates>
<ContentControl Content="{Binding}" />
</Window>
This is already one of the first differences to WPF: WPF offers to store any resources at any level. This works with Avalonia UI as well, but there are specialised collections to store DataTemplates. Another difference is that derived classes and interfaces are now also considered for type specifications. This of course gives you more freedom, but you have to be careful with the order of the definitions of the DataTemplates.
Styles
The styling of WPF Controls is a very powerful, but also very complex tool. I have had very good, but also very bad experiences with it, especially when customising third-party controls. Probably for these reasons, the developers of Avalonia UI decided against adopting the old styling system. The new one reminds more of CSS and is described with selectors. Here is a small example:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.Styles>
<Style Selector="TextBlock.h1">
<Setter Property="FontSize" Value="24"/>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
</Window.Styles>
<TextBlock Classes="h1">I'm a Heading!</TextBlock>
</Window>
Further differences to WPF
In the following, I would like to briefly present the – in my view – most exciting changes compared to WPF. I find many of them extremely refreshing and a good addition to the features that have been retained:
Simplified specification of grid row and column definitions
I was immediately very enthusiastic about it, because I use grids very often, and a short syntax allows you to write the definitions for the columns and rows together.
Usually:
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="1.5*" />
<ColumnDefinition Width="4*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!—Controls... -->
</Grid>
Avalonia UI:
<Grid ColumnDefinitions="100,1.5*,4*" RowDefinitions="Auto,Auto,Auto" Margin="4">
<!—Controls... -->
</Grid>
Bindings to methods instead of ICommand
It is now possible to directly bind a method with a parameter without implementing an ICommand. Ultimately, this can save a lot of code, especially if you implement ViewModelCommands separately from the ViewModels in your architecture and call public methods of the ViewModels in ICommand.Execute(). The public ViewModel methods can now be easily bound directly.
namespace Example
{
public class MainWindowViewModel : ViewModelBase
{
public void RunTheThing(string parameter)
{
// Code for executing the command here.
}
}
}
<Window xmlns="https://github.com/avaloniaui">
<Button Command="{Binding RunTheThing}" CommandParameter="Hello World">Do the thing!</Button>
</Window>
Definition of bindings
Bindings to a property of the parent control or to a named element are enabled by means of a shortened syntax:
<!—Binding an den Text der Textbox -->
<TextBox Name="other">
<TextBlock Text="{Binding #other.Text}"/>
<!—Binding an das Vatercontrol -->
<Border Tag="Hello World!">
<TextBlock Text="{Binding $parent.Tag}"/>
</Border>
<!—Binding an ein Vatercontrol eines bestimmten Typs -->
<Border Tag="Hello World!">
<Decorator>
<TextBlock Text="{Binding $parent[Border].Tag}"/>
</Decorator>
</Border>
My improvement wishes
Despite the many improvements, there are a few key points which, in my opinion, could still be improved:
Support of other IDEs: Currently, development with Avalonia UI is very easy with Visual Studio only. When working with Visual Studio Code or Rider, more code support would be helpful and desirable. There, some things have to be installed manually and that’s really a pity, especially since I wanted to develop with Rider under Linux and currently everything feels much more coherent under Windows.
Improve documentation: All in all, the documentation is more than sufficient to get an application up and running. Nevertheless I would like to get more information at one or the other place. For example, it is not obvious how DataTemplates are provided centrally in an application via ResourceDictionaries. Unfortunately, the solution described in the documentation did not work for me.
Third party support: Many WPF applications use external controls, e.g. from the company DevExpress or Telerik. All of these controls cannot be used, which generally restricts their use.
Conclusion
I am enthusiastic about Avalonia UI. After all, a lot of developers wanted WPF to be available for Mac and Linux as well – now it finally is. I didn’t have any problems during my experiments and in most cases I was able to develop a WPF application as usual, in the few remaining cases it just changed the behaviour.
Even if the trend is towards web applications, native desktop applications are still a good option and I can only warmly recommend to have a look at the Avalonia UI project. For me it was worth it.
Notes:
[1] Create smartphone applications with Flutter
Peter Friedland has published more articles in the t2informatik Blog, including
Peter Friedland
Software Consultant at t2informatik GmbH
Peter Friedland works at t2informatik GmbH as a software consultant. In various customer projects he develops innovative solutions in close cooperation with partners and local contact persons. And from time to time he also blogs.