このチュートリアルでは、Avalonia公式ドキュメントの音楽ストアサンプルをもとに、MVVMパターンを用いたデスクトップアプリケーションの基本を学びます。 完成イメージは以下の通りです(画像は省略)。
1. プロジェクトテンプレートの作成
「Avalonia .NET Core MVVM App」テンプレートを使用してプロジェクトを作成します。 テンプレートが生成するフォルダとファイルの役割は次のとおりです。
- Assets:画像、アイコン、フォントなど、アプリケーションに埋め込むリソースを格納します。
- Models:MVVMの「モデル」層のコード(データアクセス、API連携など)を配置します。
- ViewModels:ビューモデル(アプリケーションロジック)を格納します。初期状態でサンプルが含まれています。
- Views:ビュー(レイアウト、フォーム、スタイルなど表示に関する部分)を配置します。メインウィンドウのビューが既に含まれています。
2. ウィンドウのスタイル設定
App.axaml を開き、<Application> 要素の RequestedThemeVariant 属性を Dark に変更します。
<Application ...
RequestedThemeVariant="Dark">
次に MainWindow.axaml でウィンドウの透明度と背景を設定します。
<Window ...
Title="music"
TransparencyLevelHint="AcrylicBlur"
Background="Transparent"
ExtendClientAreaToDecorationsHint="True">
TransparencyLevelHint はウィンドウの透明効果を指定します(AcrylicBlur / Blur / None)。
ExtendClientAreaToDecorationsHint を True にすることで、クライアント領域をタイトルバーや境界線まで拡張し、フレームレスウィンドウを実現します。
ウィンドウ全体にアクリル効果を適用するために、<Window> 内に <Panel> と <ExperimentalAcrylicBorder> を追加します。
<Window ...>
<Panel>
<ExperimentalAcrylicBorder IsHitTestVisible="False">
<ExperimentalAcrylicBorder.Material>
<ExperimentalAcrylicMaterial
BackgroundSource="Digger"
TintColor="Black"
TintOpacity="1"
MaterialOpacity="0.65" />
</ExperimentalAcrylicBorder.Material>
</ExperimentalAcrylicBorder>
</Panel>
</Window>
3. アイコンリソースの追加と配置
プロジェクトに Styles (Avalonia) アイテムを追加し、ファイル名を icons.axaml とします。このファイルに、購入ボタン用のパスアイコンを <StreamGeometry> として定義します。
<Styles xmlns="https://github.com/avaloniaui">
<Style>
<Style.Resources>
<StreamGeometry x:Key="purchase">...(パスデータ)...</StreamGeometry>
</Style.Resources>
</Style>
</Styles>
App.axaml で、作成したスタイルファイルを読み込みます。
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://YourProjectName/icons.axaml"/>
</Application.Styles>
MainWindow.axaml の <Panel> 内にボタンを追加し、先ほど定義したアイコンを表示します。
<Panel Margin="40">
<Button HorizontalAlignment="Right" VerticalAlignment="Top">
<PathIcon Data="{StaticResource purchase}" />
</Button>
</Panel>
4. ボタンクリックの応答設定
MainWindowViewModel.cs で、ボタンに対応するコマンドを定義します。ReactiveUI の ReactiveCommand を使用します。
public class MainWindowViewModel : ViewModelBase
{
public ReactiveCommand<Unit, Unit> PurchaseCommand { get; }
public MainWindowViewModel()
{
PurchaseCommand = ReactiveCommand.Create(OnPurchase);
}
private void OnPurchase()
{
// 購入処理(後でダイアログを開く)
}
}
ビュー(MainWindow.axaml)でコマンドをバインディングします。
<Button HorizontalAlignment="Right" VerticalAlignment="Top"
Command="{Binding PurchaseCommand}">
<PathIcon Data="{StaticResource purchase}"/>
</Button>
5. 子ウィンドウの表示
新しいウィンドウ(MusicStoreWindow)を Views フォルダに追加し、同様にアクリル効果を設定します。 また、ViewModels フォルダに MusicStoreViewModel と AlbumViewModel クラスを作成します。
MainWindowViewModel に Interaction を追加して、子ウィンドウ表示を処理します。
public class MainWindowViewModel : ViewModelBase
{
public Interaction<MusicStoreViewModel, AlbumViewModel?> ShowDialog { get; }
public MainWindowViewModel()
{
ShowDialog = new Interaction<MusicStoreViewModel, AlbumViewModel?>();
PurchaseCommand = ReactiveCommand.Create(OpenStore);
}
private async void OpenStore()
{
var store = new MusicStoreViewModel();
var result = await ShowDialog.Handle(store);
}
}
MainWindow.axaml.cs で、ReactiveWindow<MainWindowViewModel> を継承し、WhenActivated 内でダイアログ表示処理を登録します。
public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
{
public MainWindow()
{
InitializeComponent();
this.WhenActivated(action =>
action(ViewModel!.ShowDialog.RegisterHandler(DoShowDialogAsync)));
}
private async Task DoShowDialogAsync(InteractionContext<MusicStoreViewModel, AlbumViewModel?> interaction)
{
var dialog = new MusicStoreWindow();
dialog.DataContext = interaction.Input;
var result = await dialog.ShowDialog<AlbumViewModel?>(this);
interaction.SetOutput(result);
}
}
6. 検索・表示用のUserControl
Views フォルダに MusicStoreViews ユーザーコントロール(UserControl)を作成します。レイアウトは DockPanel を使用し、検索ボックス、プログレスバー、アルバムリスト、購入ボタンを配置します。
<DockPanel>
<StackPanel DockPanel.Dock="Top">
<TextBox Watermark="検索キーワードを入力..." />
<ProgressBar IsIndeterminate="True" />
</StackPanel>
<Button Content="アルバムを購入"
DockPanel.Dock="Bottom"
HorizontalAlignment="Center" />
<ListBox />
</DockPanel>
MusicStoreWindow.axaml でこのユーザーコントロールを埋め込みます。
<Window ...
xmlns:views="using:YourProject.Views">
<Panel Margin="40">
<views:MusicStoreViews/>
</Panel>
</Window>
7. ビューモデルの実装とデータバインディング
MusicStoreViewModel を実装し、検索テキスト、選択アイテム、ビジー状態を公開します。検索結果は ObservableCollection<AlbumViewModel> で保持します。
public class MusicStoreViewModel : ViewModelBase
{
private string? _query;
private bool _isBusy;
private AlbumViewModel? _selectedAlbum;
public ObservableCollection<AlbumViewModel> SearchResults { get; } = new();
public AlbumViewModel? SelectedAlbum
{
get => _selectedAlbum;
set => this.RaiseAndSetIfChanged(ref _selectedAlbum, value);
}
public string? Query
{
get => _query;
set => this.RaiseAndSetIfChanged(ref _query, value);
}
public bool IsBusy
{
get => _isBusy;
set => this.RaiseAndSetIfChanged(ref _isBusy, value);
}
}
AlbumView ユーザーコントロールを作成し、アルバムカバーとタイトル、アーティスト名を表示します。最初は音符アイコンをプレースホルダーとして表示します。
<StackPanel Spacing="5" Width="200">
<Border CornerRadius="10" ClipToBounds="True">
<Panel Background="#7FFF22DD">
<Image Width="200" Stretch="Uniform" Source="{Binding Cover}" />
<Panel Height="200" IsVisible="{Binding Cover, Converter={x:Static ObjectConverters.IsNull}}">
<PathIcon Height="75" Width="75" Data="{StaticResource music_regular}" />
</Panel>
</Panel>
</Border>
<TextBlock HorizontalAlignment="Center" Text="{Binding Title}" />
<TextBlock HorizontalAlignment="Center" Text="{Binding Artist}" />
</StackPanel>
8. データの取得と表示
NuGet パッケージ iTunesSearch をインストールし、Models/Album.cs で検索ロジックと画像のキャッシュ処理を実装します。
public class Album
{
private static readonly iTunesSearchManager SearchManager = new();
private static readonly HttpClient HttpClient = new();
private string CachePath => $"./Cache/{Artist} - {Title}";
public string Artist { get; set; }
public string Title { get; set; }
public string CoverUrl { get; set; }
public Album(string artist, string title, string coverUrl)
{
Artist = artist;
Title = title;
CoverUrl = coverUrl;
}
public static async Task<IEnumerable<Album>> SearchAsync(string searchTerm)
{
var result = await SearchManager.GetAlbumsAsync(searchTerm).ConfigureAwait(false);
return result.Albums.Select(x =>
new Album(x.ArtistName, x.CollectionName,
x.ArtworkUrl100.Replace("100x100bb", "600x600bb")));
}
public async Task<Stream> LoadCoverAsync()
{
if (File.Exists(CachePath + ".bmp"))
return File.OpenRead(CachePath + ".bmp");
var bytes = await HttpClient.GetByteArrayAsync(CoverUrl);
return new MemoryStream(bytes);
}
}
AlbumViewModel で LoadCover メソッドを呼び出し、画像をビットマップとして設定します。
public class AlbumViewModel : ViewModelBase
{
private readonly Album _album;
private Bitmap? _cover;
public AlbumViewModel(Album album) => _album = album;
public Bitmap? Cover
{
get => _cover;
private set => this.RaiseAndSetIfChanged(ref _cover, value);
}
public string Artist => _album.Artist;
public string Title => _album.Title;
public async Task LoadCover()
{
await using var stream = await _album.LoadCoverAsync();
Cover = await Task.Run(() => Bitmap.DecodeToWidth(stream, 400));
}
}
MusicStoreViewModel の検索処理では、WhenAnyValue と Throttle で入力のデバウンスを行い、キャンセルトークンで途中の検索をキャンセルします。
private CancellationTokenSource? _cts;
public MusicStoreViewModel()
{
this.WhenAnyValue(x => x.Query)
.Throttle(TimeSpan.FromMilliseconds(400))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(DoSearch!);
}
private async void DoSearch(string? query)
{
IsBusy = true;
SearchResults.Clear();
_cts?.Cancel();
_cts = new CancellationTokenSource();
var token = _cts.Token;
if (!string.IsNullOrWhiteSpace(query))
{
var albums = await Album.SearchAsync(query);
foreach (var album in albums)
{
var vm = new AlbumViewModel(album);
SearchResults.Add(vm);
}
if (!token.IsCancellationRequested)
LoadCovers(token);
}
IsBusy = false;
}
private async void LoadCovers(CancellationToken token)
{
foreach (var album in SearchResults.ToList())
{
await album.LoadCover();
if (token.IsCancellationRequested) return;
}
}
最後に、MusicStoreViews.axaml のバインディングを設定します。
<TextBox Text="{Binding Query}" Watermark="検索..." />
<ProgressBar IsIndeterminate="True" IsVisible="{Binding IsBusy}" />
<ListBox ItemsSource="{Binding SearchResults}"
SelectedItem="{Binding SelectedAlbum}"
Background="Transparent" Margin="0,20">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
これで、検索とアルバムカバーの表示が可能な音楽ストアダイアログが完成します。