WPFの添付ビヘイビア


添付ビヘイビアとは

MVVMモデルを採用したWPFにおいて、Viewの状態変化に伴って実行される処理を定義したい場合に使用されるテクニックです。
MVVMの思想上View、つまりコードビハインドに処理を書くのはご法度ですが、
Viewで完結する処理をViewModelに書くのもあまりいいとは言えません。

そこで、WPFの添付プロパティを自作する処理にまとめてコードビハインドに書きたかった処理を記載することで
Viewのビヘイビア(= 振る舞い)を表現するテクニックになります。

イメージ

ざっくりとした添付ビヘイビアのイメージは以下の通りです。

サンプルコード

実際に作成してみたビヘイビアがこちら。

MessageDialogBehavior.cs
using System.Windows;
using System.Windows.Input;

namespace WPF.Views.Behaviors
{
    internal class MessageDialogBehavior
    {
        // 1. DependencyPropertyインスタンスを生成
        public static readonly DependencyProperty ShowMessageProperty = DependencyProperty.RegisterAttached(
            "ShowMessage",
            typeof(bool),
            typeof(MessageDialogBehavior),
            new PropertyMetadata(false, OnShowMessage)
        );

        // 2. Getterを作成
        public static bool GetShowMessage(DependencyObject target)
            => (bool)target.GetValue(ShowMessageProperty);

        // 3. Setterを作成
        public static void SetShowMessage(DependencyObject target, bool value)
            => target.SetValue(ShowMessageProperty, value);

        // 4. callbackメソッドを定義
        private static void OnShowMessage(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var element = sender as UIElement;
            if (element == null)
            {
                return;
            }

            var newValue = (bool)e.NewValue;
            if ((bool)e.NewValue)
            {
                element.MouseRightButtonDown += ShowMessage;
            } else
            {
                element.MouseRightButtonDown -= ShowMessage;
            }
        }

        private static void ShowMessage(object sender, MouseButtonEventArgs e)
        {
            MessageBox.Show("Behavior Sample.");
        }
    }
}

添付プロパティで True を指定したコントロールで右クリックするとポップアップが表示されるものになります。

1. DependencyPropertyインスタンスを生成

まずは DependencyProperty.RegisterAttached() を利用してDependencyPropertyインスタンスを 生成します。
DependencyProperty.RegisterAttached() が要求する4つの引数には次の内容を記載します。

引数
第1引数 プロパティ名を string 指定します。
この値が実際にXAMLで指定するプロパティ名になります。
第2引数 プロパティの型を Type で指定します。
第3引数 プロパティを所有する型を Type で指定します。
今回の場合は自分自身である MessageDialogBehavior になります。
第4引数 メタデータを指定します。
実際の値としては PropertyMetadata のインスタンスになります。

PropertyMetadataについて

DependencyProperty インスタンスの生成に必要な PropertyMetadata インスタンスですが、
デフォルトコンストラクタの他に4種類のオーバーロードで用意されていて、状況に応じて使い分けることになります。
各コンストラクタの引数は以下の通りです。

System.Windows.PropertyMetadata
// ※一部省略

namespace System.Windows
{
    public class PropertyMetadata
    {
        public PropertyMetadata();
        public PropertyMetadata(object defaultValue);
        public PropertyMetadata(PropertyChangedCallback propertyChangedCallback);
        public PropertyMetadata(object defaultValue, PropertyChangedCallback propertyChangedCallback);
        public PropertyMetadata(object defaultValue, PropertyChangedCallback propertyChangedCallback, CoerceValueCallback coerceValueCallback);
    }
}

今回使用したのはこのコンストラクタです。


public PropertyMetadata(object defaultValue, PropertyChangedCallback propertyChangedCallback);

添付プロパティのデフォルト値と、値が変更された際のcallbackを指定しています。
※callbackがどのように使用されるかは後述

2. Getterを作成

添付プロパティの値を取得するためのメソッドを定義します。
引数の DependencyObjectGetValue() メソッドの戻り値を
「1. DependencyPropertyインスタンスを生成」でRegisterAttached() の第2引数に指定した
型でキャストして返します。
※第2引数はプロパティの型でした

メソッド名について

Getterのメソッド名は必ず Get + プロパティ名 とする必要があります。
今回の場合だとプロパティ名を ShowMessage としているので、Geeterのメソッド名は GetShowMessage となります。

3. Setterを作成

添付プロパティの値を設定するためのメソッドを定義します。
引数の DependencyObjectSetValue() メソッドを使用して設定を行います。

メソッド名について

Getterと同じく、Setterのメソッド名も必ず Set + プロパティ名 とする必要があります。
そのため今回の場合だと、メソッド名は SetShowMessage となります。

4. callbackメソッドを定義

値が変更されたときに実行するcallbackメソッドを定義しています。
これは「1. DependencyPropertyインスタンスを生成」で登場した
PropertyMetadata コンストラクタの第2引数として使用されています。

この中で少し独特だなと感じたのはこの部分でした。

if ((bool)e.NewValue)
{
    element.MouseRightButtonDown += ShowMessage;
} else
{
    element.MouseRightButtonDown -= ShowMessage;
}

DependencyPropertyChangedEventArgse で変更前と変更後の値を取得できるのですが
今回は特に何かしらの処理で値を変更するといった処理は行っていません。
にもかかわらず、変更前・変更後どちらからも値が取得できるようになっていました。
デバッグして確認してみたところ、どうやら値を変更するような処理を行っていなくても
変更前の値である e.OldValue にはデフォルト値が設定されているようでした。
で、このデフォルト値がどこで指定されているかというと...


public static readonly DependencyProperty ShowMessageProperty = DependencyProperty.RegisterAttached(
    "ShowMessage",
    typeof(bool),
    typeof(MessageDialogBehavior),
    new PropertyMetadata(false, OnShowMessage)  // ここの第1引数
);

DependencyProperty インスタンス作成時に指定していました!
初期値がfalseなので実質添付プロパティでTrueを指定するとcallbackメソッドが実行されるとなっています。

View側の記述

このビヘイビアを実際に使用するためのXAMLと使用結果は以下の通りです。

BehaviorSample.xaml
<Window x:Class="WPFP.Views.BehaviorSample"
        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:WPFP.Views"
        xmlns:b="clr-namespace:WPFP.Views.Behaviors"
        mc:Ignorable="d"
        Title="BehaviorSample" Height="450" Width="800">
    <StackPanel>
        <TextBlock Text="Trueにするとメッセージボックスが表示される"
                   Margin="5"
                   FontSize="20"
                   b:MessageDialogBehavior.ShowMessage="True" />
        <TextBlock Text="falseだとメッセージボックスが表示されない"
                   Margin="5"
                   FontSize="20"
                   b:MessageDialogBehavior.ShowMessage="False" />
    </StackPanel>
</Window>

実行結果はこんな感じ。