C#でMVVMなコーディング

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

目次

ToolkitをNuGetで取得

ツールメニューからNuGetパッケージの管理を開き、参照タブでmvvm toolkitを検索すると出てくるライブラリをインストールする。

MVVM Toolkitをコードで使う

usingディレクティブ追加

using CommunityToolkit.Mvvm;

何がいいの?

Windows Form時代

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メソッドの中に本当に必要なのか?と考えるとそれはNoであり、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にはこのようなコード生成以外にも、コントロールへの入力値を自動的に規制したり、範囲外入力の場合はエラー表示させるなどを、ほぼコード書かずに実施することができる。
それはまた別に説明する。

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

よかったらシェアしてね!

この記事を書いた人

コメント

コメントする

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

目次