0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MVVMでボタンを押したときの処理はどこに書く?|ClickイベントからViewModelへ移す基本【外伝G32】

0
Posted at

この記事は、WPF / .NET MAUIでMVVMを書き始めた人向けに、ボタンを押したときの処理をどこに書くかを整理する記事です。

前回の記事では、CommunityToolkit.Mvvmを使って ObservableObjectObservableProperty の基本を整理しました。

今回はその続きとして、保存ボタンのような操作をViewModelに移す流れを見ていきます。

連載全体はこちらです。

この記事では、Commandの型名を細かく覚えることよりも、

  • ボタンを押したときの処理をどこに書くか
  • Clickイベントに書いていた処理をどうViewModelへ移すか
  • ボタン連打や入力不足をどう防ぐか

を中心に整理します。

この記事で扱うこと

この記事では、MVVMでボタン操作を書くときの基本を扱います。

  • Clickイベントに処理を書き続けると何が困るか
  • ボタンを押したときの処理をViewModelへ移す考え方
  • Button.Command からViewModelの処理を呼ぶ流れ
  • CommunityToolkit.Mvvmを使ったCommandの書き方
  • ボタンの有効・無効をViewModelで管理する方法
  • 保存中の二重押しを防ぐ方法
  • Commandが動かないときに見るところ

画面遷移、DI、Prism、ReactiveProperty、複雑な入力検証までは深く扱いません。

まずは、保存ボタンを例にして、ClickイベントからViewModelへ処理を移す流れを見ます。

最初に結論

MVVMでは、ボタンを押したときの業務処理は基本的にViewModelに書きます。

画面側のClickイベントには、なるべく処理を書きません。

ただし、すべてをViewModelに寄せればよいわけではありません。

判断基準はこのようになります。

処理 書く場所
入力値を保存する ViewModel
APIを呼び出す ViewModel
DBに登録する ViewModel
入力チェックをする ViewModel
ボタンの有効・無効を切り替える ViewModel
画面のアニメーションを動かす View
フォーカスを移動する View
スクロール位置を調整する View
ダイアログを表示する View、またはサービス経由

大事なのは、ボタンを押したあとの「意味のある処理」をViewModelに置くことです。

保存する、検索する、削除する、送信する、といった処理はViewModel側に置いた方が扱いやすくなります。

今回作る画面

今回作るのは、名前を入力して保存するだけの小さな画面です。

名前
[              ]
[ 保存 ]

保存しました。

この画面では、次の3つをViewModelにつなぎます。

画面側 ViewModel側 役割
TextBox.Text / Entry.Text UserName 入力された名前
Button.Command SaveCommand 保存ボタンの処理
TextBlock.Text / Label.Text Message 結果メッセージ

MVVMでは、このつながりを BindingCommand で作ります。

WPFなら次のような形です。

<TextBox Text="{Binding UserName}" />
<Button Command="{Binding SaveCommand}" />
<TextBlock Text="{Binding Message}" />

.NET MAUIなら次のような形です。

<Entry Text="{Binding UserName}" />
<Button Command="{Binding SaveCommand}" />
<Label Text="{Binding Message}" />

ポイントは、ボタンのClickイベントに保存処理を書かないことです。

画面側は Command を呼び、処理の中身はViewModelに置きます。

Clickイベントに書くとどうなるか

最初は、ボタンのClickイベントに処理を書きたくなります。

たとえばWPFなら、このような書き方です。

<Button Content="保存" Click="SaveButton_Click" />
private void SaveButton_Click(object sender, RoutedEventArgs e)
{
    if (string.IsNullOrWhiteSpace(NameTextBox.Text))
    {
        MessageTextBlock.Text = "名前を入力してください。";
        return;
    }

    Save(NameTextBox.Text);

    MessageTextBlock.Text = "保存しました。";
}

小さい画面なら、これでも動きます。

ただ、この書き方を続けると、画面側のコードが少しずつ重くなります。

たとえば、次のような状態になりやすいです。

  • TextBoxの値を画面側から直接読む
  • 入力チェックが画面側に増える
  • 保存処理が画面側に入る
  • 処理中の二重押し防止も画面側に入る
  • エラー表示の制御も画面側に入る

こうなると、画面の見た目と処理の中身が混ざります。

最初は問題なくても、入力項目が増えたり、保存処理が複雑になったりすると、どこを直せばよいか分かりにくくなります。

Clickイベントに書くこと自体が、すべて悪いわけではありません。

問題になりやすいのは、Clickイベントの中に入力チェック、保存処理、通信処理、DB更新などが増えていくことです。

ViewModelに移すと何が変わるか

MVVMでは、画面で入力された値をViewModelのプロパティに持たせます。

そして、ボタンを押したときの処理もViewModelに置きます。

画面は「ボタンが押された」という操作をViewModelへ渡すだけです。

このときに使うのがCommandです。

Commandは、ボタンなどの操作とViewModelの処理をつなぐための仕組みです。

ざっくり言うと、Clickイベントの代わりに使うものです。

Button.Command
  ↓ Binding
SaveCommand
  ↓
Save()
  ↓
Message更新
  ↓
Bindingで画面へ反映

この流れが見えると、MVVMで「どこからどこへつながっているか」を追いやすくなります。

CommunityToolkit.Mvvmを使う

この記事では、CommunityToolkit.Mvvmを使います。

NuGetで次のパッケージを追加します。

CommunityToolkit.Mvvm

ViewModelは次のように書けます。

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

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

    [ObservableProperty]
    private string? message;

    [RelayCommand]
    private void Save()
    {
        if (string.IsNullOrWhiteSpace(UserName))
        {
            Message = "名前を入力してください。";
            return;
        }

        // ここに保存処理を書く
        Message = "保存しました。";
    }
}

このコードでは、UserNameMessageSaveCommand が使えるようになります。

SaveCommand は、Save メソッドから自動生成されます。

つまり、ボタン側からは SaveCommand を呼び出せばよいです。

CommunityToolkit.Mvvmの属性を使う場合、ViewModelは partial class にします。

partial を付け忘れると、生成コードとうまくつながらず、プロパティやCommandが使えない原因になります。

WPFの場合

WPFでは、画面のDataContextにViewModelを設定します。

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

        DataContext = new MainViewModel();
    }
}

XAMLでは、TextBoxとButtonをViewModelにバインドします。

<StackPanel Margin="20">
    <TextBox
        Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}" />

    <Button
        Content="保存"
        Command="{Binding SaveCommand}"
        Margin="0,8,0,0" />

    <TextBlock
        Text="{Binding Message}"
        Margin="0,8,0,0" />
</StackPanel>

これで、ボタンを押すとViewModelの Save メソッドが動きます。

画面側のClickイベントは不要です。

.NET MAUIの場合

.NET MAUIでも考え方は同じです。

違うのは、WPFのDataContextにあたるものがBindingContextになる点です。

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();

        BindingContext = new MainViewModel();
    }
}

XAMLは次のようになります。

<VerticalStackLayout Padding="20">
    <Entry
        Text="{Binding UserName}" />

    <Button
        Text="保存"
        Command="{Binding SaveCommand}" />

    <Label
        Text="{Binding Message}" />
</VerticalStackLayout>

WPFでも.NET MAUIでも、考え方は同じです。

画面側はCommandを呼び出すだけです。

処理の中身はViewModelに置きます。

WPF       : DataContext
.NET MAUI : BindingContext

名前は違いますが、どちらも「この画面はどのViewModelを見るか」を設定するものです。

ボタンを押せる条件をViewModelで管理する

保存ボタンは、いつでも押せればよいとは限りません。

たとえば、名前が空なら押せないようにしたい場合があります。

その場合は、Commandの実行可否をViewModel側で管理します。

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

public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    private string? userName;

    [ObservableProperty]
    private string? message;

    private bool CanSave()
    {
        return !string.IsNullOrWhiteSpace(UserName);
    }

    [RelayCommand(CanExecute = nameof(CanSave))]
    private void Save()
    {
        Message = "保存しました。";
    }
}

この例では、UserName が空の場合、保存ボタンは押せません。

UserName に値が入ると、保存ボタンを押せるようになります。

ポイントは、ボタンを押せるかどうかもViewModelに書いていることです。

画面側でTextBoxを見て、ボタンの有効・無効を切り替える必要はありません。

UserName が変わったときにボタンの有効・無効も更新したい場合は、[NotifyCanExecuteChangedFor(nameof(SaveCommand))] を付けます。

これを忘れると、入力値を変えてもボタンの状態が変わらないことがあります。

非同期処理ならasyncにする

保存処理がすぐ終わるとは限りません。

APIを呼ぶ、ファイルを読み込む、DBに登録する、といった処理は時間がかかることがあります。

その場合は、非同期処理にします。

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

public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    private string? userName;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    private bool isBusy;

    [ObservableProperty]
    private string? message;

    private bool CanSave()
    {
        return !IsBusy && !string.IsNullOrWhiteSpace(UserName);
    }

    [RelayCommand(CanExecute = nameof(CanSave))]
    private async Task SaveAsync()
    {
        if (!CanSave())
        {
            return;
        }

        try
        {
            IsBusy = true;
            Message = "保存しています...";

            // 実際の処理ではAPI呼び出しやファイル保存などを行う
            await Task.Delay(1000);

            Message = "保存しました。";
        }
        catch (Exception ex)
        {
            Message = $"保存に失敗しました。{ex.Message}";
        }
        finally
        {
            IsBusy = false;
        }
    }
}

SaveAsync メソッドからは、SaveCommand が生成されます。

ボタン側の書き方は変わりません。

WPFなら次のように書きます。

<Button
    Content="保存"
    Command="{Binding SaveCommand}" />

.NET MAUIなら次のように書きます。

<Button
    Text="保存"
    Command="{Binding SaveCommand}" />

メソッド名が SaveAsync でも、生成されるCommand名は基本的に SaveCommand です。

XAML側で SaveAsyncCommand と書かないように注意します。

二重押しを防ぐ

保存処理でよくある問題が、ボタンの二重押しです。

保存中にもう一度ボタンを押せてしまうと、同じデータが二重登録されることがあります。

そのため、処理中はボタンを押せないようにします。

先ほどのコードでは、IsBusy を使って二重押しを防いでいます。

private bool CanSave()
{
    return !IsBusy && !string.IsNullOrWhiteSpace(UserName);
}

IsBusytrue の間は、保存ボタンを押せません。

try
{
    IsBusy = true;

    await Task.Delay(1000);
}
finally
{
    IsBusy = false;
}

finallyIsBusy = false に戻しているのは、途中で例外が発生しても処理中状態を解除するためです。

この形にしておくと、成功しても失敗してもボタンが戻ります。

DB登録、注文確定、ファイル書き込みのような処理では、二重押し対策を入れた方が安全です。

「保存中にもう一度押せる状態」は、実務では不具合につながりやすいです。

async voidは避ける

Clickイベントで非同期処理を書くと、次のようなコードになりがちです。

private async void SaveButton_Click(object sender, RoutedEventArgs e)
{
    await SaveAsync();
}

イベントハンドラなので async void 自体は書けます。

ただ、保存処理や検索処理の本体を async void に寄せていくと、例外処理やテストがしにくくなります。

ViewModelでは、基本的に async Task で書きます。

[RelayCommand]
private async Task SaveAsync()
{
    await Task.Delay(1000);
}

この方が、非同期処理として扱いやすくなります。

Commandが動かないときに見るところ

Commandが動かないときは、次の順番で確認すると切り分けやすいです。

DataContextまたはBindingContextが設定されているか

WPFならDataContextです。

DataContext = new MainViewModel();

.NET MAUIならBindingContextです。

BindingContext = new MainViewModel();

ここが設定されていないと、XAMLのBinding先が見つかりません。

Command名が合っているか

Save メソッドに [RelayCommand] を付けると、生成される名前は SaveCommand です。

[RelayCommand]
private void Save()
{
}

XAMLでは次のように書きます。

<Button Command="{Binding SaveCommand}" />

Save ではなく、SaveCommand です。

ViewModelがpartial classになっているか

CommunityToolkit.Mvvmの属性を使う場合、ViewModelは partial にします。

public partial class MainViewModel : ObservableObject
{
}

partial がないと、生成コードとうまくつながりません。

ObservableObjectを継承しているか

画面更新を通知するには、ViewModelが ObservableObject を継承している必要があります。

public partial class MainViewModel : ObservableObject
{
}

入力値が更新されているか

WPFのTextBoxでは、必要に応じて UpdateSourceTrigger=PropertyChanged を指定します。

<TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}" />

これがないと、入力中の値がすぐにViewModelへ反映されないことがあります。

Commandが動かないときは、いきなりCommandの中身を疑うより、先に次の3つを見ると早いです。

  1. ViewModelが画面に設定されているか
  2. XAMLのCommand名が合っているか
  3. ViewModelが partial になっているか

ViewModelに書かない方がよい処理

ボタンを押したときの処理をViewModelに書くといっても、画面に強く依存する処理まで全部ViewModelに入れる必要はありません。

たとえば、次のような処理です。

  • TextBoxにフォーカスを当てる
  • スクロール位置を変える
  • アニメーションを開始する
  • 画面固有の表示状態を細かく切り替える

これらは画面の都合なので、View側に残してもよいです。

ViewModelに置きたいのは、画面が変わっても意味が変わらない処理です。

保存、検索、削除、入力チェック、処理中判定などはViewModelに置く候補になります。

実務での分け方

迷ったときは、次の基準で考えると分けやすいです。

判断 書く場所
画面が変わっても同じ意味を持つ処理 ViewModel
入力値から結果を判断する処理 ViewModel
保存、検索、削除などの操作 ViewModel
ボタンを押せるかどうかの条件 ViewModel
見た目だけの制御 View
フォーカス、スクロール、アニメーション View
OSや画面部品に強く依存する処理 View、または専用サービス

最初から完璧に分けようとしなくても大丈夫です。

まずは、保存、検索、削除のような処理をClickイベントからViewModelへ移すだけでも、コードの見通しはかなり変わります。

最小構成のサンプル

最後に、保存ボタンの最小構成をまとめます。

ViewModelです。

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

public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    private string? userName;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    private bool isBusy;

    [ObservableProperty]
    private string? message;

    private bool CanSave()
    {
        return !IsBusy && !string.IsNullOrWhiteSpace(UserName);
    }

    [RelayCommand(CanExecute = nameof(CanSave))]
    private async Task SaveAsync()
    {
        if (!CanSave())
        {
            return;
        }

        try
        {
            IsBusy = true;
            Message = "保存しています...";

            await Task.Delay(1000);

            Message = "保存しました。";
        }
        catch (Exception ex)
        {
            Message = $"保存に失敗しました。{ex.Message}";
        }
        finally
        {
            IsBusy = false;
        }
    }
}

WPFのXAMLです。

<StackPanel Margin="20">
    <TextBox
        Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}" />

    <Button
        Content="保存"
        Command="{Binding SaveCommand}"
        Margin="0,8,0,0" />

    <TextBlock
        Text="{Binding Message}"
        Margin="0,8,0,0" />
</StackPanel>

.NET MAUIのXAMLです。

<VerticalStackLayout Padding="20">
    <Entry
        Text="{Binding UserName}" />

    <Button
        Text="保存"
        Command="{Binding SaveCommand}" />

    <Label
        Text="{Binding Message}" />
</VerticalStackLayout>

このサンプルで見てほしいポイントは、画面側にClickイベントを書いていないことです。

ボタンはCommandを呼び、処理の中身はViewModelに置いています。

まとめ

MVVMでは、ボタンを押したときの業務処理はViewModelに書きます。

Clickイベントに直接書くと、画面のコードに入力チェックや保存処理が集まりやすくなります。

小さい画面では問題になりにくいですが、機能が増えるほど修正しにくくなります。

基本の考え方は次のとおりです。

やりたいこと 書き方
ボタンを押したときに処理を実行したい Commandを使う
入力値をViewModelに持たせたい ObservablePropertyを使う
ボタンの有効・無効を切り替えたい CanExecuteを使う
非同期処理を書きたい async Taskで書く
二重押しを防ぎたい IsBusyとCanExecuteを組み合わせる

最初は、Clickイベントをすべてなくすことを目標にしなくてもよいです。

まずは、保存、検索、削除のような意味のある処理からViewModelへ移すと、MVVMの形が分かりやすくなります。

関連記事

MVVMの全体像はこちらです。

XAMLの読み方を先に整理する場合はこちらです。

WPF側の基本実装はこちらです。

.NET MAUI側の基本実装はこちらです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?