まずは下準備!
はーい、お久しぶり!CommunityToolkit.Mvvm大好きの、もりゃきお姉ちゃんだよ!
以前さぁ……WPFにおけるPrism集中講座(3) 試しにViewのサンプルを書いてみる で書いた内容って、Prism を使わなくてもできたのよね。
だから、パパのお許しがでたから書いちゃうね!
プロジェクトを作りましょう。 Visual Studioで「WPFアプリケーション」を選んで、ソリューション名は「SimpleCTViewSample」、フレームワークは「.NET 9」選択ね!
次に、NuGet経由で「CommunityToolkit.Mvvm」と「Microsoft.Extensions.DependencyInjection」をいつもの通りにインストールしましょうね!
CommunityToolkit.Mvvm V8 を使うから、エラーが出たらとりあえずリビルドしてね、お姉さんとの約束よ!
DI の設定よ
はい、これを App.xaml.cs に適切にコピペしちゃいましょう!
public partial class App : Application
{
/// <summary>
/// サービスの登録をします
/// </summary>
public App()
{
Services = ConfigureServices();
Ioc.Default.ConfigureServices(Services);
}
/// <summary>
/// 現在の App インスタンスを取得します
/// </summary>
public new static App Current => (App)Application.Current;
/// <summary>
/// サービスプロバイダです
/// </summary>
public IServiceProvider Services { get; }
/// <summary>
/// サービスを登録します
/// </summary>
/// <returns></returns>
private static ServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<IMessenger, WeakReferenceMessenger>();
services.AddSingleton<IMainWindowViewModel, MainWindowViewModel>();
services.AddTransient<MainControlViewModel>();
services.AddTransient<SettingsControlViewModel>();
return services.BuildServiceProvider();
}
}
今回 AddTransient
を使ってるけど、これは複数の UserControl を使うときはこうした方がいいようなの。
基本的に、UserControl は DataContext
を持たないんだって、パパがどこかで聞いてきたことを、ドヤ顔でわたしに説明するのよ……
フォルダの作成と準備
今回はシンプルなサンプルだから、プロジェクトに Views と ViewModels だけを作るわよ!
MainWindowViewModel.cs
で、MainWindowViewModel.cs を ViewModel フォルダに作成して以下のようにするの。
using CommunityToolkit.Mvvm.ComponentModel;
namespace SimpleCTViewSample.ViewModels
{
public interface IMainWindowViewModel { }
class MainWindowViewModel : ObservableObject, IMainWindowViewModel
{
}
}
IViewModel.cs
まず ViewModel フォルダに IViewModel.cs を作るわよ!
namespace SimpleCTViewSample.ViewModels
{
public interface IViewModel;
}
MainControlViewModel.cs
同じように MainControlViewModel.cs を ViewModel フォルダに作成するわ。
using CommunityToolkit.Mvvm.ComponentModel;
namespace SimpleCTViewSample.ViewModels
{
public class MainControlViewModel : ObservableObject, IViewModel
{
}
}
MainControlViewModel.cs
わかると思うけど MainControlViewModel.cs も ViewModel フォルダに作成するわよ。
using CommunityToolkit.Mvvm.ComponentModel;
namespace SimpleCTViewSample.ViewModels
{
public class SettingsControlViewModel : ObservableObject, IViewModel
{
}
}
App.xaml.cs
次に App.xaml.cs の冒頭にこの一行を入れるわよ。
using SimpleCTViewSample.ViewModels;
App.xaml
そして、MainWindow.xaml を Views フォルダに移動して、App.xaml を以下のように書き換えるわ。
<Application
x:Class="SimpleCTViewSample.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SimpleCTViewSample"
StartupUri="Views/MainWindow.xaml">
<Application.Resources />
</Application>
最後に、MainWindow.xaml を Views フォルダに移せばいいわ。」
View の作成
この SimpleCTViewSample では、UserControl を二つ使うけど、これは原文とほとんど同じよ。コード貼っておくわね。プロジェクト名が変わってるから、置いておくわね。
あ、あと SettingsControl.xaml には、Command でコマンド使ってるから要注意!
MainControl.xaml
<UserControl
x:Class="SimpleCTViewSample.Views.MainControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SimpleCTViewSample.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Grid>
<Label Content="メイン画面です" FontSize="30" />
</Grid>
</UserControl>
SettingsControl.xaml
<UserControl
x:Class="SimpleCTViewSample.Views.SettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SimpleCTViewSample.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Label
Grid.Row="0"
Grid.Column="0"
Content="設定画面です"
FontSize="30" />
<Button
Grid.Row="1"
Grid.Column="1"
Margin="5"
Command="{Binding QuitSettingCommand}"
Content="設定終了"
FontSize="16" />
</Grid>
</UserControl>
MainWinodw.xaml.cs
さて、ここからが本番よ!これ、実はいつもの通り DataContext を設定するだけなのよね……
using System.Windows;
using CommunityToolkit.Mvvm.DependencyInjection;
using SimpleCTViewSample.ViewModels;
namespace SimpleCTViewSample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = Ioc.Default.GetService<IMainWindowViewModel>() ?? throw new NullReferenceException(nameof(IMainWindowViewModel));
}
}
}
そう、Prismみたいに Loaded や Click イベントなんて使わないわよ!
MainWindow.xaml の記述
<Window
x:Class="SimpleCTViewSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SimpleCTViewSample"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:v="clr-namespace:SimpleCTViewSample.Views"
xmlns:vm="clr-namespace:SimpleCTViewSample.ViewModels"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.Resources>
<DataTemplate DataType="{x:Type vm:MainControlViewModel}">
<v:MainControl />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SettingsControlViewModel}">
<v:SettingsControl />
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Menu>
<MenuItem Command="{Binding SettingsCommand}" Header="設定" />
</Menu>
<ContentControl Grid.Row="1" Content="{Binding CurrentViewModel}" />
</Grid>
</Window>
ユーザーコントロール切り替えの準備
さて、このままじゃ画面は切り替わらないの。 まだ続くのか、Prism の方が楽だと思う方は、そっとページを閉じてね。
気を取り直して……さっき、なんで IViewModel
を作ったかと思ったでしょ?画面切り替えのためなのよね。
だってさ、ViewModel なのに object?
型に入れたりしたら台無しじゃない?
MainWindowViewModel.cs を以下のようにしてちょうだい!
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
namespace SimpleCTViewSample.ViewModels
{
public interface IMainWindowViewModel { }
public partial class MainWindowViewModel : ObservableObject, IMainWindowViewModel
{
[ObservableProperty]
private IViewModel currentViewModel;
public MainControlViewModel MainControlVM { get; }
public SettingsControlViewModel SettingsControlVM { get; }
[RelayCommand]
private void Settings()
{
CurrentViewModel = SettingsControlVM;
}
private readonly IMessenger _messenger;
public MainWindowViewModel() { throw new NullReferenceException(nameof(MainWindowViewModel)); }
public MainWindowViewModel(
IMessenger messenger,
MainControlViewModel mainControlVM,
SettingsControlViewModel settingsControlVM)
{
_messenger = messenger;
MainControlVM = mainControlVM;
SettingsControlVM = settingsControlVM;
currentViewModel = MainControlVM;
}
}
}
これで……メイン画面が表示されて、設定ボタンから設定画面に行けるわ。
だけど、設定画面からのボタンの Command
を実装してないのよね。
ボタンは SettingsControl.xaml にあるから、その管轄は SettingsControlViewModel.cs よ。こう書き換えてね。
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
namespace SimpleCTViewSample.ViewModels
{
public record QuitSettingsMessage();
public partial class SettingsControlViewModel : ObservableObject, IViewModel
{
[RelayCommand]
private void QuitSetting()
{
_messenger.Send<QuitSettingsMessage>();
}
private readonly IMessenger _messenger;
public SettingsControlViewModel() { throw new NullReferenceException(nameof(SettingsControlViewModel)); }
public SettingsControlViewModel(IMessenger messenger)
{
_messenger = messenger;
}
}
}
そうして、MainWindowViewModel.cs でこのメッセージを受信するの、コンストラクタをこうしてね。
public MainWindowViewModel(
IMessenger messenger,
MainControlViewModel mainControlVM,
SettingsControlViewModel settingsControlVM)
{
_messenger = messenger;
MainControlVM = mainControlVM;
SettingsControlVM = settingsControlVM;
currentViewModel = MainControlVM;
_messenger.Register<QuitSettingsMessage>(this, (_, _) => CurrentViewModel = MainControlVM);
}
ちょっと一息
もりゃきお姉ちゃんも、ちょっと疲れちゃったわよ!
ただ、Prism のような「なんかよくわからないけど動いた」って状況にだけはならないのよね、きちんと理解できれば素直で軽量、それが CommunityToolkit.Mvvm。
さーってと、動くには動いたけど……まだ補足することがあるのよね!
来てくれた皆も疲れてるとは思うけど、もう一息よ!
リソース辞書の外だし(Resourceディレクトリ)
ここで言う「Resourceディレクトリ」って、多言語対応のあのリソースじゃないからね?
さて、もう一度 MainWindow.xaml を見てみるわよ?
<Window
x:Class="SimpleCTViewSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SimpleCTViewSample"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:v="clr-namespace:SimpleCTViewSample.Views"
xmlns:vm="clr-namespace:SimpleCTViewSample.ViewModels"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.Resources>
<DataTemplate DataType="{x:Type vm:MainControlViewModel}">
<v:MainControl />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SettingsControlViewModel}">
<v:SettingsControl />
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Menu>
<MenuItem Command="{Binding SettingsCommand}" Header="設定" />
</Menu>
<ContentControl Grid.Row="1" Content="{Binding CurrentViewModel}" />
</Grid>
</Window>
これ、この規模だから良いけど……たくさんの画面があったら Window.Resources
が膨れ上がるのは目に見えてるわよね?
そのために、この画面リソースを「外だしxaml」にするのが、リソースディレクトリよ。 適用した場合のコードだけ書いておくから、各自よく見比べてね!
MainWindow.xaml(改)
<Window
x:Class="SimpleCTViewSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SimpleCTViewSample"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ViewsTemplates.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Menu>
<MenuItem Command="{Binding SettingsCommand}" Header="設定" />
</Menu>
<ContentControl Grid.Row="1" Content="{Binding CurrentViewModel}" />
</Grid>
</Window>
あー、厳密にはこの場合 <ResourceDictionary.MergedDictionaries>
って要らないんだけど、複数のリソースディレクトリを登録する場合、必須になるからね?
ViewsTemplates.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:v="clr-namespace:SimpleCTViewSample.Views"
xmlns:vm="clr-namespace:SimpleCTViewSample.ViewModels">
<DataTemplate DataType="{x:Type vm:MainControlViewModel}">
<v:MainControl />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SettingsControlViewModel}">
<v:SettingsControl />
</DataTemplate>
</ResourceDictionary>
おわりに
さて、これでひとまずおしまい。
どうだったかしら?Prism だと裏で色々やってくれる事のが辛い、よくわからない……そういう人には CommunityToolkit.Mvvm の強みが分かるんじゃないかしら?
確かに、手間は掛かるのよね……だけど個人ベースやそれに毛が生えた小規模開発なら、意味不明のバグに苦しむよりはコストが抑えられると思うわよ。
今回、パパは突発性中二病で入院中なの、楽しみにしていた人はゴメンね?
今回の完成品は GitHub に上げておいたわよ!CC0ライセンスだから、好きに使ってちょうだいね!