C#でMVVMなコーディング

今さらWPFでWindowsアプリを作り始めたが、せっかくなのでC#MVVMのコーディング手法を身に着けようと奮闘し、冗長さやコードの多さに疲弊しMVVM Toolkitを使うとかなり改善されたので、使い方を備忘録として残しておく。

目次

ToolkitをNuGetで取得

C#でMVVMなコーディングをするためには、ツールメニューからNuGetパッケージの管理を開き、参照タブでmvvm toolkitを検索すると出てくるライブラリをインストールする。

MVVM Toolkitをコードで使う

usingディレクティブ追加

using CommunityToolkit.Mvvm;

何がいいの?

Windows Form時代

WPFでの記述例

MVVMのコードをXAMLから呼び出すとき、Windows Forms(昔のテキストボックスやボタンをペタペタ張って、クリックイベントとかにコードを書く方法)だと、WPFでは以下のようになる。

この例では入力された名前と年齢を使って挨拶を返す。
WPFではデザインをxxx.xamlファイルに記述、そのイベントなどをxxx.xaml.csに記述する。

MainWindow.xaml

<Window x:Class="SpaghettiDemo.MainWindow" ...>
    <StackPanel>
        <TextBox x:Name="NameBox" PlaceholderText="名前"/>
        <TextBox x:Name="AgeBox" PlaceholderText="年齢"/>
        <TextBlock x:Name="ErrorText" Foreground="Red"/>
        <Button Content="挨拶する" Click="OnGreetClick"/>
        <TextBlock x:Name="GreetingText"/>
        <ProgressBar x:Name="Progress" Visibility="Collapsed" IsIndeterminate="True"/>
    </StackPanel>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private async void OnGreetClick(object sender, RoutedEventArgs e)
    {
        ErrorText.Text = "";
        GreetingText.Text = "";
        Progress.Visibility = Visibility.Visible;

        string name = NameBox.Text;
        string ageText = AgeBox.Text;

        if (string.IsNullOrWhiteSpace(name))
        {
            ErrorText.Text = "名前を入力してください";
            Progress.Visibility = Visibility.Collapsed;
            return;
        }
        if (!int.TryParse(ageText, out int age) || age < 0)
        {
            ErrorText.Text = "年齢を正しく入力してください";
            Progress.Visibility = Visibility.Collapsed;
            return;
        }
        await Task.Delay(1000); // 処理のフリ
        GreetingText.Text = $"こんにちは、{name}さん!あなたは{age}歳ですね。";
        Progress.Visibility = Visibility.Collapsed;
    }
}
何が問題なのか

テキストボックスの表示内容やロジックをすべて直書きしている。これだと、ユーザに見せる・操作させるための制御コードと、その操作結果を処理するコードが混在した、非常に読みにくいコードが生まれてしまう。

さらに、部品名を使って直接データにアクセスしている。そのため、性別を追加したり、住所や電話番号といった項目を増やした場合、OnGreetClick メソッドの中身は今後どんどん肥大化していくことが容易に想像できる。

しかし、「名前が入力されているか」「年齢が正しい範囲に収まっているか」といった入力値の検証処理は、本当に OnGreetClick メソッドの責務だろうか。

OnGreetClick は「挨拶を返すボタン」で、本来は挨拶を返すことだけに集中すべきである。
それにもかかわらず、入力チェックまでを抱え込んでしまっている。

この状態で、たとえば「入力値をテキストファイルに保存する」ボタンを追加すると、「名前が入力されているか」「年齢が範囲内か」といった同じ検証コードを、別の場所に再度書くことになる。

結果として、ユーザの入力値とそれを処理するコードが強く結びいてしまう。責務を分離できない構造となり、重複したコードが大量に発生しやすい状態になってしまう。

まず、単純なMVVM

MVVM(Model-View-ViewModel)は

部品役割・責務例・イメージ
Model業務ロジックやデータそのものデータベース読み書き、APIレスポンス、純データ構造
ViewUI定義と表示だけを担当XAML、HTML、ButtonやTextBoxなどのUI
ViewModelViewとModelの仲介役・ロジック記述担当プロパティ値管理、バリデーション、コマンド実装

の3つにコードを分割し、それぞれの役割を分けてコードを書こうっていうもの。

先の例では

  • テキストボックスやボタンを配置したxamlはView
  • 入力値の確認や範囲規制はViewModdel
  • その値を使って行う処理はModel

に分けて書くことになる。

MainWindow.xaml

<Window ...>
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <StackPanel>
        <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" PlaceholderText="名前"/>
        <TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged}" PlaceholderText="年齢"/>
        <TextBlock Text="{Binding ErrorMessage}" Foreground="Red"/>
        <Button Content="挨拶する" Command="{Binding GreetCommand}"/>
        <TextBlock Text="{Binding Greeting}"/>
        <ProgressBar IsIndeterminate="True" Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibilityConverter}}"/>
    </StackPanel>
</Window>

MainViewModel.cs

public class MainViewModel : INotifyPropertyChanged
{
    private string _name;
    public string Name { get => _name; set { _name = value; OnPropertyChanged(nameof(Name)); } }

    private string _age;
    public string Age { get => _age; set { _age = value; OnPropertyChanged(nameof(Age)); } }

    private string _errorMessage;
    public string ErrorMessage { get => _errorMessage; set { _errorMessage = value; OnPropertyChanged(nameof(ErrorMessage)); } }

    private string _greeting;
    public string Greeting { get => _greeting; set { _greeting = value; OnPropertyChanged(nameof(Greeting)); } }

    private bool _isBusy;
    public bool IsBusy { get => _isBusy; set { _isBusy = value; OnPropertyChanged(nameof(IsBusy)); } }

    public ICommand GreetCommand { get; }

    public MainViewModel()
    {
        GreetCommand = new RelayCommand(async () => await GreetAsync());
    }

    private async Task GreetAsync()
    {
        ErrorMessage = "";
        Greeting = "";
        IsBusy = true;
        try
        {
            if (string.IsNullOrWhiteSpace(Name))
            {
                ErrorMessage = "名前を入力してください";
                return;
            }
            if (!int.TryParse(Age, out int age) || age < 0)
            {
                ErrorMessage = "年齢を正しく入力してください";
                return;
            }
            await Task.Delay(1000);
            Greeting = $"こんにちは、{Name}さん!あなたは{age}歳ですね。";
        }
        finally
        {
            IsBusy = false;
        }
    }

    // INotifyPropertyChanged実装は省略
}

これでMVVMに分割でき、役割が明確になったが明らかにコードが増えてるし同じような書き方のものがMainViewModel.csにいくつか発生している。
ここがポイントで、同じような内容が複数回ということは処理が自動化できるということで、それを担うのがMVVM Toolkit。

MVVM ToolkitでMVVM

MainWindow.xaml(前出と同じ)

<Window ...>
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <StackPanel>
        <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" PlaceholderText="名前"/>
        <TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged}" PlaceholderText="年齢"/>
        <TextBlock Text="{Binding ErrorMessage}" Foreground="Red"/>
        <Button Content="挨拶する" Command="{Binding GreetCommand}"/>
        <TextBlock Text="{Binding Greeting}"/>
        <ProgressBar IsIndeterminate="True" Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibilityConverter}}"/>
    </StackPanel>
</Window>

MainViewModel.cs

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

public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    private string name;

    [ObservableProperty]
    private string age;

    [ObservableProperty]
    private string errorMessage;

    [ObservableProperty]
    private string greeting;

    [ObservableProperty]
    private bool isBusy;

    [RelayCommand]
    private async Task GreetAsync()
    {
        ErrorMessage = "";
        Greeting = "";
        IsBusy = true;
        try
        {
            if (string.IsNullOrWhiteSpace(Name))
            {
                ErrorMessage = "名前を入力してください";
                return;
            }
            if (!int.TryParse(Age, out int ageValue) || ageValue < 0)
            {
                ErrorMessage = "年齢を正しく入力してください";
                return;
            }
            await Task.Delay(1000);
            Greeting = $"こんにちは、{Name}さん!あなたは{ageValue}歳ですね。";
        }
        finally
        {
            IsBusy = false;
        }
    }
}

このように、

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

を先頭で宣言すると、

[ObservableProperty]
[RelayCommand]

などが使えるようになる。この宣言を行うと、その直下のローカル変数の名前を認識し、xamlのコントロール(TextBoxやButton)の値の取得・設定を行うコードが内部に自動生成される。

応用

MVVM Toolkitにはこのようなコード生成以外にも、コントロールへの入力値を自動的に規制したり、範囲外入力の場合はエラー表示させるなどを、ほぼコード書かずに実施することができる。
それはまた別に説明する。

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

目次