avalonia について
avalonia は、Avalonia 12 でデスクトップアプリを構築するためのガイダンスを提供するスキルです。.NET 開発者向けに、コンパイル済みバインディング、クロスプラットフォームサービス、再利用可能なコントロール、トリミングや Native AOT の注意点を扱います。
主な機能
- 01Avalonia 12 のコンパイル済みバインディング指針
- 02トリミング対応のビューとビューモデルのマッピング
- 03再利用可能なコントロールとテーマ
- 04TopLevel によるクロスプラットフォームサービス
- 05Native AOT とパッケージングの指針
活用シーン
- 01クロスプラットフォームの Avalonia デスクトップアプリ開発
- 02再利用・再スタイル可能なコントロールの作成
- 03トリミングまたは Native AOT 向けのアプリ準備
- 04ファイル選択やクリップボード機能の追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill avaloniaインストール後、Claude Code で/avaloniaを入力して読み込まれたか確認します。
詳細
Avalonia UI (v12)
Avalonia 12.1.x で検証済みです。Avalonia 12 には .NET 8 以降(.NET 10 を推奨)が必要で、コンパイル済みバインディングがデフォルトになっています。ビューモデルのパターンには、このスキルを desktop-mvvm と組み合わせてください。
基本原則
- あらゆる場所でコンパイル済みバインディングを使う — Avalonia 12 では、XAML の
{Binding}はデフォルトでコンパイルされます(AvaloniaUseCompiledBindingsByDefault=true)。すべてのビューとDataTemplateにx:DataTypeが必要です。バインディングの誤記は、何も表示されないだけで済むのではなくビルドエラーになり、リフレクションも使わないため、トリミングと AOT が引き続き機能します。 - ビューとビューモデルはコンパイル時に対応付ける — 型付きの
DataTemplateまたは辞書ベースのViewLocatorを使用します。テンプレートのリフレクションを使うViewLocator(Type.GetType+Activator.CreateInstance)は、トリミング時に機能しなくなります。 - 合成には
UserControl、再利用可能なコントロールにはTemplatedControlを使う — ルックレスなテンプレートコントロールは、利用側でスタイルを変更できます。UserControlは画面の再利用可能な一部分にすぎません。 - プラットフォームサービスは
TopLevelから取得する —TopLevel.GetTopLevel(visual)を使って、ファイルピッカー(StorageProvider)、Clipboard、Launcherを取得します。これらは Windows、macOS、Linux、ブラウザーで動作します。Win32/WinForms のダイアログは決して使用しないでください。 - すべての対象 OS でビルドとテストを行う — フォント、DPI、ウィンドウの外観、ファイルパスはプラットフォームごとに異なります。CI マトリックス(windows/macos/ubuntu)を使えば、ユーザーが問題に遭遇する前に検出できます。
パターン
Generic Host を使った起動
internal static class Program
{
[STAThread]
public static void Main(string[] args) =>
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
// Must stay public static: the XAML previewer calls it
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure<App>().UsePlatformDetect().LogToTrace();
}
public sealed partial class App : Application
{
public override void Initialize() => AvaloniaXamlLoader.Load(this);
public override void OnFrameworkInitializationCompleted()
{
var builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings
{
ContentRootPath = AppContext.BaseDirectory
});
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<MainWindowViewModel>();
builder.Services.AddTransient<OrdersViewModel>();
var host = builder.Build();
host.Start();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = host.Services.GetRequiredService<MainWindowViewModel>()
};
desktop.Exit += (_, _) => host.Dispose();
}
base.OnFrameworkInitializationCompleted();
}
}
理由: OnFrameworkInitializationCompleted はコンポジションルートです。プラットフォームの準備が整い、まだウィンドウが存在しない時点にあたります。IClassicDesktopStyleApplicationLifetime のチェックにより、同じ App をモバイルやブラウザーのライフタイムでも使用できます。UseSkia() を明示的に呼び出して「No text shaping system configured」が表示された場合は、12.0 での変更に対応するため、Avalonia.HarfBuzz と .UseHarfBuzz() を追加してください。
x:DataType を使ったコンパイル済みバインディング
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Contoso.Desktop.ViewModels"
x:Class="Contoso.Desktop.Views.OrdersView"
x:DataType="vm:OrdersViewModel">
<DockPanel>
<TextBox DockPanel.Dock="Top" Text="{Binding CustomerName}" PlaceholderText="Customer" />
<Button DockPanel.Dock="Top" Content="Save" Command="{Binding SaveCommand}" />
<ListBox ItemsSource="{Binding Orders}" SelectedItem="{Binding SelectedOrder}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:OrderRow">
<TextBlock Text="{Binding Number}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</UserControl>
理由: XAML コンパイラーは、ビルド時に CustomerName を OrdersViewModel に照らして解決します。各 DataTemplate はデータコンテキストを切り替えるため、それぞれに独自の x:DataType を宣言します。12 では、TextBox.Watermark の名前が PlaceholderText に変更されました。本当に動的なパスに限り {ReflectionBinding} を使用し、それぞれがトリミングのリスクになると考えてください。
ビューモデルとビューの対応付け(トリミング対応)
<Application.DataTemplates>
<DataTemplate DataType="vm:OrdersViewModel"><views:OrdersView /></DataTemplate>
<DataTemplate DataType="vm:SettingsViewModel"><views:SettingsView /></DataTemplate>
</Application.DataTemplates>
// If you prefer a ViewLocator: explicit factories, no reflection
public sealed class ViewLocator : IDataTemplate
{
private static readonly Dictionary<Type, Func<Control>> Views = new()
{
[typeof(OrdersViewModel)] = () => new OrdersView(),
[typeof(SettingsViewModel)] = () => new SettingsView(),
};
public Control? Build(object? param) =>
param is not null && Views.TryGetValue(param.GetType(), out var factory)
? factory()
: new TextBlock { Text = $"No view for {param?.GetType().Name}" };
public bool Match(object? data) => data is ObservableObject;
}
理由: <ContentControl Content="{Binding Current}" />(desktop-mvvm のナビゲーションを参照)を使うと、適切なビューが描画されます。テンプレートの名前規約に基づくロケーターはコンパイルできますが、トリマーが「未使用」のビュー型を削除するため、トリミング後は実行時に失敗します。
StyledProperty と DirectProperty の違い
public sealed class Badge : TemplatedControl
{
// StyledProperty: settable from styles/themes, supports inheritance and animation
public static readonly StyledProperty<string?> TextProperty =
AvaloniaProperty.Register<Badge, string?>(nameof(Text));
public static readonly StyledProperty<IBrush?> AccentProperty =
AvaloniaProperty.Register<Badge, IBrush?>(nameof(Accent));
// DirectProperty: plain backing field, cheap, NOT settable from styles — use for read-only state
public static readonly DirectProperty<Badge, bool> IsEmptyProperty =
AvaloniaProperty.RegisterDirect<Badge, bool>(nameof(IsEmpty), o => o.IsEmpty);
private bool isEmpty = true;
public string? Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public IBrush? Accent
{
get => GetValue(AccentProperty);
set => SetValue(AccentProperty, value);
}
public bool IsEmpty
{
get => isEmpty;
private set => SetAndRaise(IsEmptyProperty, ref isEmpty, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == TextProperty)
{
IsEmpty = string.IsNullOrEmpty(Text);
PseudoClasses.Set(":empty", IsEmpty);
}
}
}
理由: スタイル付きプロパティは Avalonia の値の優先順位システム(ローカル値 > スタイル > テーマ > 継承 > デフォルト)を経由するため、多少のコストがかかる一方で、テーマ設定が可能になります。直接プロパティはこのシステムを経由しません。:empty などの疑似クラスを使うと、追加のプロパティなしでテーマから状態に応じたスタイルを設定できます。
ControlTheme を使ったテンプレートコントロール
<ControlTheme x:Key="{x:Type local:Badge}" TargetType="local:Badge">
<Setter Property="Accent" Value="{DynamicResource SystemAccentColor}" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="PART_Border" Background="{TemplateBinding Accent}" CornerRadius="8" Padding="6,2">
<TextBlock Text="{TemplateBinding Text}" />
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:empty /template/ Border#PART_Border">
<Setter Property="IsVisible" Value="False" />
</Style>
</ControlTheme>
// Same Badge class as above, shown here for the template part only
[TemplatePart("PART_Border", typeof(Border))]
public sealed class Badge : TemplatedControl
{
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var border = e.NameScope.Find<Border>("PART_Border"); // may be null if a consumer re-templated
}
}
理由: 型をキーにした ControlTheme がコントロールのデフォルトの外観となり、利用側はサブクラス化せずに置き換えられます。TemplateBinding は、テンプレートからコントロールへの低コストな一方向リンクです。利用側は自由にテンプレートを再定義できるため、PART_ が存在しない場合にも必ず対応してください。
TopLevel を使ったクロスプラットフォームサービス
public sealed class AvaloniaDialogService(Func<TopLevel> topLevel) : IFileDialogService
{
public async Task<Stream?> OpenCsvAsync()
{
var files = await topLevel().StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Import orders",
AllowMultiple = false,
FileTypeFilter = [new FilePickerFileType("CSV") { Patterns = ["*.csv"] }]
});
return files is [var file] ? await file.OpenReadAsync() : null;
}
}
理由: IStorageProvider は、サンドボックス化された macOS やブラウザーを含むすべてのプラットフォームで動作します。これらの環境では生のパスを利用できません。従来の OpenFileDialog/SaveFileDialog クラスは 12 で削除されました。12 ではビジュアルルートを TopLevel にキャストする方法が機能しなくなったため、TopLevel.GetTopLevel(control) を使って TopLevel を取得してください。
Native AOT とトリミング
<PropertyGroup>
<PublishAot>true</PublishAot>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
チェックリスト:
- コンパイル済みバインディングのみを使用します(既定で12)。
{ReflectionBinding}とx:CompileBindings="False"がないか監査します - トリミングに安全なビューのマッピングを使用します(型指定された
DataTemplateまたは辞書のViewLocator) - CommunityToolkit.Mvvm のジェネレーター(リフレクションなし)と、ソース生成 JSON(
native-aotスキル)を使用します - 実行時に XAML を読み込まないでください(
AvaloniaRuntimeXamlLoader)。動的にコントロールを生成するアセンブリに限り、TrimmerRootAssemblyを追加します
理由: Avalonia は Native AOT に対応しており、高速なコールドスタートと実行時インストール不要を実現します。これは WPF に対する大きな利点です。ただし、リフレクションバインディングやロケーターが1つでもあると、公開ビルドでしか発生しない実行時クラッシュにつながります。必ず AOT の出力をスモークテストしてください。
パッケージ化
dotnet publish src/Contoso.Desktop -c Release -r win-x64
dotnet publish src/Contoso.Desktop -c Release -r osx-arm64
dotnet publish src/Contoso.Desktop -c Release -r linux-x64
理由: 各 OS で、その OS 用の RID を指定して公開してください(Native AOT では、対象 OS のネイティブツールチェーンが必要です)。macOS では .app バンドル(Info.plist とアイコン)に加えて、署名と公証が必要です。Windows では exe に署名します。3つすべての OS でインストーラーと自動更新を行うには、独自に更新コードを作成するのではなく、Velopack などの専用ツールを使用してください。
アンチパターン
x:DataType のないバインディング
<TextBlock Text="{ReflectionBinding CustmerName}" />
<UserControl x:DataType="vm:OrdersViewModel">
<TextBlock Text="{Binding CustomerName}" />
</UserControl>
Binding クラスを使った C# でのバインディング
// BAD — in Avalonia 12, Binding always means ReflectionBinding
textBlock.Bind(TextBlock.TextProperty, new Binding("CustomerName"));
// GOOD — explicit compiled binding
textBlock.Bind(TextBlock.TextProperty,
CompiledBinding.Create((OrdersViewModel vm) => vm.CustomerName));
共有 UI コードでのプラットフォーム固有 API の使用
// BAD — Windows-only, crashes on macOS/Linux
System.Diagnostics.Process.Start("explorer.exe", folder);
// GOOD — launcher from TopLevel (desktop OSes; sandboxed targets need IStorageFolder overloads)
await TopLevel.GetTopLevel(this)!.Launcher.LaunchDirectoryInfoAsync(new DirectoryInfo(folder));
バックグラウンドスレッドからのコントロール操作
// BAD — throws: call from invalid thread
_ = Task.Run(() => statusText.Text = "Done");
// GOOD — bind to a view model property and await the work; or dispatch explicitly
await Dispatcher.UIThread.InvokeAsync(() => Status = "Done");
Avalonia 12 では、各スレッドが独自のディスパッチャーを持つことができます。UI スレッドが1つのアプリコードでは Dispatcher.UIThread を使用できますが、再利用可能なコントロールライブラリでは、コントロールの Dispatcher プロパティを使用してください。
選択ガイド
| シナリオ | 推奨事項 |
|---|---|
| 新しいクロスプラットフォームデスクトップアプリ | Avalonia 12 + CommunityToolkit.Mvvm + Generic Host |
| ビューのバインディング | ルートとすべての DataTemplate に x:DataType を指定 |
| 真に動的なバインディングパス | {ReflectionBinding} を使用し、隔離して AOT ビルドでは使用しない |
| VM → View のマッピング | App.axaml の型指定された DataTemplate(または辞書の ViewLocator) |
| 再利用可能でスタイルを変更できるコントロール | TemplatedControl + ControlTheme + StyledProperty |
| 1回限りの画面セクション | UserControl |
| 計算されたコントロール状態/読み取り専用のコントロール状態 | DirectProperty + 擬似クラス |
| ファイルの開く/保存 | TopLevel.StorageProvider |
| 最速の起動、実行時インストール不要 | RID ごとに Native AOT で公開 |
| インストーラー + 自動更新 | Velopack(またはプラットフォームネイティブのパッケージ化) |
| 11.x からの移行 | まず公式の「Breaking changes in Avalonia 12」ページを読んでください。バインディング、クリップボード、TopLevel、削除されたダイアログがすべて変更されています |
| 開発者ツール | AvaloniaUI.DiagnosticsSupport の AttachDeveloperTools()(AttachDevTools を置き換えます) |