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?

WPFのMVVM入門|DataContextとBindingでViewModelを画面につなぐ【外伝G29】

0
Posted at

連載Index(読む順・公開済リンクが最新)
S00_門前の誓い_総合Index

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

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

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

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

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

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

では、XAMLはどうやってViewModelを見に行くのか。

そこで使うのが DataContext です。

DataContext = new CustomerViewModel();

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

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

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

  • DataContext
  • Binding
  • Button.Command
  • INotifyPropertyChanged
  • RelayCommand

この記事は WPFのMVVM実装編 です。

先に全体像を押さえるなら、
C#のMVVM入門|WPF/MAUIに共通するViewModel・Binding・Commandの考え方【外伝G27】

XAMLの要素名・属性・Bindingの読み方を確認するなら、
XAML読み方辞典|要素・属性・Binding・CommandとC#側へのつながりを整理する【外伝G28】


この記事で扱うこと

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

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

DI、画面遷移、Prism、ReactiveProperty、CommunityToolkit.Mvvmの属性構文までは深く扱いません。

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


今回作る画面

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

顧客名
[              ]
[ 保存 ]

保存しました。

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

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

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

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

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

その入口が DataContext です。

DataContext = new CustomerViewModel();

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


DataContextはBindingが見るViewModel

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

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

DataContext = new CustomerViewModel();

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

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

この場合、WPFは DataContext に設定された CustomerViewModel の中から CustomerName を探します。

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

TextBox.Text
  ↓
{Binding CustomerName}
  ↓
DataContext に設定された CustomerViewModel
  ↓
CustomerViewModel.CustomerName

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

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


ファイルの置き場所

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

SampleApp
├ Views
│  └ MainWindow.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 TextBox.Text 入力された顧客名
Message TextBlock.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側から次のように参照されます。

<TextBox Text="{Binding CustomerName}" />
<Button Command="{Binding SaveCommand}" />
<TextBlock 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();
    }
}

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

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

流れはこうです。

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

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


RelayCommandを書く

WPFの 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 Content="保存"
        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を書く

次に、WPFのXAMLを書きます。

MainWindow.xaml です。

<Window x:Class="SampleApp.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        Title="Customer Sample"
        Height="220"
        Width="360">
    <StackPanel Margin="20">
        <TextBlock Text="顧客名" />

        <TextBox
            Text="{Binding CustomerName, UpdateSourceTrigger=PropertyChanged}"
            Width="240"
            HorizontalAlignment="Left" />

        <Button
            Content="保存"
            Command="{Binding SaveCommand}"
            Width="120"
            Margin="0,12,0,0"
            HorizontalAlignment="Left" />

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

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

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

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


DataContextにViewModelを設定する

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

MainWindow.xaml.cs で、DataContext にViewModelを設定します。

using System.Windows;
using SampleApp.ViewModels;

namespace SampleApp.Views;

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

        DataContext = new CustomerViewModel();
    }
}

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

MainWindow
  ↓ DataContext
CustomerViewModel
  ↓ Binding
CustomerName / Message / SaveCommand

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


TextBox.TextのUpdateSourceTrigger

WPFの TextBox.Text では、入力した値がViewModelへ反映されるタイミングに注意が必要です。

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

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

UpdateSourceTrigger=PropertyChanged を付けると、入力するたびにViewModelの CustomerName へ反映されます。

指定しない場合、TextBox.Text はフォーカスが外れたタイミングで反映される動きになりやすいです。

書き方 ViewModelへ反映されるタイミング
Text="{Binding CustomerName}" 主にフォーカスが外れたとき
Text="{Binding CustomerName, UpdateSourceTrigger=PropertyChanged}" 入力するたび

入力中の値をすぐViewModelへ反映したい場合は、UpdateSourceTrigger=PropertyChanged を付けると分かりやすいです。


Bindingの追い方

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

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

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

追い方はこうです。

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

流れで書くとこうです。

TextBox.Text
  ↓ Binding
CustomerName
  ↓ DataContext
CustomerViewModel.CustomerName

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


Commandの追い方

次に、Commandを追います。

<Button Content="保存"
        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更新
  ↓
TextBlock.Textへ反映

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


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

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

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

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

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

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

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

        DataContext = new CustomerViewModel();
    }
}

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

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

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


よくある詰まりどころ

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

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

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

DataContext = new CustomerViewModel();

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

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

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

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

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

public class CustomerViewModel : INotifyPropertyChanged

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

_message = value;
OnPropertyChanged();

TextBoxの入力値がすぐViewModelに入らない

UpdateSourceTrigger=PropertyChanged が必要か確認します。

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

Buttonを押しても処理が呼ばれない

Command のBinding先を確認します。

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

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

public ICommand SaveCommand { get; }

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

SaveCommand = new RelayCommand(Save);

ViewModelに書くもの、書かないもの

WPFのMVVMでは、ViewModelに何を書くかが大事です。

ViewModelに書きやすいものは次です。

内容 理由
画面に表示する値 画面状態だから
入力中の値 画面状態だから
選択中の項目 画面状態だから
Command 画面操作の入口だから
画面表示用のメッセージ 画面状態だから

逆に、ViewModelに集めすぎない方がよいものは次です。

内容 理由
DB接続 外部アクセスだから
API通信 通信処理だから
ファイル保存 I/O処理だから
複雑な業務ルール ServiceやModelへ分けたいから
UI部品そのものの操作 Viewに依存するから

ViewModelは、画面状態と操作の入口を持つ場所です。
業務処理や外部アクセスを全部抱える場所ではありません。


今回の全体の流れ

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

MainWindow
  ↓ DataContext
CustomerViewModel
  ↓
CustomerName / Message / SaveCommand

入力時の流れです。

TextBox.Text
  ↓ Binding
CustomerName
  ↓
CustomerViewModel.CustomerName

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

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

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


まとめ

WPFのMVVMでは、まず DataContextBinding を押さえることが大事です。

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

DataContext = new CustomerViewModel();

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

<TextBox Text="{Binding CustomerName, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding Message}" />
<Button Command="{Binding SaveCommand}" />

対応関係はこうです。

XAML ViewModel
TextBox.Text CustomerName
TextBlock.Text Message
Button.Command SaveCommand
DataContext CustomerViewModel

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

OnPropertyChanged();

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

Button.Command
  ↓
SaveCommand
  ↓
Save()

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

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

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

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

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


関連記事

MVVMの共通概念はこちらです。

XAMLの読み方はこちらです。

MVVMの前提となる画面設計の整理はこちらです。

UIスレッドや非同期処理が絡む場合はこちらも関連します。


次に読む

この流れで読むと、WPFとMAUIの違いが整理しやすくなります。

  • G30: .NET MAUIのMVVM入門|BindingContextとCommandで画面処理を分ける

連載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?