まずは下準備!

はーい、お久しぶり!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ライセンスだから、好きに使ってちょうだいね!