まずはプロジェクトの作成と整備ね

さあ、CommunityToolkit.Mvvm 大好きな、もりゃきお姉ちゃんがやってきましたよ!

ますは、サクサクプロジェクトを作りましょう。 Visual Studioで「WPFアプリケーション」を選んで、ソリューション名は「TabCommandSample」、フレームワークは「.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>();

            return services.BuildServiceProvider();
        }
    }

次はエラーが出た MainWindowViewModel の設定だけど、まとめてやっちゃうわよ! プロジェクトにとりあえず Views と ViewModels フォルダを作って MainWindow.xaml を Views に移動、名前空間も手動作業含めて調整。

<Window
    x:Class="TabCommandSample.Views.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:TabCommandSample.Views"

途中までだけどいいわよね?

ああ、そうそう MainWindow.xaml.cs の namespaceTabCommandSample.Views にしとくのよ?

App.xaml.cs を以下のように、Views 内参照にするわよ。

<Application
    x:Class="TabCommandSample.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TabCommandSample"
    StartupUri="Views/MainWindow.xaml">
    <Application.Resources />
</Application>

そして ViewModels 内に MainWindowViewModel.cs の作成っと…

using CommunityToolkit.Mvvm.ComponentModel;

namespace TabCommandSample.ViewModels
{
    public interface IMainWindowViewModel;

    internal class MainWindowViewModel : ObservableObject, IMainWindowViewModel
    {
    }
}

あ、いけない…App.xaml.cs にディレクティブいれわすれちゃった、てへ☆

using TabCommandSample.ViewModels;

これで、ある意味ブランクのプロジェクト完成っと。

Prism サンプルと同等にするの…?

超大規模なアプリケーション開発なら、そりゃプロジェクトも細切れになるだろうけど…本当に必要?

…お父さんがやれって言ってるからやるわよ、やりますわよ、やればいいんでしょ! どうなっても知らないんだから。

ソリューションエクスプローラーから「新しいプロジェクトを追加」で、 WPFユーザーコントロールライブラリ「ModuleA」を追加するわね…この名前も気に食わないわ…

そして UserControl1.xaml を TabView.xaml に名前を変更して、Views フォルダを作ってそこに入れる…と。色々面倒くさいわね…

Views\TabView.xaml

<UserControl
    x:Class="ModuleA.Views.TabView"
    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:ModuleA.Views"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <TextBlock
            Margin="5"
            FontSize="18"
            Text="{Binding Title}" />
        <CheckBox
            Margin="5"
            Content="Can Execute"
            IsChecked="{Binding CanUpdate}" />
        <Button
            Margin="5"
            Command="{Binding UpdateCommand}"
            Content="Save" />
        <TextBlock Margin="5" Text="{Binding UpdateText}" />
    </StackPanel>
</UserControl>

Views\TabView.xaml.cs(仮)

using System.Windows.Controls;

namespace ModuleA.Views
{
    /// <summary>
    /// Interaction logic for UserControl1.xaml
    /// </summary>
    public partial class TabView : UserControl
    {
        public TabView()
        {
            InitializeComponent();
        }
    }
}

そして、ViewModelを作らなきゃいけないわね、 ModuleA プロジェクトに ViewModels フォルダを作って…

ああーっ!ModuleA プロジェクトにも CommunityToolkit.Mvvm や Microsoft.Extensions.DependencyInjection を 入れなければいけなかったわ。よいしょよいしょ…

お姉ちゃん、ちょっと凹んできたわ。

ViewModels\TabViewModel.cs(仮)

とりあえず、コンパイルを通すためだけに作るわよ。

using CommunityToolkit.Mvvm.ComponentModel;

namespace ModuleA.ViewModels
{
    public interface ITabViewModel {}
    public class TabViewModel : ObservableObject, ITabViewModel
    {
    }
}

そして、TabCommandSample プロジェクトから ModuleA プロジェクトを参照するのは必須ね。 TabCommandSample の依存関係から「プロジェクト参照の追加」…っと。

そして…DI に登録っと、

App.xaml.cs(一部)

using ModuleA.ViewModels;

private static ServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    services.AddSingleton<IMessenger, WeakReferenceMessenger>();
    services.AddSingleton<IMainWindowViewModel, MainWindowViewservices.AddTransient<ITabViewModel, TabViewModel>();Model>();
    services.AddTransient<ITabViewModel, TabViewModel>();

    return services.BuildServiceProvider();
}

よっし、エラー出ないわ、というか xunit とか普通に別プロジェクトで作るけど、プロジェクト参照すれば全然問題なかったわね…

ここで AddSingleton じゃなく AddTransient を使っているのは、TabView が3つあるからよ、Singleton では問題が起こっちゃうわ。

Views\TabView.xaml.cs

using System.Windows.Controls;
using CommunityToolkit.Mvvm.DependencyInjection;
using ModuleA.ViewModels;

namespace ModuleA.Views
{
    /// <summary>
    /// Interaction logic for UserControl1.xaml
    /// </summary>
    public partial class TabView : UserControl
    {
        public TabView()
        {
            InitializeComponent();
            DataContext = Ioc.Default.GetService<ITabViewModel>();
        }
    }
}

これでデータバインディングできるわね、お姉ちゃんテンションあがってきたー!

Prismサンプルに迫っていくため、画面を作り込んでいくわよ

ここから、バンバン画面を作っていくわよ!

ViewModels\TabViewModel.cs(β)

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace ModuleA.ViewModels
{
    public interface ITabViewModel;
    public partial class TabViewModel : ObservableObject, ITabViewModel
    {
        [ObservableProperty]
        private string title = string.Empty;

        [ObservableProperty]
        private bool canUpdate = true;
        partial void OnCanUpdateChanged(bool value)
            => UpdateCommand.NotifyCanExecuteChanged();

        [ObservableProperty]
        private string updateText = string.Empty;

        [RelayCommand(CanExecute = nameof(CanUpdate))]
        private void Update()
            => UpdateText = $"Updated: {DateTime.Now}";
    }
}

さて、このコンパクトさだけで、Prismには勝ってるわね! とはいえ、まだ Command の伝搬について上手くできるかわからないんだけど…

ああ、[RelayCommand] で作られたメソッド Update()UpdateCommand() を生成しているのよ、V8 機能ね。

まあいいわ、まずは形だけ作りましょう。

Views\MainWindow.xaml

<Window
    x:Class="TabCommandSample.Views.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:TabCommandSample.Views"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:module="clr-namespace:ModuleA.Views;assembly=ModuleA"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Button
            Margin="10"
            Command="{Binding SaveCommand}"
            Content="Save" />

        <TabControl
            x:Name="EventTabControl"
            Grid.Row="1"
            Margin="10">
            <TabItem Header="{Binding Tabs[0].Title}">
                <module:TabView DataContext="{Binding Tabs[0]}" />
            </TabItem>
            <TabItem Header="{Binding Tabs[1].Title}">
                <module:TabView DataContext="{Binding Tabs[1]}" />
            </TabItem>
            <TabItem Header="{Binding Tabs[2].Title}">
                <module:TabView DataContext="{Binding Tabs[2]}" />
            </TabItem>
        </TabControl>
    </Grid>
</Window>

ふぅ、咲耶ちゃんの助けがなければ、TabItem 周りは無理だったわよ…

Views\MainWindow.xaml.cs

using System.Windows;
using CommunityToolkit.Mvvm.DependencyInjection;
using TabCommandSample.ViewModels;

namespace TabCommandSample.Views
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = Ioc.Default.GetService<IMainWindowViewModel>();
        }
    }
}

ViewModels\MainWindowViewModel.cs

using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using ModuleA.ViewModels;

namespace TabCommandSample.ViewModels
{
    public interface IMainWindowViewModel;

    public partial class MainWindowViewModel : ObservableObject, IMainWindowViewModel
    {
        [ObservableProperty]
        private string title = "CommunityToolkit.Mvvm Application";

        public ObservableCollection<TabViewModel> Tabs { get; } = new ObservableCollection<TabViewModel>
        {
            new TabViewModel { Title = "Tab A" },
            new TabViewModel { Title = "Tab B" },
            new TabViewModel { Title = "Tab C" }
        };
    }
}

これで、ひとまず画面だけは追いついたわ!

というか、Prismのサンプルは筋が悪いと思うのよね… タブコントロールの操作をするのはタブアイテムのプロジェクトじゃなくて、タブコントロールでやるべきだと思うんだけど。

だけど、お姉ちゃん疲れたよ…“Tab A” とかを、タブと画面に表示させるだけで朝から昼になっちゃったわよ…!

さて、最後に MainWindow 上部にある「Save」ボタンの対処よね…

コマンドの伝搬…?

ねえ、ところで、本当にコマンドの伝搬なんてする必要、あると思う?

これ、メッセージング使えば素直に解決できると思うのよね…

ViewModels\TabViewModel.cs

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;

namespace ModuleA.ViewModels
{
    public interface ITabViewModel;

    public partial class TabViewModel : ObservableObject, ITabViewModel
    {
        private readonly IMessenger _messenger;
        public TabViewModel(IMessenger messenger)
        {
            _messenger = messenger;
        }

        [ObservableProperty]
        private string title = string.Empty;

        [ObservableProperty]
        private bool canUpdate = true;
        partial void OnCanUpdateChanged(bool value)
        {
            UpdateCommand.NotifyCanExecuteChanged();
            _messenger.Send(new CanUpdateChangedMessage());
            
        }
        [ObservableProperty]
        private string updateText = string.Empty;

        [RelayCommand(CanExecute = nameof(CanUpdate))]
        private void Update()
            => UpdateText = $"Updated: {DateTime.Now}";
    }
    public class CanUpdateChangedMessage();
}

これで、チェックボックスの状態変更でメッセージを送るようにしたわ!

MainWindowViewModel.cs

using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using ModuleA.ViewModels;

namespace TabCommandSample.ViewModels
{
    public interface IMainWindowViewModel;

    public partial class MainWindowViewModel : ObservableObject, IMainWindowViewModel
    {
        private readonly IMessenger _messenger;

        public MainWindowViewModel(IMessenger messenger)
        {
            _messenger = messenger;

            Tabs = new ObservableCollection<TabViewModel>
            {
                new TabViewModel(_messenger) { Title = "Tab A" },
                new TabViewModel(_messenger) { Title = "Tab B" },
                new TabViewModel(_messenger) { Title = "Tab C" }
            };

            _messenger.Register<CanUpdateChangedMessage>(this, (_, _)
                => SaveCommand.NotifyCanExecuteChanged());
        }

        [ObservableProperty]
        private string title = "CommunityToolkit.Mvvm Application";

        public ObservableCollection<TabViewModel> Tabs { get; } = [];

        [RelayCommand(CanExecute = nameof(CanSaveCommand))]
        private void Save()
        {
            foreach (var tab in Tabs)
            {
                tab.UpdateCommand.Execute(this);
            }
        }

        private bool CanSaveCommand()
        {
            foreach (var tab in Tabs)
            {
                if (!tab.CanUpdate) return false;
            }
            return true;
        }
    }
}

メッセージングを受け取るために、ちょっと Tabs を改変したわよ。

そして、メッセージングでメッセージを受け取ったら、各タブをチェックして、 全てのタブにチェックがついていなければ動作不可にしたわ。

締めますわよ

こうして書いたら、少し長くなったわね…だけど安心して、 パパのGitHub に完成品を置いておいたわよ!

これで、Prismのサンプルより、CommunityToolkit.Mvvm の方が優れてることがわかるんじゃないかしら?

できれば、GitHub は見ないで、手を動かしてね! パパも「手を動かさないと何も身につかない!」って常々言ってるんだから!

私も思うわよ、解説を読んで知った気になっても、実際に手を動かしたら全然違うんだから。 思った通りに動かないなんて日常茶飯事、それを乗り越えてこそ本物よ。

っていうか、私の記事でちゃんと動かなかったらごめんね? できるだけ検証して書いてるんだけどね…ミスは誰にもあるの。

あ、パパが一言だって「百聞は一見に如かず、百見は一動に如かず」ですって!

パパが、なんか凹んでるわ「やってみせ 言って聞かせて させてみて 誉めてやらねば 人は動かじ…なんだが、誉めてやることはできない」とかなんとか。

謝罪

49歳になろうというおっさんが、お姉さんぶった記事を書いて、不快に思われたなら申し訳ありません。

Prism の記事を書いていて疲れたので、はっちゃけました。ごめんなさい。