プロジェクトの準備

Visual Studioから新しいプロジェクトで「C#」「Windows」経由で「WPFアプリケーション」を選び、プロジェクト名は「NameTest」にして、ソリューションを作成してください。これでソリューションも NameTest になります。説明通りに動かないことを避けるため、この記事に関しては可能な限りソリューション名を NameTest にしてください。

フレームワークは「.NET 9.0」を使用します。

この「WPF における CommunityToolkit.Mvvm 覚え書き」シリーズは、ある程度のMVVM知識を有していることを前提としております!

簡単に言うとViewは見た目、ViewModelはViewにデータバインディグする関連の処理、Modelはアプリケーションのコアとなる処理群ですね。大規模プロジェクトなら、おそらくModelが一番大きくなります。

MVVM Toolkit の利用準備

ソリューションに対し、Visual Studioのメニュー「ツール」「NuGetパッケージマネージャ」から「ソリューションのNuGetパッケージの管理」を選び、「参照」タブから CommunityToolkit.Mvvm をインストールします。

Microsoft.Extensions.DependencyInjection も同様にインストールします。

もちろん理解している方なら、コマンドからInstall-Package CommunityToolkit.Mvvm等としても構いません。

Ioc - 制御の逆転って?

DI(依存性注入)については、簡単に言うと、オブジェクトの生成や依存関係の解決を外部(具体的には後に出てくる App.xaml.cs)に委譲することで、コードの柔軟性とテストの容易さを向上させるパターンです。この記事では、具体的な手順を通じてDIの使い方を学びます。

何より大切なのは、自分でソリューションを作り、手を動かすことです。コピペしても何も習得できず、応用が利きません。

App.xamlの書き換え

まずは MVVM としてのスタンダードとして、プロジェクト直下に [追加] から [新しいフォルダ] を利用して ViewsViewModels そして Models フォルダを作ります、(今回は Models 使わないので省いていいですけど、一般的な構成として覚えておいてください、すなわち大規模アプリケーション開発に直接アドバイスする記事ではありません)。

プロジェクト直下にある MainWindow.xamlViews にドラッグドロップで移動します。移動時、名前空間も調整してください。

そして App.xaml を以下のように調整してください。

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

    </Application.Resources>
</Application>

から

<Application x:Class="NameTest.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:NameTest"
             StartupUri="Views/MainWindow.xaml">
             <!-- MainWindowをViewsフォルダに移動したからそれを反映 -->
    <Application.Resources>

    </Application.Resources>
</Application>

と変更します。StartupUri の行にフォルダを追加するだけです(コメントは不要です)。

ここで、ソリューションを実行して「MainWindow」が開かれることを確認してください。

依存性注入の設定

Views\App.xaml 直下にある App.xaml.csApp クラスを以下のように置き換えます。

とりあえずは、何も考えずにこうすればいいと思います、一度設定さえすれば基本弄りません。DIで扱うクラスの追加は ConfigureServices になり、ここだけは弄ります。

    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    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();

            // ViewModel
            services.AddSingleton<IMainWindowViewModel, MainWindowViewModel>();
            return services.BuildServiceProvider();
        }
    }

これで必要なディレクティブ

using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;

が自動的に追加されるはずです。

ここでは、AddSingletonの行にある IMainWindowViewModel MainWindowViewModel のためコンパイルできません!焦らないで!

で「依存性注入とはなんぞや?」という話になりますが、具体的には ConfigureService 内でクラスのインスタンスを一括生成し、クラスを利用する際にはインターフェース IMainWindowViewModel を使って「クラスのコンストラクタ引数を使ってインスタンスを渡す(注入する)」という仕組みです。

MainMainWindowViewModel.cs の作成

次に、プロジェクトの ViewModels フォルダに MainWindowViewModel.cs を「クラス」として作ります。名前の通り MainWindow の ViewModelとなります。

その内容を、全部消してから、以下の内容に置き換えてください。

using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;

namespace NameTest.ViewModels
{
    public interface IMainWindowViewModel;

    public class MainWindowViewModel : ObservableObject, IMainWindowViewModel
    {
        private string _name = string.Empty;
        public string Name
        {
            get => _name;
            set => SetProperty(ref _name, value);
        }
    }
}

ソリューション名を NameTest にしていない場合、namespace の後の NameTest を書き換えることはわかりますよね?

それすらわからないなら、ソリューションを NameTest で作り直してやり直しましょう。ソリューションの作り直しすら分からないなら、それ以前の問題なので、この記事を読む以前から出直してきましょう

SetPropertyObservableObject を利用したデータバインディングを簡単に実行する方法です。

「データバインティングしないとデータ壊れる可能性がある、だからデータバインディングしてデータ壊さないようにしてる」という言い回しで大丈夫でしょうか?

ある程度以上の規模の開発ならデータバインディングは必須になるので、覚えておきましょう。

ViewModelの登録

先ほど、AddSingletonの行にある IMainWindowViewModel MainWindowViewModel のためコンパイルできません!と書きましたが、先ほどの行追加によってコンパイルの準備が整いました。

App.xaml.cs を開き、必要なディレクティブ

using NameTest.ViewModels;

を追記しましょう。これでコンパイルだけは通るようになります。

DIを利用したViewModelとViewのデータバインディング設定

ここで、ViewModelには Name というバインディング可能な値があります。それを紐付けます。

まず Views フォルダにある MainWindow.xaml の中にある MainWindow.xaml.cs を開き、以下のように置き換えます。

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

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

これで、MainMainWindowViewModel.cs にある Name と XAML のデータバインディングの準備が整いました。

Views\MainWindow.xaml を開いて、以下のようにしましょう。

<Window x:Class="NameTest.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:NameTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid VerticalAlignment="Center">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <TextBox Grid.Row="0" Width="300" Height="30" Text="{Binding Name,UpdateSourceTrigger=PropertyChanged}"/>
        <Button Grid.Row="1" Width="60" Height="30" Content="名前表示" Command="{Binding NameCommand}" />
    </Grid>
</Window>

これを実行すれば、_name にデータバインディングできています。

Binding Name にある UpdateSourceTrigger は、今回のように Name が変更される度に _name を更新する必要がある場合に記述してください。

_name にデータバインディングされているかの確認

そして、NameTest.ViewModels.MainWindowViewModel.csMainWindowViewModel クラス内に以下の記述をしてください。

public RelayCommand NameCommand { get; set; }

public MainWindowViewModel()
{
    NameCommand = new RelayCommand(
        () => MessageBox.Show(_name)
    );
}

ここで、RelayCommand を利用するために必要なディレクティブが追記されます

using CommunityToolkit.Mvvm.Input;

これで、このソリューションは最低限コンパイルして実行できます。

メッセージボックスで Name_name にバインディングされていることが確認できましたね?

ここで問題なのは、Name が空欄でもボタンが押せることなので、どうにかしましょう。

private string _name = string.Empty;
public string Name
{
    get => _name;
    set => SetProperty(ref _name, value);
}

という行がありますよね、ここを改変します。

private string _name = string.Empty;
public string Name
{
    get => _name;
    set
    {
        SetProperty(ref _name, value);
        NameCommand.NotifyCanExecuteChanged();
    }
}

これで、Name TextBox の変更に対し、ボタンの Command にイベント発火するようになります。

NotifyCanExecuteChangedであることに注意!

更に NameCommand を書き換えます。

public RelayCommand NameCommand { get; set; }

public MainWindowViewModel()
{
    NameCommand = new RelayCommand(
        () => MessageBox.Show(_name)
    );
}

から

public RelayCommand NameCommand { get; set; }

public MainWindowViewModel()
{
    NameCommand = new RelayCommand(
        () => MessageBox.Show(_name),
        () => !string.IsNullOrEmpty(_name)
    );
}

と、有効無効の条件を入れます。

こうすることで、Name が空欄の時に「名前表示」が無効になり、名前を入力したら「名前表示」が有効になります。

ここまでで、依存性注入を利用しての、データバインディングができました。

現時点での画面

この時点でのソリューションエクスプローラーは、こうなっているはずです。

ソリューションエクスプローラー

ソリューションを動かしたら、こうなるはずです。

名前が空欄のウィンドウ

また、名前を入力したら、こうなるはずです。

名前が入力されたウィンドウ

こうなっていれば、あなたはきちんと手順を踏んで作業をやり遂げたことになります。お疲れ様でした。

最後に

本記事の作成にあたり、一部ChatGPTの添削アドバイスを参考にしました。

次記事は 「WPF における CommunityToolkit.Mvvm(MVVM ToolKit) 覚え書き(2) 依存性注入(DI)を利用した簡単なサンプル」となります。