1. Introduction
2. Avalonia UI
3. Dependency Injection
4. MVVM Design
5. Dialogs and Tools
6. Unit Testing
7. Reactive
8. Deployment
9. Assembly Trimming
10. Multiple Environments
Avalonia is a modern WPF-like UI that works on all OS: Windows, Linux, MacOS, iOS, Android and even Web Assembly! It is mature enough for commercial use.
First, install the plugin in your IDE. Instructions here.
- Main website
- GitHub source code
- Documentation
- FluentAvalonia - Better UI
- AwesomeAvalonia - List of 3rd party tools
The documentation is very well-written. Take the time to read the various sections. It is very similar to WPF, with some differences that are great improvements. It's a lot easier to translate WPF to Avalonia than the other way around. For undocumented classes, you can often refer to WPF and UWP documentation that will be very similar.
For more help:
Do not pay attention to the code-behind aspect; that will be covered in the MVVM section. What you need to learn is the XAML language and creating styles and resources. Practice creating some great UIs that do nothing.
One limitation is that you get no visual editor and instead only get a visual preview. This forces you to write all the XAML manually. As I found out, this is a good thing. My WPF code was all built with absolute coordinates and large margins to position the controls. That's not how layouts should be done. StackPanel
is your best friend for layouts, so use that instead.
Also use Grid
with rows and columns. Margins should be used only to control spacing between controls, not to position the controls in the window.
<Grid Margin="10,6,10,10" ColumnDefinitions="150,*" RowDefinitions="*,40">
Rewrite your auto-generated WPF code with StackPanel and grids and you'll get much cleaner code.
FluentAvalonia provides better styles and more controls, so you may want to use that too. There are no build-in MessageBox in Avalonia, and FluentAvalonia provides that.
Download their source code and run their sample app to view all the controls in action. The sample application also allows you to browse all style resources and active colors, and is thus a very useful tool.
Svg.Skia is an option for SVG icons; but it cannot adapt the icon color dynamically.
FluentAvalonia provides SymbolIcon
with build-in icons for most of your needs. Download their source code and run their sample app to view them.
<flu:SymbolIcon Symbol="Add" FontSize="20" />
If you need icons that are not in SymbolIcon, there is a way to convert SVG to a Path, but that can get complicated, and Path requires you to set a fixed Brush so it won't adapt to the button Forecolor.
I find it easier to create a font containing the custom icons.
Here's a simple guide on creating an icons font using FontForge.
My button then looks like this
<Button Classes="icon" Width="35" Content="I" />
With icon
defined as
<Style Selector="Button.icon">
<Setter Property="FontFamily" Value="avares://Common.Avalonia.App/Styles/Icons.otf#" />
<Setter Property="FontSize" Value="17" />
</Style>
Here are some sample Views created using these principles. I've created custom Styles and Resources that are registered in App.xaml.
The app then works great with Light and Dark themes that can be set on-the-fly. The styles add a background gradient, changes the style of buttons to look like the FluentAvalonia 'accent' style with 35% opacity, alters the ListBox so that double-click covers the whole item area, and adjusts a few details for my needs.
Setting and altering styles can be difficult, as you don't know how controls are internally structured.
First, run your app and press F12 to open the Avalonia DevTools. Put the tools window on the right-side of the screen and your app on the left. Click on the Visual Tree tab, hover over the control that you want to analyze, and press CTRL+SHIFT. It will browse to that element in the visual tree and you can see what attributes are in effect.
Also, you can browse the source styles, Avalonia.Themes.Fluent if using the built-in styles, or FluentAvalonia.
Finally, here's what my code-behind looks like. I want to keep it that way. You might wonder how I achieve a fully-functional app with no code-behind whatsoever. I'll cover that later.
public partial class MainView : CommonWindow<MainViewModel>
{
protected override void Initialize() => AvaloniaXamlLoader.Load(this);
}
In the XAML, 'd' prefix allows setting design-time properties. This allows setting a DataContext for the designer. We'll create the ViewModelLocator class in the next section.
d:DataContext="{x:Static local:ViewModelLocator.Main}"
There are two project templates available: desktop-only or cross-platform. Instructions here and here.
Make sure that you target the right framework, that Nullables are enabled, and that you use the latest language features, by having this in your csproj
.
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>default</LangVersion>
For code styles, if you want to use the most recommended coding practices, it is safe to grab the .editorconfig
file from the .NET Roslyn Compiler project here. Place it in your solution folder to be available for all projects.
Also make sure to create a file Usings.cs
that contains namespaces that you commonly use.
global using System;
global using System.Collections;
global using System.Collections.Generic;
global using System.Threading.Tasks;
global using RxCommandUnit = ReactiveUI.ReactiveCommand<System.Reactive.Unit, System.Reactive.Unit>;
global using static System.FormattableString;
Put this in App.xaml.cs
As of writing this, there's a bug where XAML namespaces of custom libraries do not get recognized by the designer. You must use GC.KeepAlive
on any class within such libraries to solve this.
Also, I found out that when using the designer, there is no point in initializing your MainView and application; you can skip app initialization and the preview will show faster.
public override async void OnFrameworkInitializationCompleted()
{
#if DEBUG
// Required by Avalonia XAML editor to recognize custom XAML namespaces. Until they fix the problem.
GC.KeepAlive(typeof(SvgImage));
GC.KeepAlive(typeof(EventTriggerBehavior));
if (Design.IsDesignMode)
{
base.OnFrameworkInitializationCompleted();
return;
}
#endif