この記事は .NET MAUIのMVVM実装編 です。
先に全体像を押さえるなら G27: MVVM共通編、
XAMLの読み方を確認するなら G28: XAML読み方辞典、
WPF側の実装を見るなら G29: WPF実装編 から読むとつながりやすいです。
.NET MAUIでMVVMを書くときは、BindingContext と Binding の関係を先に押さえておくと、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でよく使う次の要素を整理します。
BindingContextBindingButton.CommandINotifyPropertyChangedRelayCommand
この記事で扱うこと
この記事では、.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では、この RelayCommand に Save メソッドを渡しています。
public CustomerViewModel()
{
SaveCommand = new RelayCommand(Save);
}
XAML側では、ボタンの Command にBindingします。
<Button
Text="保存"
Command="{Binding SaveCommand}" />
流れはこうです。
Button.Command
↓ Binding
SaveCommand
↓ Execute
Save()
この RelayCommand は、Commandの流れを説明するための最小版です。
ボタンの有効/無効を動的に切り替える場合は、CanExecute と CanExecuteChanged の扱いも追加で考えます。
保存処理は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}" />
これは、Entry の Text と、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 = "保存しました。";
画面側では、その Message を Label.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; } = "";
Massage と Message のようなスペル違いでも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では、まず BindingContext と Binding を押さえることが大事です。
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