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?

.NET MAUIのMVVM入門|BindingContextとCommandで画面処理を分ける【外伝G30】

0
Posted at

この記事は .NET MAUIのMVVM実装編 です。

先に全体像を押さえるなら G27: MVVM共通編
XAMLの読み方を確認するなら G28: XAML読み方辞典
WPF側の実装を見るなら G29: WPF実装編 から読むとつながりやすいです。

.NET MAUIでMVVMを書くときは、BindingContextBinding の関係を先に押さえておくと、XAMLとViewModelのつながりを追いやすくなります。

ここが曖昧なままだと、ViewModelにプロパティを書いても、XAML側が何を見ているのか分かりにくくなります。

たとえば、XAMLに次のように書いたとします。

<Entry Text="{Binding CustomerName}" />

この CustomerName は、XAMLの中だけで完結する名前ではありません。
ViewModel側にあるプロパティ名です。

public string CustomerName { get; set; } = "";

XAMLがViewModelを見るために使う入口が BindingContext です。

BindingContext = new CustomerViewModel();

.NET MAUIのMVVMでは、まず次のつながりを作ります。

View
  ↓ BindingContext
ViewModel
  ↓ Binding
XAMLの表示・入力・操作

この記事では、小さな入力画面を作りながら、.NET MAUIのMVVMでよく使う次の要素を整理します。

  • BindingContext
  • Binding
  • Button.Command
  • INotifyPropertyChanged
  • RelayCommand

この記事で扱うこと

この記事では、.NET MAUIでMVVMを組むときの基本を扱います。

  • BindingContext でViewModelを画面に渡す
  • Entry.Text とViewModelのプロパティをBindingする
  • Label.Text にViewModelの値を表示する
  • Button.Command からViewModelの処理を呼ぶ
  • INotifyPropertyChanged で画面へ変更を伝える
  • コードビハインドに書くもの、書かない方がよいものを分ける

Shell Navigation、DI、CommunityToolkit.Mvvmの属性構文、非同期Commandまでは深く扱いません。

まずは、.NET MAUI標準の仕組みでMVVMの流れを確認します。


今回作る画面

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

顧客名
[              ]
[ 保存 ]

保存しました。

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

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

.NET MAUIでは、このつながりを Binding で書きます。

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

ただし、Bindingを書くだけではViewModelにはつながりません。
画面側にViewModelを渡す必要があります。

その入口が BindingContext です。

BindingContext = new CustomerViewModel();

.NET MAUIのMVVMでは、まずここを押さえると理解しやすくなります。
BindingContext でViewModelを渡す。
Binding でViewModelのプロパティを見る。
Command でViewModelの処理を呼ぶ。


BindingContextはBindingが見るViewModel

BindingContext は、Bindingが値を探しに行く相手です。

たとえば、画面側で次のように設定します。

BindingContext = new CustomerViewModel();

この状態で、XAMLに次のBindingがあるとします。

<Entry Text="{Binding CustomerName}" />

この場合、.NET MAUIは BindingContext に設定された CustomerViewModel の中から CustomerName を探します。

この1行は、次の順につながっています。

Entry.Text
  ↓
{Binding CustomerName}
  ↓
BindingContext に設定された CustomerViewModel
  ↓
CustomerViewModel.CustomerName

つまり、BindingContext を設定することで、XAMLとViewModelがつながります。

Bindingを書いているのに値が出ない場合、まず BindingContext を確認します。
XAML側の名前が合っていても、ViewModelが渡されていなければBindingは期待通りに動きません。


ファイルの置き場所

今回のサンプルでは、役割ごとにファイルを分けます。

SampleApp
├ Views
│  └ MainPage.xaml
├ ViewModels
│  └ CustomerViewModel.cs
├ Services
│  └ CustomerService.cs
├ Models
│  └ SaveResult.cs
└ Commands
   └ RelayCommand.cs

目的は、コードを探しやすくすることです。

場所 置くもの
Views XAML画面
ViewModels 画面に表示する値、画面操作の入口
Services 保存や取得などの処理
Models 処理結果やデータ
Commands Command用の共通クラス

大事なのはフォルダ名そのものではありません。
見た目は Views、入力値やメッセージは ViewModels、保存判断は Services のように、変更理由が違うものを同じ場所に詰め込みすぎないことです。


ViewModelを書く

ここから、画面とつながるViewModelを書きます。

今回のViewModelに必要なのは3つです。

メンバー XAML側 役割
CustomerName Entry.Text 入力された顧客名
Message Label.Text 画面に表示するメッセージ
SaveCommand Button.Command 保存ボタンの処理
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using SampleApp.Commands;
using SampleApp.Services;

namespace SampleApp.ViewModels;

public class CustomerViewModel : INotifyPropertyChanged
{
    private readonly CustomerService _customerService = new();

    private string _customerName = "";
    private string _message = "";

    public CustomerViewModel()
    {
        SaveCommand = new RelayCommand(Save);
    }

    public string CustomerName
    {
        get => _customerName;
        set
        {
            if (_customerName == value)
            {
                return;
            }

            _customerName = value;
            OnPropertyChanged();
        }
    }

    public string Message
    {
        get => _message;
        set
        {
            if (_message == value)
            {
                return;
            }

            _message = value;
            OnPropertyChanged();
        }
    }

    public ICommand SaveCommand { get; }

    private void Save()
    {
        var result = _customerService.Save(CustomerName);

        Message = result.Success
            ? "保存しました。"
            : result.Message;
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        // BindingしているViewへ「値が変わった」ことを通知する
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

このViewModelは、XAML側から次のように参照されます。

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

XAMLのBinding名とViewModelのプロパティ名は対応します。
{Binding CustomerName} と書くなら、ViewModel側に CustomerName が必要です。


画面更新にはINotifyPropertyChangedを使う

ViewModelの値を変えたとき、画面にも反映したい場合があります。

たとえば、保存後に次のように Message を変えます。

Message = "保存しました。";

この変更を画面へ伝えるために、INotifyPropertyChanged を使います。

public event PropertyChangedEventHandler? PropertyChanged;

private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Message のsetterでは、値を変えたあとに OnPropertyChanged() を呼んでいます。

public string Message
{
    get => _message;
    set
    {
        if (_message == value)
        {
            return;
        }

        _message = value;
        OnPropertyChanged();
    }
}

これで、Label.Text にBindingしている画面側へ変更が伝わります。

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

流れはこうです。

Message を変更
  ↓
OnPropertyChanged()
  ↓
Bindingへ通知
  ↓
Label.Text が更新される

ViewModelの値を変更しても画面が変わらない場合、OnPropertyChanged() を呼んでいるか確認します。


RelayCommandを書く

.NET MAUIの Button.Command にBindingするには、ViewModel側に ICommand を用意します。

ここでは、最小限の RelayCommand を作ります。

using System;
using System.Windows.Input;

namespace SampleApp.Commands;

public class RelayCommand : ICommand
{
    private readonly Action _execute;
    private readonly Func<bool>? _canExecute;

    public RelayCommand(Action execute, Func<bool>? canExecute = null)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute;
    }

    public bool CanExecute(object? parameter)
    {
        return _canExecute?.Invoke() ?? true;
    }

    public void Execute(object? parameter)
    {
        _execute();
    }

    public event EventHandler? CanExecuteChanged;

    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
}

ViewModelでは、この RelayCommandSave メソッドを渡しています。

public CustomerViewModel()
{
    SaveCommand = new RelayCommand(Save);
}

XAML側では、ボタンの Command にBindingします。

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

流れはこうです。

Button.Command
  ↓ Binding
SaveCommand
  ↓ Execute
Save()

この RelayCommand は、Commandの流れを説明するための最小版です。
ボタンの有効/無効を動的に切り替える場合は、CanExecuteCanExecuteChanged の扱いも追加で考えます。


保存処理はServiceへ分ける

保存処理そのものは、ViewModelに直接書きすぎないようにします。

今回は、CustomerService に分けます。

using SampleApp.Models;

namespace SampleApp.Services;

public class CustomerService
{
    public SaveResult Save(string customerName)
    {
        if (string.IsNullOrWhiteSpace(customerName))
        {
            return SaveResult.Failed("顧客名を入力してください。");
        }

        // 本来はここでDB保存やAPI呼び出しなどを行う

        return SaveResult.Succeeded();
    }
}

結果を表すクラスも用意します。

namespace SampleApp.Models;

public class SaveResult
{
    public bool Success { get; }
    public string Message { get; }

    private SaveResult(bool success, string message)
    {
        Success = success;
        Message = message;
    }

    public static SaveResult Succeeded()
    {
        return new SaveResult(true, "");
    }

    public static SaveResult Failed(string message)
    {
        return new SaveResult(false, message);
    }
}

ViewModelでは、Serviceの結果を受け取り、画面状態である Message に反映します。

private void Save()
{
    var result = _customerService.Save(CustomerName);

    Message = result.Success
        ? "保存しました。"
        : result.Message;
}

Serviceで判定する。
ViewModelで画面に出す状態へ変える。
XAMLはBindingで表示する。
この流れにすると、責務が混ざりにくくなります。


Viewを書く

次に、.NET MAUIのXAMLを書きます。

MainPage.xaml です。

<ContentPage
    x:Class="SampleApp.Views.MainPage"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    Title="Customer Sample">

    <VerticalStackLayout
        Padding="20"
        Spacing="12">

        <Label Text="顧客名" />

        <Entry
            Text="{Binding CustomerName}"
            Placeholder="顧客名を入力" />

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

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

    </VerticalStackLayout>
</ContentPage>

このXAMLとViewModelの対応はこうです。

XAML ViewModel 役割
Entry.Text CustomerName 顧客名の入力
Button.Command SaveCommand 保存処理の実行
Label.Text Message 結果メッセージの表示

XAMLの {Binding CustomerName} は、ViewModelの CustomerName を見に行きます。
そのためには、Viewの BindingContext にViewModelが設定されている必要があります。


BindingContextにViewModelを設定する

XAMLにBindingを書いたら、画面側にViewModelを渡します。

MainPage.xaml.cs で、BindingContext にViewModelを設定します。

using SampleApp.ViewModels;

namespace SampleApp.Views;

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

        BindingContext = new CustomerViewModel();
    }
}

これで、XAMLのBindingが CustomerViewModel を参照できるようになります。

MainPage
  ↓ BindingContext
CustomerViewModel
  ↓ Binding
CustomerName / Message / SaveCommand

XAMLのBindingが正しく見えても、BindingContext が設定されていなければ値は出ません。
.NET MAUIのBinding不調では、まず BindingContext を確認します。


Entry.TextのBinding

今回のXAMLでは、入力欄を次のように書いています。

<Entry Text="{Binding CustomerName}" />

これは、EntryText と、ViewModelの CustomerName をつなぐ指定です。

Entry.Text
  ↓ Binding
CustomerName
  ↓ BindingContext
CustomerViewModel.CustomerName

入力された文字をViewModel側で扱いたい場合、ViewModelに対応するプロパティを用意します。

public string CustomerName
{
    get => _customerName;
    set
    {
        if (_customerName == value)
        {
            return;
        }

        _customerName = value;
        OnPropertyChanged();
    }
}

Entry.Text は、入力欄の文字です。
MVVMでは、この文字をViewModelのプロパティへBindingして扱います。


Bindingの追い方

.NET MAUIでBindingを追うときは、次の順で見ると迷いにくいです。

例として、このXAMLを見ます。

<Entry Text="{Binding CustomerName}" />

追い方はこうです。

順番 見る場所 内容
1 Entry 入力欄
2 Text 入力欄の文字プロパティ
3 {Binding CustomerName} Binding先の名前
4 BindingContext どのViewModelを見るか
5 ViewModel CustomerName プロパティを探す

流れで書くとこうです。

Entry.Text
  ↓ Binding
CustomerName
  ↓ BindingContext
CustomerViewModel.CustomerName

Bindingで迷ったら、XAMLだけを見続けない方がよいです。
BindingContext とViewModel側のプロパティ名までセットで追います。


Commandの追い方

次に、Commandを追います。

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

追い方はこうです。

順番 見る場所 内容
1 Button ボタン
2 Command ボタン操作の入口
3 {Binding SaveCommand} Binding先のCommand
4 ViewModel SaveCommand プロパティを探す
5 ViewModelのコンストラクタ SaveCommand = new RelayCommand(Save) を見る
6 Save() 実際に呼ばれる処理を見る

流れで書くとこうです。

Button.Command
  ↓ Binding
SaveCommand
  ↓ RelayCommand
Save()
  ↓
CustomerService.Save(CustomerName)
  ↓
Message更新
  ↓
Label.Textへ反映

Commandは、XAML側の操作とViewModel側の処理をつなぐ入口です。
Button.Command から SaveCommand、さらに Save() へ追うと流れが見えます。


DisplayAlertはどう扱うか

.NET MAUIでは、画面にメッセージを出したくなる場面があります。

たとえば、保存後に DisplayAlert を出したい場合です。

ただし、ViewModelから直接 DisplayAlert を呼び始めると、ViewModelが画面に依存しやすくなります。

// ViewModelに直接書きすぎると、画面依存が強くなりやすい
await Application.Current.MainPage.DisplayAlert("保存", "保存しました。", "OK");

今回のサンプルでは、ViewModelは Message を更新するだけにしています。

Message = "保存しました。";

画面側では、その MessageLabel.Text にBindingします。

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

入門段階では、まず Message を画面にBindingする形で十分です。
ダイアログ表示、画面遷移、通知サービスなどは、次の段階で分けて考えると整理しやすくなります。


コードビハインドに書くもの

MVVMだからといって、コードビハインドを完全にゼロにする必要はありません。

ただし、何を書くかは分けた方がよいです。

コードビハインドに書いてよいものは、主に画面そのものに関係する処理です。

書いてよいもの 理由
InitializeComponent() Viewの初期化だから
BindingContext の設定 ViewとViewModelをつなぐ入口だから
画面固有のUI制御 Viewに閉じた処理だから

今回のコードビハインドはこれだけです。

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

        BindingContext = new CustomerViewModel();
    }
}

一方で、コードビハインドに集めない方がよいものもあります。

避けたいもの 理由
入力チェック ViewModelやServiceへ寄せたい
保存処理 Serviceへ寄せたい
API通信 Serviceへ寄せたい
複雑な業務判断 ModelやServiceへ寄せたい
状態管理 ViewModelへ寄せたい

MVVMで避けたいのは、コードビハインドに業務処理や状態管理が集まることです。
コードビハインドをゼロにすること自体が目的ではありません。


よくある詰まりどころ

ここでは、.NET MAUIのMVVMでよく詰まるところを整理します。

Bindingしているのに値が出ない

まず見るところは BindingContext です。

BindingContext = new CustomerViewModel();

次に、Binding名とViewModelのプロパティ名が一致しているか見ます。

<Label Text="{Binding Message}" />
public string Message { get; set; } = "";

MassageMessage のようなスペル違いでもBindingは期待通りに動きません。
まずはBinding名とプロパティ名を確認します。

値を変えても画面が更新されない

ViewModelが INotifyPropertyChanged を実装しているか確認します。

public class CustomerViewModel : INotifyPropertyChanged

さらに、setterで OnPropertyChanged() を呼んでいるか見ます。

_message = value;
OnPropertyChanged();

Buttonをタップしても処理が呼ばれない

Command のBinding先を確認します。

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

ViewModel側に SaveCommand があるか確認します。

public ICommand SaveCommand { get; }

さらに、コンストラクタで処理を登録しているか確認します。

SaveCommand = new RelayCommand(Save);

ViewModelに画面処理が増えてきた

ViewModelに DisplayAlert、画面遷移、API通信などが増えてきたら、責務が混ざっていないか確認します。

増えてきたもの 逃がし先の候補
API通信 Service
DBやファイル保存 Repository / Service
画面遷移 NavigationServiceなど
ダイアログ表示 DialogServiceなど
複雑な業務判断 Model / Service

最初から全部をサービス化する必要はありません。
ただ、ViewModelが読みにくくなってきたら、画面状態と業務処理が混ざっていないかを見ると整理しやすくなります。


WPFを読んだ人向けの差分

WPFのMVVMを先に見ている場合、.NET MAUIでは名前の違いを押さえると読み替えやすくなります。

目的 WPF .NET MAUI
ViewModelをViewへ渡す DataContext BindingContext
文字入力 TextBox Entry
文字表示 TextBlock Label
ボタンの文字 Content Text
ボタン操作 Button.Command Button.Command
縦並び StackPanel VerticalStackLayout

考え方は近いです。

WPF:
TextBox.Text
  ↓ Binding
DataContext.CustomerName

MAUI:
Entry.Text
  ↓ Binding
BindingContext.CustomerName

WPFは DataContext、.NET MAUIは BindingContext
名前は違いますが、どちらも「Bindingが見るViewModelを渡す」役割です。


今回の全体の流れ

今回の画面全体の流れをもう一度並べます。

MainPage
  ↓ BindingContext
CustomerViewModel
  ↓
CustomerName / Message / SaveCommand

入力時の流れです。

Entry.Text
  ↓ Binding
CustomerName
  ↓
CustomerViewModel.CustomerName

保存ボタンを押したときの流れです。

Button.Command
  ↓ Binding
SaveCommand
  ↓
Save()
  ↓
CustomerService.Save(CustomerName)
  ↓
Message更新
  ↓
OnPropertyChanged()
  ↓
Label.Text更新

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


まとめ

.NET MAUIのMVVMでは、まず BindingContextBinding を押さえることが大事です。

BindingContext は、Bindingが見るViewModelを決めます。

BindingContext = new CustomerViewModel();

XAML側では、ViewModelのプロパティへBindingします。

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

対応関係はこうです。

XAML ViewModel
Entry.Text CustomerName
Label.Text Message
Button.Command SaveCommand
BindingContext CustomerViewModel

値を画面へ反映するには、INotifyPropertyChanged で変更通知を出します。

OnPropertyChanged();

ボタン操作は、Command を使ってViewModelへ渡します。

Button.Command
  ↓
SaveCommand
  ↓
Save()

MVVMは、コードを難しくするためのものではありません。

画面、画面状態、業務処理を混ぜないための整理方法です。

.NET MAUIでは、次の3つを最初に押さえると進めやすくなります。

  • BindingContext でViewModelを渡す
  • Binding でViewModelの値を画面につなぐ
  • Command で画面操作をViewModelへ渡す

.NET MAUIのMVVMで迷ったら、まず次を追います。
BindingContext は何か。
Binding はどのプロパティを見ているか。
Command はどの処理につながっているか。
この3点を見ると、原因を絞りやすくなります。


関連記事


次に読む

次は、MVVMの定型コードを減らす方向へ進めます。

  • G31: CommunityToolkit.Mvvm入門|ObservableObjectとRelayCommandで定型コードを減らす

連載Index(読む順・公開済リンクが最新)
S00: 総合Index

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?