WPFでMVVMを学ぶ:基本からMVVM Toolkitでの簡潔な実装まで

IT

WPF(Windows Presentation Foundation)アプリケーション開発において、MVVM(Model-View-ViewModel)パターンは、UIとロジックの分離を実現する設計手法として広く採用されています。しかしながら、MVVMの導入にはコードの冗長化やINotifyPropertyChangedなどの実装負荷が課題となるケースもあります。

本記事では、シンプルな検索アプリケーションを題材に、MVVMの基本的な構成要素(Model、ViewModel、View)を手動で実装した後、CommunityToolkit.Mvvm(以下、MVVM Toolkit)を用いて同等のアプリケーションを構築し、両者の実装の違いとメリットを比較検証します。

実行環境

以下の環境で動作確認を行っています。

oswindows11(4cpu、16GB)
.NET8.0.304
IDEVisual Studio 2022 Community
CommunityToolkit.Mvvm8.4.0

記事のコードは、解説をわかりやすくするために、エラーチェックなど一部の処理を省略しています。

サンプルアプリケーション概要

本記事で扱うアプリは、ユーザー名を入力し、該当するユーザーの詳細情報(名前、メールアドレス、年齢)を表示する検索機能のみを持つシンプルなWPFアプリケーションです。UIの外観は検証目的のため簡素な構成としています。

MVVMの手動実装

Modelの実装

Modelはアプリケーションのデータを保持します。以下のようなPersonクラスを定義します。

namespace WpfTest.Model;

public class Person
{
	public string? Name { get; set; }
	public string? Email { get; set; }
	public int Age { get; set; }
}

ViewModel(バインドするプロパティの実装)

ViewModelViewModelの橋渡しを担うコンポーネントです。INotifyPropertyChangedインターフェースを用いてプロパティ変更通知を実装します。

public class UserVM : INotifyPropertyChanged
{
    // プロパティの変更を通知するイベント
	public event PropertyChangedEventHandler? PropertyChanged;

    // イベントを呼び出すメソッド
	private void OnPropertyChanged(string propertyName)
	{
		PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
	}
}

次にテキストボックスに入力された値や結果欄に表示するユーザー情報を保持するプロパティを作成します。このプロパティのSetterで先ほど作成したメソッドを呼び出します。プロパティごとにprivateとpublicのセットを作成します。このプロパティに値がセット(変更)されるたびに、変更通知用イベントが発火します。

// イベントなどは省略
public class UserVM : INotifyPropertyChanged
{
    // (略)イベント定義

    // テキストボックスに入力されたユーザー名用プロパティ
	private string? userName;
	public string? UserName
	{
		get { return userName; }
		set
		{
			userName = value;
			OnPropertyChanged(nameof(UserName));
		}
	}

    // ユーザー情報用プロパティ
	private Person person = default!;
	public Person Person
	{
		get { return person; }
		set
		{
			person = value;
			OnPropertyChanged(nameof(Person));
		}
	}

}

Viewの実装

まだボタン押下時の処理を実装していませんが、まずはここまでの状態でViewを作成します。ViewDataContextViewModelのインスタンスを設定することで両者を紐づけることができます。

public partial class Main : Window
{
	public Main()
	{
		InitializeComponent();
        // DIとしてVMを受け取りDataContextにセットする
		DataContext = App.Current.Services.GetService<UserVM>();
	}
}

TextBox(19行目)」や「TextBlock(27行目)」コンポーネントに『Binding』で先ほどViewModelで作成したプロパティを指定します。これでViewModelのプロパティと画面をバインドさせることができます。

<Window x:Class="WpfTest.View.Main"
     (略)
        xmlns:local="clr-namespace:WpfTest.View"
        xmlns:vm="clr-namespace:WpfTest.ViewModel"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance vm:UserVM}"
        Title="Main" Height="450" Width="800">

    <Grid Width="400">
        <Grid.RowDefinitions>
          (略)
        </Grid.RowDefinitions>

        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="7*"/>
                <ColumnDefinition Width="3*"/>
            </Grid.ColumnDefinitions>
            <TextBox Text="{Binding UserName}" Grid.Column="0" Style="{StaticResource FormInput}"/>
            <Button Grid.Column="1" Style="{StaticResource Common}" Content="検索"/>
        </Grid>

        <Grid Grid.Row="1">
            <GroupBox Header="結果" Style="{StaticResource GroupBox_Common}">
                <StackPanel>
                    // ResultTextもUserNameなどと同様にViewModelにプロパティを作成しておく
                    <TextBlock Text="{Binding ResultText}" />
                    <TextBlock Text="{Binding Person.Name}" />
                    <TextBlock Text="{Binding Person.Email}"/>
                    <TextBlock Text="{Binding Person.Age}" />
                </StackPanel>
            </GroupBox>
        </Grid>
    </Grid>
</Window>

Commandの作成

ボタンクリック時に実行する処理を作成します。まずはICommandを継承したクラスを作成します。そして「CanExecuteChanged」、「CanExecute」、「Execute」を実装します。

CanExecuteメソッドは処理を実行するかどうかの判断ロジックを記載するメソッドです。今回は特に条件は付けませんので常にtrueを返します。実行したくない条件があればそれを記載してfalseを返します。

Executeメソッドが実際にボタンがクリックされたときに実行される処理を記載するメソッドです。ただし、具体的な処理は別のクラスに作成して、それを呼び出すように作成しています。

public class UserCommand : ICommand
{
	public UserVM UserVM { get; set; }

	public UserCommand(UserVM vm) => UserVM = vm;

	public event EventHandler? CanExecuteChanged
	{
		add { CommandManager.RequerySuggested += value; }
		remove { CommandManager.RequerySuggested -= value; }
	}

	public bool CanExecute(object? parameter) => true;

	public void Execute(object? parameter)
	{
        // ViewModelに以下のメソッドを作成
		UserVM.GetPersonInfo(parameter as string ?? "");
	}
}

ViewModelの更新

CommandクラスとViewModelを紐づけます。コンストラクタの中でUserCommandインスタンスを作成してプロパティに設定します。またユーザー名をもとにユーザーを検索する処理を記載したPersonInfoサービスをDIで利用します。さらに新たにGetPersonInfoメソッドを作成します。この中でユーザー取得処理を記載し、その情報をViewとバインドしているPersonプロパティに設定します。すると、プロパティの変更が検知されその内容がViewに反映されます。

public class UserVM : INotifyPropertyChanged
{

	private readonly IPersonInfo pi;

	public UserCommand UserCommand { get; set; }

	public UserVM(IPersonInfo personInfo)
	{
		UserCommand = new UserCommand(this);
		pi = personInfo;
	}

	public void GetPersonInfo(string name)
	{
		Person = pi.Get(name);
		if (string.IsNullOrEmpty(person.Name))
		{
			ResultText = "該当なし";
		}
		else
		{
			ResultText = "1件";
		}
	}

}

PersonInfoサービスで具体的なユーザー検索処理を記載します。ここはViewModelに記載しても問題ありませんが、ViewModelが膨らみ可読性も悪化してしまうので、サービスとして切り出しています。今回は簡単なサンプルですが、実際はDBにアクセスして情報を取得するような処理を記載する場所です。

public class PersonInfo : IPersonInfo
{

	private List<Person> _people = new List<Person>()
	{
		new Person{Name = "Test1",Age=10,Email="aaa.com"},
		new Person{Name = "Test2",Age=20,Email="bbb.com"},
		new Person{Name = "Test3",Age=30,Email="ccc.com"},
	};

	public Person Get(string name)
	{
		return _people.Find(n => n.Name == name) ?? new();
	}
}

Viewの更新(Commandのバインド)

ボタン実行時の処理を記載したメソッドを作成しましたので、これをViewから呼び出せるようにしていきます。

ButtonのCommand属性にUserCommandクラスをバインドします。またユーザー名を引数に受け取りますので、それをCommandParameter属性で指定します。ここではテキストボックスに記載した文字列を渡してあげる必要がありますので、テキストボックスに入力された文字を保持しているUserNameプロパティをバインドします。

<Button Grid.Column="1" Command="{Binding UserCommand}" CommandParameter="{Binding UserName}" ・・・/>

これで完成です。テキストボックスにTest1と入力して検索ボタンをクリックすると、結果欄にその情報が表示されます。

MVVM Toolkit

MVVMを素で作成するには、複数のクラスやプロパティを毎回作成しないといけないため、実装が冗長化しがちで可読性・保守性に課題が残ります。これをもっと簡単に実装できるライブラリを使って、どこまで簡潔に書けるようになるのか見ていきましょう

パッケージをインストール

CommunityToolkit.Mvvmをプロジェクトに追加します。

ViewModelの作成

ViewModelToolkitを使用しない場合と同様です。変わるのはViewModelになります。

まずは、ObservableObjectを継承したpartialクラスとしてViewModelを作成します。あとは、情報を保持するプロパティにはObservableProperty属性を、ボタンクリック時の処理などを実行するメソッドにはRelayCommand属性を付けるだけでよくなります。イベントやSetterなどを毎回記載する必要がなくなり非常にシンプルに記述できます。

internal partial class ToolkitVM : ObservableObject
{
	private readonly IPersonInfo _pi;
	public ToolkitVM(IPersonInfo personInfo)
	{
		_pi = personInfo;
	}

	[ObservableProperty]
	private string? _userName;
	[ObservableProperty]
	private string? _resultText;

	[ObservableProperty]
	private Person _person = default!;

	[RelayCommand]
	private void GetPersonInfo(string name)
	{
		Person = _pi.Get(name);
		if (string.IsNullOrEmpty(Person.Name))
		{
			ResultText = "該当なし";
		}
		else
		{
			ResultText = "1件";
		}
	}

}

DI

ViewModelServiceの利用にはDependancy Injectionを用いています。詳細は以下を確認ください。ここでは実装サンプルだけ記載します。

// Microsoft.Extensions.DependencyInjectionをインストール

namespace WpfTest;

/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
	public App()
	{
		Services = ConfigureServices();
		this.InitializeComponent();
	}
	public new static App Current => (App)Application.Current;

	public IServiceProvider Services { get; } = default!;

	private static IServiceProvider ConfigureServices()
	{
		var services = new ServiceCollection();
		services.AddSingleton<IPersonInfo, PersonInfo>();
		services.AddTransient<UserVM>();
		services.AddTransient<ToolkitVM>();

		return services.BuildServiceProvider();
	}

}

まとめ

従来のMVVM実装では、INotifyPropertyChangedICommandの実装が不可欠であり、実装量の多さが開発効率に影響を与える要因となっていました。

MVVM Toolkitを活用することで、プロパティ変更通知やコマンド定義をアノテーションベースで簡素化でき、保守性や可読性が大幅に向上します。また、依存性注入(DI)を併用することで、アプリケーション全体のアーキテクチャも柔軟かつスケーラブルに設計することが可能です。

MVVM Toolkitは、MVVMパターンを採用するWPF開発者にとって非常に有用な選択肢となるでしょう。

コメント