Skip to main content

Avalonia UI

Setup

Devcontainer

Dockerfile
FROM mcr.microsoft.com/devcontainers/dotnet:0-6.0

RUN sudo apt-get -y update && \
sudo apt-get -y install \
libx11-dev \
libice6 \
libsm6 \
libfontconfig1

Install Template

dotnet new install Avalonia.Templates

Create new Application

dotnet new avalonia.app -o MyApp

Run Application

dotnet run

Microsoft Visual Studio 2022 Plugin

Basics

Window

MainWindow.axml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="AvaloniaApplication1.MainWindow"
Title="Window with Button"
WindowStartupLocation="CenterScreen"
Icon="/Assets/logo.ico"
Width="250" Height="100"
CanResize="False"
SizeToContent="WidthAndHeight">
</Window>

Button

<Button Name="button" Click="onClick">Click Me!</Button>
MainWindow.cs
public void onClick(object sender, RoutedEventArgs e) {
var button = (Button)sender;
button.Content = "Hello, Avalonia!";
}

Datagrid

<DataGrid Name="DataGrid"
DoubleTapped="OnDoubleTapped"
Items="{Binding MyItems}"
SelectionMode="Single"
AutoGenerateColumns="False"
CanUserReorderColumns="False"
CanUserSortColumns="False" >
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Header="Edit" Click="OnEditButtonClicked"/>
<MenuItem Header="Delete" Click="OnDeleteButtonClicked"/>
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridTextColumn Header="#" IsReadOnly="True" Binding="{Binding Index}" />
<DataGridTextColumn Header="Name" IsReadOnly="True" Binding="{Binding Name}" />
</DataGrid.Columns>
</DataGrid>
App.axml
<Application.Styles>
<FluentTheme Mode="Light"/>
<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>
  • Model
public class MyItem {
public int Index { get; set; }
public string Name { get; set; }
}

public ObservableCollection<MyItem> MyItems { get; } = new ObservableCollection<MyItem>();
  • Item Double Tapped
private async void OnDoubleTapped(object sender, RoutedEventArgs e) {
var dataGrid = (DataGrid)sender;
var model = (MyItem)dataGrid.SelectedItem;
}
  • Context Menu Clicked
private async void OnEditButtonClicked(object sender, RoutedEventArgs e) {
var dataGrid = (DataGrid)sender;
var model = (MyItem)dataGrid.SelectedItem;
}

Show Dialog

public static Window GetWindowFromUserControl(UserControl userControl) {
IControl current = userControl;
while (current != null && !(current is Window))
{
current = current.Parent;
}
return current as Window;
}

await editItemWindow.ShowDialog(GetWindowFromUserControl(this));

Database

- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.Sqlite

Create a model

namespace App.Models
{
public class MyItem
{
public MyItem(int index, string name)
{
Id = index;
Name = name;
}
[Key] // optional if field name is Id or MyItemId
public int Id { get; set; }

[Column("Name", TypeName="ntext")] // optional
[MaxLength(20)] // optional
public string Name { get; set; }

[NotMapped] // optional Will not be saved inside the DB
public bool IsConnected { get; set; }
}
}

Create Database

Database.cs
namespace App.Services
{
public class Database : DbContext
{
public DbSet<MyItem> MyItems { get; set; }
public Database()
{
Database.EnsureCreated();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Filename=data.db");
}
}
}

Create

using (var db = new Database()) {
var my_item = new MyItem(1, "New Item 1");
db.Add(my_item);
db.SaveChanges();
}

Read

using (var db = new Database()) {
// Get all
var my_items = db.MyItems.ToList();

// With Filter
var filtered = db.MyItems
.Where(m => m.Name == "Bernd")
.ToList();

// Load Subitems
var item = db.MyItems
.Where(m => m.Name == "Bernd")
.Include(m => m.SubItem)
.FirstOrDefault();
}

Update

using (var db = new Database()) {
db.Update(my_item);

IList<MyItem> modifiedItems = new List<MyItem>() {
modifiedItem1,
modifiedItem2,
modifiedItem3
};
db.UpdateRange(modifiedItems);

db.SaveChanges();
}

Delete

using (var db = new Database()) {
db.Remove(model);
db.SaveChanges();
}

Show Changed

DisplayStates(context.ChangeTracker.Entries());

private static void DisplayStates(IEnumerable<EntityEntry> entries) {
foreach (var entry in entries) {
Console.WriteLine($"Entity: {entry.Entity.GetType().Name},
State: {entry.State.ToString()} ");
}
}

Enable Logging

public class Database : DbContext
{
public static readonly ILoggerFactory loggerFactory = new LoggerFactory(new[] {
new ConsoleLoggerProvider((_, __) => true, true)
});

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLoggerFactory(loggerFactory) //tie-up DbContext with LoggerFactory object
.EnableSensitiveDataLogging()
/* .. */
}
}

Localization

  • Dependencies
Newtonsoft.Json
  • Usage
<Window
<!-- [..] -->
xmlns:i18n="clr-namespace:App">

<Button>"{i18n:Localize ButtonCaption}"</Button>

</Window>
  • Create Translation File
en-US.json
{
"ButtonCaption": "Click me!"
}
ja-JP.json
{
"ButtonCaption": "私をクリックしてください!"
}
  • Load Language
App.axml.cs
public override void OnFrameworkInitializationCompleted() {
/* [..] */
Localizer.Instance.LoadLanguage("en-US");
/* [..] */
}
Localizer/LocalizeExtension.cs
namespace App
{
using System;
using Avalonia.Data;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.MarkupExtensions;

public class LocalizeExtension : MarkupExtension
{
public LocalizeExtension(string key)
{
this.Key = key;
}

public string Key { get; set; }

public string Context { get; set; }

public override object ProvideValue(IServiceProvider serviceProvider)
{
var keyToUse = Key;
if (!string.IsNullOrWhiteSpace(Context))
keyToUse = $"{Context}/{Key}";

var binding = new ReflectionBindingExtension($"[{keyToUse}]")
{
Mode = BindingMode.OneWay,
Source = Localizer.Instance,
};

return binding.ProvideValue(serviceProvider);
}
}
}
Localizer/Localizer.cs
    using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text;
using static System.Net.WebRequestMethods;

public class Localizer : INotifyPropertyChanged
{
private const string IndexerName = "Item";
private const string IndexerArrayName = "Item[]";
private Dictionary<string, string> m_Strings = null;

public bool LoadLanguage(string language)
{
Language = language;
var assets = AvaloniaLocator.Current.GetService<IAssetLoader>();
var languageFile = $"avares://Toolkit/Assets/i18n/{language}.json";
Debug.Print($"Loading language file: {languageFile}");
Uri uri = new Uri(languageFile);
if (assets.Exists(uri))
{
using (StreamReader sr = new StreamReader(assets.Open(uri), Encoding.UTF8))
{
m_Strings = JsonConvert.DeserializeObject<Dictionary<string, string>>(sr.ReadToEnd());
}
Invalidate();

return true;
}
return false;
} // LoadLanguage

public string Language { get; private set; }

public string this[string key]
{
get
{
string res;
if (m_Strings != null && m_Strings.TryGetValue(key, out res))
return res.Replace("\\n", "\n");

return $"{Language}:{key}";
}
}

public static Localizer Instance { get; set; } = new Localizer();
public event PropertyChangedEventHandler PropertyChanged;

public void Invalidate()
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(IndexerName));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(IndexerArrayName));
}
}
}

StackPanel

<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">

</StackPanel>

Grid

<Grid ColumnDefinitions="Auto, Auto, *, Auto" Margin="8" Background="Orange" Height="40">
<Rectangle Grid.Column="0" Fill="Red" Width="40"></Rectangle>
<Rectangle Grid.Column="1" Fill="Blue" Width="40"></Rectangle>
<Rectangle Grid.Column="2" Fill="Green" Width="40"></Rectangle>
<Rectangle Grid.Column="3" Fill="Yellow" Width="40"></Rectangle>
</Grid>
<Grid RowDefinitions="Auto, Auto, *, Auto" ShowGridLines="true" Background="Orange" Height="40">
<Rectangle Grid.Row="0" Fill="Red" Width="40"></Rectangle>
<Rectangle Grid.Row="1" Fill="Blue" Width="40"></Rectangle>
<Rectangle Grid.Row="2" Fill="Green" Width="40"></Rectangle>
<Rectangle Grid.Row="3" Fill="Yellow" Width="40"></Rectangle>
</Grid>
<Grid RowDefinitions="Auto, *" ColumnDefinitions="Auto, *" Background="Orange" Height="40">
<Rectangle Grid.Row="0" Grid.ColumnSpan="2" Fill="Red" Width="40"></Rectangle>
<Rectangle Grid.Row="1" Fill="Blue" Width="40"></Rectangle>
<Rectangle Grid.Row="2" Fill="Green" Width="40"></Rectangle>
<Rectangle Grid.Row="3" Fill="Yellow" Width="40"></Rectangle>
</Grid>

Label

<Label Foreground="White" FontSize="20" FontWeight="Black">My Label</Label>

Syle

  • Global Style
<Window.Styles>
<Style Selector="Button">
<Setter Property="FontSize" Value="20"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Background" Value="#7f98c7"/>
</Style>
</Window.Styles>
  • Local Style
<StackPanel>
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="FontSize" Value="20"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Background" Value="#7f98c7"/>
</Style>
</Window.Styles>
</StackPanel>
  • Style a class
<Window.Styles>
<Style Selector="Rectangle.red">
<Setter Property="Height" Value="100"/>
</Style>
</Window.Styles>

<Rectangle Classes="red"/>
  • Include Style from file
<Window.Styles>
<StyleInclude Source="/src/App/Views/MyView.xaml"/>
</Window.Styles>

Inside the file: