Skip to content
Bruno Sonnino
Menu
  • Home
  • About
Menu

Making your WPF app cross platform with Avalonia UI

Posted on 19 November 2022

One advantage of the new .NET Core (now .NET) apps is that they are cross platform. Once you create an app, the same code will run on Windows, Mac or Linux with no change. But there are some exceptions to this: WPF or WinForms apps are only for Windows. You cannot create a WPF app and run it on Linux, for example.

If you want to create a cross platform app, you will have to use another technology, like Blazor or MAUI. Blazor UI is based on Razor components and it's not compatible with XAML, so it may be very difficult to convert. MAUI, on the other side, uses XAML and can be used to port your app to Mac, iOS or Android. But MAUI apps don't run on Linux. If you want a Linux app, this is not the way. You can use Uno Platform (see my article here), but this is a Web technology (it uses WebAssembly in the browser), and you won't be able to create a desktop app (yes, you can create a PWA, but technically, it's not a desktop app). The other option is to use Avalonia UI.

In this article we will show how to convert an existing WPF app to Avalonia UI. We will use the project described in my MVVM Community toolkit 8.0 article. This project has some interesting features to explore:

  • It uses a .NET Standard library for the repository
  • It has a Datagrid to display the data
  • It uses the MVVM pattern and the MVVM Community toolkit NuGet package

Avalonia UI is an open source cross platform framework for .NET to develop cross platform apps using XAML. To use it, you need to install the Avalonia project templates with:

dotnet new install Avalonia.Templates

Once you do it, you can create a new basic project with:

dotnet new avalonia.app -o BasicApp
cd BasicApp
dotnet run

When you run it, you will see a basic app:

The generated code has these differences:

  • The extension for the view files is axaml instead of xaml (and the code behind extension is axaml.cs)
  • The default namespace for the view files is https://github.com/avaloniaui instead of http://schemas.microsoft.com/winfx/2006/xaml/presentation
  • The project includes the Avalonia, Avalonia.Desktop, Avalonia.Diagnostics and XamlNameReferenceGenerator NuGet packages
  • The project targets net6.0 (or net7.0), instead of net6.0-windows
  • There is no need to include the UseWPF clause in the project file
  • The Program.cs file and Main method are explicit
  • There is some initialization code in App.axaml.cs

Apart from that, designing the UI and the C# code aren't much different from the standard WPF app.

For this app, we will use Visual Studio 2022 and the Avalonia Extension. This extension will provide all templates and a designer for the views. If you don't want to use Visual Studio, you can use VS Code, but you won't have the visual designer. In Visual Studio, go to Extensions/Manage Extensions and install the Avalonia for Visual Studio 2022 extension:

Let's start converting our app. We have two approaches, here: convert our app in place, making changes in the files as needed, or create a new basic Avalonia project and add the features incrementally. I prefer to use the second approach, in this case all the basic infrastructure is already set and we can make sure that things are running while we are adding the features. In the in place conversion, it's an all-or-nothing, and at the end we may not have any clue of what we've missed, in case it doesn't run.

The first step is to clone our app from https://github.com/bsonnino/MVVMToolkit8.git. Then, we will create our app in Visual Studio:

That will create a new basic app. If you open MainWindow.axaml, you will see the code and the visual designer:

Let's start converting our app. The first step is to add the two NuGet packages, CommunityToolkit.Mvvm and Microsoft.Extensions.DependencyInjection.

Then, copy the CustomerLib folder with all files to the folder of the Avalonia solution. We will use this project as is, as it's a .NET Standard project and it can be used by Avalonia unchanged. In the solution explorer, add an existing project and select the CustomerLib.csproj file. That will add the lib to our solution. In the main project, add a project reference and add the CustomerLib project:

Then, copy the ViewModel folder to the project folder, it will appear in the solution explorer. Open MainViewModel.cs, you will see an error in ColletionViewSource:

That's because the CollectionViewSource class doesn't exist in Avalonia and we need to replace it with this code:

private readonly ICustomerRepository _customerRepository;
private Func<Customer, bool> _filter = c => true;
public IEnumerable<Customer> Customers => _customerRepository.Customers.Where(_filter);

[RelayCommand]
private void Search(string textToSearch)
{
    if (!string.IsNullOrWhiteSpace(textToSearch))
        _filter = c => ((Customer)c).Country.ToLower().Contains(textToSearch.ToLower());
    else
        _filter = c => true;
    OnPropertyChanged(nameof(Customers));
}

Instead of using the WPF CollectionViewSource class, we are creating our filter and using it before displaying the data. Just to check, we can copy the Test project to the solution folder, add it to the current solution and run the tests to check. For that, we must do the following changes:

  • Change the Target Framework in the csproj file to .net6.0
  • Change the reference for the main project to MvvmAvalonia

Once we do that, we can compile the project, but we get the errors for the CollectionViewSource. For that, we must change the tests to:

[TestMethod]
public void SearchCommand_WithText_ShouldSetFilter()
{
    var customers = new List<Customer>
    {
        new Customer { Country = "a"},
        new Customer { Country = "text"},
        new Customer { Country = "b"},
        new Customer { Country = "texta"},
        new Customer { Country = "a"},
        new Customer { Country = "b"},
    };
    var repository = A.Fake<ICustomerRepository>();
    A.CallTo(() => repository.Customers).Returns(customers);
    var vm = new MainViewModel(repository);
    vm.SearchCommand.Execute("text");
    vm.Customers.Count().Should().Be(2);
}

[TestMethod]
public void SearchCommand_WithoutText_ShouldSetFilter()
{
    var customers = new List<Customer>
    {
        new Customer { Country = "a"},
        new Customer { Country = "text"},
        new Customer { Country = "b"},
        new Customer { Country = "texta"},
        new Customer { Country = "a"},
        new Customer { Country = "b"},
    };
    var repository = A.Fake<ICustomerRepository>();
    A.CallTo(() => repository.Customers).Returns(customers);
    var vm = new MainViewModel(repository);
    vm.SearchCommand.Execute("");
    vm.Customers.Count().Should().Be(6);
}

Now, when we run the tests, they all pass and we can continue. We will start adding the UI to the main window:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="40" />
        <RowDefinition Height="*" />
        <RowDefinition Height="2*" />
        <RowDefinition Height="50" />
    </Grid.RowDefinitions>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Country" VerticalAlignment="Center" Margin="5"/>
        <TextBox x:Name="searchText" VerticalAlignment="Center" Margin="5,3" Width="250" Height="25" VerticalContentAlignment="Center"/>
        <Button x:Name="PesqBtn" Content="Find" Width="75" Height="25" Margin="10,5" VerticalAlignment="Center"
                Command="{Binding SearchCommand}" CommandParameter="{Binding ElementName=searchText,Path=Text}"/>
    </StackPanel>
    <DataGrid AutoGenerateColumns="False" x:Name="master" CanUserAddRows="False" CanUserDeleteRows="True" Grid.Row="1"
              ItemsSource="{Binding Customers}" SelectedItem="{Binding SelectedCustomer, Mode=TwoWay}">
        <DataGrid.Columns>
            <DataGridTextColumn x:Name="customerIDColumn" Binding="{Binding Path=CustomerId}" Header="Customer ID" Width="60" />
            <DataGridTextColumn x:Name="companyNameColumn" Binding="{Binding Path=CompanyName}" Header="Company Name" Width="160" />
            <DataGridTextColumn x:Name="contactNameColumn" Binding="{Binding Path=ContactName}" Header="Contact Name" Width="160" />
            <DataGridTextColumn x:Name="contactTitleColumn" Binding="{Binding Path=ContactTitle}" Header="Contact Title" Width="60" />
            <DataGridTextColumn x:Name="addressColumn" Binding="{Binding Path=Address}" Header="Address" Width="130" />
            <DataGridTextColumn x:Name="cityColumn" Binding="{Binding Path=City}" Header="City" Width="60" />
            <DataGridTextColumn x:Name="regionColumn" Binding="{Binding Path=Region}" Header="Region" Width="40" />
            <DataGridTextColumn x:Name="postalCodeColumn" Binding="{Binding Path=PostalCode}" Header="Postal Code" Width="50" />
            <DataGridTextColumn x:Name="countryColumn" Binding="{Binding Path=Country}" Header="Country" Width="80" />
            <DataGridTextColumn x:Name="faxColumn" Binding="{Binding Path=Fax}" Header="Fax" Width="100" />
            <DataGridTextColumn x:Name="phoneColumn" Binding="{Binding Path=Phone}" Header="Phone" Width="100" />
        </DataGrid.Columns>
    </DataGrid>
    <customerApp:Detail Grid.Row="2" DataContext="{Binding SelectedCustomer}" Margin="5" x:Name="detail"/>
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="5" Grid.Row="3">
        <Button Width="75" Height="25" Margin="5" Content="Add" Command="{Binding AddCommand}" />
        <Button Width="75" Height="25" Margin="5" Content="Remove" Command="{Binding RemoveCommand}" />
        <Button Width="75" Height="25" Margin="5" Content="Save" Command="{Binding SaveCommand}" />
    </StackPanel>
</Grid>

There is an error with the DataGrid. That's because we need to add the package Avalonia.Controls.Datagrid. Once we add that, we can see some other errors:

  • The ItemsSource property has been changed to Items
  • The columns don't have the Name field and should be removed
  • The CanUserAddRows and CanUserDeleteRows do not exist and should be removed
  • We should add the themes for the DataGrid in App.axaml:
<Application.Styles>
    <StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
    <StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
    <StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Default.xaml"/>
</Application.Styles>

We can also see that this code is missing the Detail control. Add to the project a new item of type UserControl (Avalonia) and add the content from the original project:

<Grid>
    <Grid Name="grid1" >
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Label Content="Customer Id:" Grid.Column="0" Grid.Row="0"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="0"   Margin="3" Name="customerIdTextBox" Text="{Binding Path=CustomerId, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Company Name:" Grid.Column="0" Grid.Row="1"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="1"   Margin="3" Name="companyNameTextBox" Text="{Binding Path=CompanyName, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Contact Name:" Grid.Column="0" Grid.Row="2"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="2"   Margin="3" Name="contactNameTextBox" Text="{Binding Path=ContactName, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Contact Title:" Grid.Column="0" Grid.Row="3"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="3"   Margin="3" Name="contactTitleTextBox" Text="{Binding Path=ContactTitle, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Address:" Grid.Column="0" Grid.Row="4" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="4" Margin="3" Name="addressTextBox" Text="{Binding Path=Address, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
        <Label Content="City:" Grid.Column="0" Grid.Row="5"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="5"   Margin="3" Name="cityTextBox" Text="{Binding Path=City, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Postal Code:" Grid.Column="0" Grid.Row="6"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="6"   Margin="3" Name="postalCodeTextBox" Text="{Binding Path=PostalCode, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Region:" Grid.Column="0" Grid.Row="7"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="7"   Margin="3" Name="regionTextBox" Text="{Binding Path=Region, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Country:" Grid.Column="0" Grid.Row="8"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="8"   Margin="3" Name="countryTextBox" Text="{Binding Path=Country, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Phone:" Grid.Column="0" Grid.Row="9"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="9"   Margin="3" Name="phoneTextBox" Text="{Binding Path=Phone, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Fax:" Grid.Column="0" Grid.Row="10"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="10"   Margin="3" Name="faxTextBox" Text="{Binding Path=Fax, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
    </Grid>
</Grid>

We must remove the , ValidatesOnExceptions=true, NotifyOnValidationError=true from the code, as it's not available in Avalonia. Then, we should add the correct using clause in the main xaml:

xmlns:customerApp="using:MvvmAvalonia"

Once we do that and we run, we can see the UI (but not the data):

For the data, we must add the configuration for the services, in App.axaml.cs:

public partial class App : Application
{
    public override void Initialize()
    {
        AvaloniaXamlLoader.Load(this);
        Services = ConfigureServices();
    }

    public override void OnFrameworkInitializationCompleted()
    {
        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
        {
            desktop.MainWindow = new MainWindow();
        }

        base.OnFrameworkInitializationCompleted();
    }

    public new static App Current => (App)Application.Current;

    public IServiceProvider Services { get; private set; }

    private static IServiceProvider ConfigureServices()
    {
        var services = new ServiceCollection();

        services.AddSingleton<ICustomerRepository, CustomerRepository>();
        services.AddSingleton<MainViewModel>();

        return services.BuildServiceProvider();
    }

    public MainViewModel MainVM => Services.GetService<MainViewModel>();
}

Then, we must set the DataContext on MainWindow.axaml.cs:

public MainWindow()
{
    InitializeComponent();
    DataContext = App.Current.MainVM;
}

Now, when we run the code, we can see it runs fine:

We've ported our WPF project to Avalonia, now it's ready to be run on Linux. We'll use WSLg (Windows Subsystem for Linux GUI) to run the app. Just open a Linux tab on terminal and cd to the project directory (the drive is mounted on /mnt/drive, like /mnt/c) and run the app with dotnet run. You should have something like this:

As you can see, porting a WPF app to Avalonia requires some changes, but most of the code is completely portable. If you want to ease the process, you can move the non-UI code to .NET Standard libraries and use them as-is.

All the source code for the project is at https://github.com/bsonnino/MvvmAvalonia

2 thoughts on “Making your WPF app cross platform with Avalonia UI”

  1. Shannon John says:
    21 August 2024 at 20:57

    If `NotifyOnValidationError=true ` is a must, how to handle it in Avalonia?

    Reply
    1. sonnino@gmail.com says:
      24 August 2024 at 17:52

      You can check this: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation

      Reply

Leave a Reply Cancel reply

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

  • May 2025
  • December 2024
  • October 2024
  • August 2024
  • July 2024
  • June 2024
  • November 2023
  • October 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • November 2022
  • October 2022
  • September 2022
  • August 2022
  • June 2022
  • April 2022
  • March 2022
  • February 2022
  • January 2022
  • July 2021
  • June 2021
  • May 2021
  • April 2021
  • March 2021
  • February 2021
  • January 2021
  • December 2020
  • October 2020
  • September 2020
  • April 2020
  • March 2020
  • January 2020
  • November 2019
  • September 2019
  • August 2019
  • July 2019
  • June 2019
  • April 2019
  • March 2019
  • February 2019
  • January 2019
  • December 2018
  • November 2018
  • October 2018
  • September 2018
  • August 2018
  • July 2018
  • June 2018
  • May 2018
  • November 2017
  • October 2017
  • September 2017
  • August 2017
  • June 2017
  • May 2017
  • March 2017
  • February 2017
  • January 2017
  • December 2016
  • November 2016
  • October 2016
  • September 2016
  • August 2016
  • July 2016
  • June 2016
  • May 2016
  • April 2016
  • March 2016
  • February 2016
  • October 2015
  • August 2013
  • May 2013
  • February 2012
  • January 2012
  • April 2011
  • March 2011
  • December 2010
  • November 2009
  • June 2009
  • April 2009
  • March 2009
  • February 2009
  • January 2009
  • December 2008
  • November 2008
  • October 2008
  • July 2008
  • March 2008
  • February 2008
  • January 2008
  • December 2007
  • November 2007
  • October 2007
  • September 2007
  • August 2007
  • July 2007
  • Development
  • English
  • Português
  • Uncategorized
  • Windows

.NET AI Algorithms asp.NET Backup C# Debugging Delphi Dependency Injection Desktop Bridge Desktop icons Entity Framework JSON Linq Mef Minimal API MVVM NTFS Open Source OpenXML OzCode PowerShell Sensors Silverlight Source Code Generators sql server Surface Dial Testing Tools TypeScript UI Unit Testing UWP Visual Studio VS Code WCF WebView2 WinAppSDK Windows Windows 10 Windows Forms Windows Phone WPF XAML Zip

  • Entries RSS
  • Comments RSS
©2025 Bruno Sonnino | Design: Newspaperly WordPress Theme