Styles in AvaloniaUI

ingvarx

Ingvar

Posted on August 29, 2020

Styles in AvaloniaUI

Hello guys! In this post I gonna explain how to stylish your AvaloniaUI app according to Avalonia best practices. As always I gonna use my app Camelot as an example.

Difference from WPF

In WPF styles are added in separate xaml files using xaml syntax like here:

<Style TargetType="TextBlock">
    <Setter Property="Foreground" Value="Gray" />
    <Setter Property="FontSize" Value="24" />
</Style>

Styles in WPF target specific element types. If you want to set different styles on different views for same control you should include different styles files into your view. Avalonia provides more flexible approach here. It allows to add classes (similar to CSS) to your element and specify different styles for classes. Also it has selectors support (also similar to CSS approach) that allows you to avoid using triggers and write your styles in similar way.

Styling an element

In Avalonia it's also recommended to separate view and styles files. Let's start from view example:

<TextBlock Classes="dataGridColumnHeaderTextBlock" Text="{x:Static p:Resources.Extension}" />

In this example I added a TextBox. Note that I don't have any inline styles here. The only thing (related to styling) that was set is Classes="dataGridColumnHeaderTextBlock". This property adds class to element. I use this class for adding styles:

<Style Selector="TextBlock.dataGridColumnHeaderTextBlock">
    <Setter Property="Foreground" Value="{DynamicResource InactiveTabForegroundBrush}" />
</Style>

In selector field I add css-like selector for my element. It always should contain element type (otherwise it's not possible to determine set of properties available for customizing) and optionally has classes, inner elements and events. For example, for hover event you will have something like this:

<Style Selector="Button.tabButton:pointerover">
    <Setter Property="Background" Value="{DynamicResource TabButtonHoverBrush}" />
</Style>

:pointerover sets styles for button if only pointer is above this button, looks simple, right?

I don't want to describe all available selectors here because they are listed in official documentation. It's available here: Avalonia official styling docs

Creating app theme

After adding basic styling you would probably think about adding themes support. For example, some apps have both dark and light themes and allow user to choose between them. I will explain how to achieve this.

I extracted all theme-related styles into single file. I didn't add it to App.xaml like it's done by default but I load it dynamically on app start. For this purpose I modified my App.xaml.cs and added themes loading:

public override void Initialize()
{
    AvaloniaXamlLoader.Load(this);
    LoadTheme();
}

private void LoadTheme()
{
    Styles.Add(new DarkTheme());
}

For now I have only one dark theme so I simply add dark theme to list of app styles. In future I gonna light theme as well so in this method I will read value from config and load theme based on this value. Pretty simple. Also I had to add cs file for using styles from code:

using Avalonia.Markup.Xaml;
using AvaloniaStyles = Avalonia.Styling.Styles;

namespace Camelot.Styles.Themes
{
    public class DarkTheme : AvaloniaStyles
    {
        public DarkTheme() => AvaloniaXamlLoader.Load(this);
    }
}

Themes best practices: files structure

For better using themes I used following solution structure:
1) I completely separated view and styles, there are no styling in views except basic stuff like margin etc.
2) I moved themes independent styles into their own files
3) I created a directory for each style and added theme file there. It could include other style files too if needed
4) Into theme file I put colors and basic options values, so theme file modifies color scheme and nothing more

Themes best practices: colors

Coloring an Avalonia app is definitely the hardest part of styling your project. There are no good docs regarding this so I had to learn on my own mistakes here πŸ˜ƒ

Initially I tried WPF-like way. I specified brushes in common styling files and tried to override them in themes file. I called my brushes like TransparentButtonBorderBrush so it was obvious where is it used. But sometimes it didn't work. Avalonia continued to show me it's default control colors! It ignored all my settings even if I added them into view itself. It was really confusing. Where does it get those weird colors? Obviously it loaded them from somewhere and as longer I didn't have anything except my custom and Avalonia default styles in my app, I decided to look into default styles and check how they work. I expected similar approach there - different brushes per each control etc. I was wrong.

Initially I had Avalonia dark theme used which is available here. I opened that file and found out that it has only colors/brushes specified and nothing else! I opened control styles (available here) and realized that all Avalonia controls has been styled in following way:
1) In control file set of brushes for control is specified in both view code and styles code
2) Controls brushes are reused across different controls and all listed in theme file

I understood that I can style my app by modifying default Avalonia brushes. For example here is part of my dark theme file:

<SolidColorBrush x:Key="ThemeBackgroundBrush" Color="{DynamicResource MainBackgroundColor}" />
<SolidColorBrush x:Key="ThemeForegroundBrush" Color="{DynamicResource WhiteColor}" Opacity="0.8" />
<SolidColorBrush x:Key="HighlightBrush" Color="{DynamicResource OrangeColor}" />
<SolidColorBrush x:Key="HighlightForegroundBrush" Color="{DynamicResource WhiteColor}" Opacity="0.9" />
<SolidColorBrush x:Key="ThemeAccentBrush4" Color="{DynamicResource OrangeColor}" />
<SolidColorBrush x:Key="ThemeAccentBrush3" Color="{DynamicResource BrightOrangeColor}" />
<SolidColorBrush x:Key="ThemeControlLowBrush" Color="{DynamicResource BlackColor}" Opacity="0.2" />

If I need to override some styles I used same selectors like default style has, for example:

<Style Selector="ToggleButton:checked /template/ ContentPresenter">
    <Setter Property="Background" Value="Transparent" />
</Style>

My algorithm of styling is following:
1) Check default brush for control that I want to style
2) If it's possible override default brush with your custom value
3) If it's not possible write new selector that will add color to control inner part that needs to be styled. This style could use another default brush or your custom one.

This algorithm allowed me to style whole app exactly as I wished. Screenshots on different platforms are available here

I wanna say that AvaloniaUI styling system it neat and flexible. It completely covers all needs in css-like manner. It's a bit hard to understand initially but still easy even for beginners. What do you think about styles in Avalonia? Let me know in comments πŸ˜ƒ

πŸ’– πŸ’ͺ πŸ™… 🚩
ingvarx
Ingvar

Posted on August 29, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related