WPFのINotifyPropertyChangedとICommandを使ったバインディングの概要

コンピュータ

データをバインディングをする場合バインディングソースとなるオブジェクトはINotifyPropertyChangedの実装である必要があります。
また、コマンドをバインディングする場合、ICommandを実装する必要となります。

この記事ではINotifyPropertyChangedICommandを実装したサンプルコードを解説することで、バインディングの概要を確認します。

サンプルコード

ファイル名:binding1.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

ファイル名:ActionCommand.cs

using System;
using System.Windows.Input;

public class ActionCommand : ICommand
{
    private readonly Action<object?> _execute;
    private readonly Predicate<object?> _canExecute;

    public ActionCommand(Action<object?> execute, Predicate<object?> canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public bool CanExecute(object? parameter)
        => _canExecute(parameter);

    public void Execute(object? parameter)
        => _execute(parameter);

    public event EventHandler? CanExecuteChanged;

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

ファイル名:MainViewModel.cs

using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace binding1;
public class MainViewModel : INotifyPropertyChanged
{
    private string? _selectedValue;
    public string? SelectedValue
    {
        get => _selectedValue;
        set
        {
            if (_selectedValue == value) return;
            _selectedValue = value;
            OnPropertyChanged();

            // ★ parameter の元が変わったので通知
            ActionCommand.RaiseCanExecuteChanged();
        }
    }

    public ActionCommand ActionCommand { get; }

    public MainViewModel()
    {
        ActionCommand = new ActionCommand(
            execute: p =>
            {
                var value = (string)p!;
                // 実行処理
                if (value is not null)
                {
                    Debug.Print($"{value}");
                }
            },
            canExecute: p =>
            {
                return !string.IsNullOrEmpty(p as string);
            }
        );
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

ファイル名:MainWindow.xaml.cs

using System.Windows;

namespace binding1;

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

ファイル名:MainWindow.xaml

<Window x:Class="binding1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:binding1"
        mc:Ignorable="d"
        Title="binding1" Height="150" Width="300">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>

    <StackPanel Margin="20">
        <!-- parameter の元 -->
        <TextBox Text="{Binding SelectedValue, UpdateSourceTrigger=PropertyChanged}" />

        <!-- parameter が Command に流れる -->
        <Button Content="Execute"
                Margin="0,10,0,0"
                Command="{Binding ActionCommand}"
                CommandParameter="{Binding SelectedValue}" />
    </StackPanel>
</Window>

サンプルコードの仕様

起動時、テキストボックスは空の状態で、ボタンは非活性(押せない)状態です。

テキストボックスに文字を入力すると、ボタンが活性化され押せる状態になります。

ボタンを押すとテキストボックスの内容が出力されます。

解説

MainWindow.xaml

Viewに相当し、UIのデザインを静的に宣言しています。

・MainWindow.xamlのデータソースをMainViewModelに設定する。

<Window.DataContext>
    <local:MainViewModel />
</Window.DataContext>

・テキストボックスの定義

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

MainViewModelクラスのSelectedValueプロパティとバインディングすることを宣言しています。
UpdateSourceTrigger=PropertyChangedを指定することで、1文字入力することに変更イベントもとに、内容の変更バインディング(同期処理)が実行されます。
指定しない場合、TextBoxからフォーカスが失われた際、バインディングされます。

・ボタンの定義

<Button Content="Execute"
           Margin="0,10,0,0"
           Command="{Binding ActionCommand}"
           CommandParameter="{Binding SelectedValue}" />

CommandプロパティがActionCommandプロパティとバインディングすることを宣言しています。
また、CommandParameterはSelectedValueとバインディングすることを宣言しています。
SelectedValueは、TextBoxのText(入力値)ともバインドしていますので、TextBoxの入力した内容が、CommandParameterに影響を与えます。

ActionCommand.cs

ICommandの実装です。

コンストラクタで、実行するコードを

Action<object?> execute

で、コマンドが有効・無効のフラグを

Predicate<object?> canExecute

で、引数として渡されて、メンバーとしてセットされます。

 

そして、

public void Execute(object? parameter)

public bool CanExecute(object? parameter)

で、実行されます。

 

public void RaiseCanExecuteChanged()

では、

CanExecuteChanged?.Invoke(this, EventArgs.Empty);

で、イベントを発火しています。

こちらは、parameter(CommandParameter)であるSelectedValueが変更された際、呼び出されるようにすることで、

CanExecuteが変化した可能性があることを通知しています。

 

MainViewModel.cs

まず、

public ActionCommand ActionCommand { get; }

は、前項のActionCommandをプロパティとして公開しています。

またコンストラクタ内で、

ActionCommand = new ActionCommand(
    execute: p =>
    {
        var value = (string)p!;
        // 実行処理
        if (value is not null)
        {
             Debug.Print($"{value}");
        }
    },
    canExecute: p =>
    {
         return !string.IsNullOrEmpty(p as string);
    }
);

ActionCommandを生成し、

executeでコマンドで実行したい内容(入力文字のデバック出力)と、
canExecuteで実行の許可フラグ(入力文字がnull又は空ではないこと)

を定義しています。

 

private string? _selectedValue;
public string? SelectedValue
{
    get => _selectedValue;
    set
    {
        if (_selectedValue == value) return;
        _selectedValue = value;
        OnPropertyChanged();

        // ★ parameter の元が変わったので通知
        ActionCommand.RaiseCanExecuteChanged();
    }
}

SelectedValueプロパティをプロパティと公開し、バインドソースとして機能します。

getはメンバー変数_selectedValueをそのまま返します。

setは、_selectedValueにvalueをセットするわけですが、

セットされるということは値が変更されることに成りますので、

OnPropertyChanged();

で、変更を通知しています。

OnPropertyChangedのCallerMemberNameは、nameがnullの場合(引数を省略)プロパティの名前(この場合”SelectedValue”)がセットされます。

 

また、

ActionCommand.RaiseCanExecuteChanged();

で、ActionCommandにも、SelectedValueが変化したことを通知し、

ボタンの実行可能・不可能フラグにに連動(バインディング)します。

 

 

流れ

各コードは最終的に

CanExecuteChanged

PropertyChanged

にたどり着きますが、

これ以降のコードは記述されていません。

 

それは、

バインディングシステム側で、

処理されているためと考えられます。

 

① データの流れ

TextBox.Text
(Binding)
SelectedValue

CommandParameter

CanExecute / Execute

② 通知の流れ

TextBox入力

SelectedValue setter
OnPropertyChanged

RaiseCanExecuteChanged

CanExecute 再評価

Button 有効/無効

汎用ベースクラス

INotifyPropertyChangedやICommandの実装は、
毎回同じようなコードになるので、
ベースクラスにまとめて継承する使い方が便利です。

ViewModelのベースクラス

ViewModelBase.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;

public abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    /// <summary>
    /// backing field を更新して PropertyChanged を発火する定番ヘルパー。
    /// </summary>
    protected bool SetProperty<T>(
        ref T field,
        T value,
        Action? onChanged,
        [CallerMemberName] string? propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value))
            return false;

        field = value;
        onChanged?.Invoke();
        OnPropertyChanged(propertyName);
        return true;
    }
}

/*
使い方1
public sealed class MainViewModel : ViewModelBase
{
    private string? _name;

    public string? Name
    {
        get => _name;
        set => SetProperty(ref _name, value);
    }
}

使い方2
private string? _firstName;
private string? _lastName;

public string? FirstName
{
    get => _firstName;
    set => SetProperty(ref _firstName, value, () => OnPropertyChanged(nameof(FullName)));
}

public string? LastName
{
    get => _lastName;
    set => SetProperty(ref _lastName, value, () => OnPropertyChanged(nameof(FullName)));
}

public string FullName => $"{FirstName} {LastName}".Trim();


*/

Commandのベースクラス

RelayCommand.cs

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

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

    public bool CanExecute(object? parameter)
        => _canExecute?.Invoke((T?)parameter) ?? true;

    public void Execute(object? parameter)
        => _execute((T?)parameter);

    public event EventHandler? CanExecuteChanged;

    public void RaiseCanExecuteChanged()
        => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
/*
使い方(ViewModelBaseと一緒に使う例)
public sealed class MainViewModel : ViewModelBase
{
    private string? _text;

    public string? Text
    {
        get => _text;
        set
        {
            if (SetProperty(ref _text, value))
            {
                SaveCommand.RaiseCanExecuteChanged();
            }
        }
    }

    public RelayCommand SaveCommand { get; }

    public MainViewModel()
    {
        SaveCommand = new RelayCommand(
            execute: Save,
            canExecute: () => !string.IsNullOrWhiteSpace(Text)
        );
    }

    private void Save()
    {
        // 保存処理
    }
}
*/

コメント