依存プロパティ(依存関係プロパティ)でバインディング

d:id:cu39:20090719:updatecontrols を書いた時点では、MVVM パターンもしくは MVP パターン(らしき)構成のとき INotifyPropertyChanged を利用したバインディングと比べて Update Controls .NET の手軽さを感じたんですが、依存プロパティもしくは依存関係プロパティ(MSDN の訳語は後者)を使ったバインディングのほうはよくわかってなかったので実装してみました。

パフォーマンスの最適化 : データ バインディング」(MSDN)を読むと、依存プロパティを利用したデータバインディングはリフレクションを利用する必要がなく、INotifyPropertyChanged を利用するよりも低負荷だと推奨されてます。実装の手間がそれほどでなければこれで済ませたいんですが、そのあたりを検討したいなと。

挙動は前回と同じものを作ります。まず「未確認飛行 C」の例を参考に基本的なクラスから作っていきます。

Name.cs

using System.Windows; // DependencyObjectを継承するのに必要

namespace DepPropBinding
{
    class Name : DependencyObject
    {
        #region 依存関係プロパティ dependency properties

        public static readonly DependencyProperty FirstProperty =
            DependencyProperty.Register("First", typeof(string), typeof(Name));
        public static readonly DependencyProperty LastProperty =
            DependencyProperty.Register("Last", typeof(string), typeof(Name));
        public static readonly DependencyProperty FirstLastProperty =
            DependencyProperty.Register("FirstLast", typeof(string), typeof(Name));
        public static readonly DependencyProperty LastFirstProperty =
            DependencyProperty.Register("LastFirst", typeof(string), typeof(Name));

        #endregion // 依存関係プロパティ dependency properties

        #region CLRプロパティ (CLR property wrappers)

        public string First
        {
            get { return (string)GetValue(FirstProperty); }
            set { SetValue(FirstProperty, value); }
        }
        public string Last
        {
            get { return (string)GetValue(LastProperty); }
            set { SetValue(LastProperty, value); }
        }
        public string FirstLast
        {
            get { return (string)GetValue(FirstLastProperty); }
            set { SetValue(FirstLastProperty, value); }
        }
        public string LastFirst
        {
            get { return (string)GetValue(LastFirstProperty); }
            set { SetValue(LastFirstProperty, value); }
        }

        #endregion // CLRプロパティ (CLR property wrappers)

        #region コンストラクタ

        public Name()
        {
            SetValue(FirstProperty, "Barack");
            SetValue(LastProperty, "Obama");
        }

        #endregion // コンストラクタ

    }
}

これだけでも基本的なバインディングは動作します。例えばスライダーと数字の入ったテキストボックスのような2つのコントロールを1つのプロパティにバインドするだけであれば、これで充分(コンバータを書く必要はあるかもしれないけど)。でもそれなら2つのコントロールにコンバータを挟んで、直接(中継クラスを入れず)バインドするほうが手間も処理も効率的です。

ここでは単に2つのプロパティを結合するだけでなく、 First プロパティや Last プロパティが変更されたとき、FirstLast プロパティと LastFirst プロパティも連動して変わるよう処理を付加します。ここで INotifyPropertyChanged を使うとリフレクションの負荷が増えてしまうわけですが、依存プロパティには別の仕組みが用意されています。

DependencyProperty.Register メソッドには、第3引数として PropertyMetadata インスタンスを渡すことができます。このインスタンスで依存プロパティの挙動を細かく設定できると。

DependencyProperty.Register( String, Type, Type )
DependencyProperty.Register( String, Type, Type, PropertyMetadata )
DependencyProperty.Register( String, Type, Type, PropertyMetadata, ValidateValueCallback )

実際には PropertyMetadata そのものでなく、派生クラス FrameworkPropertyMetadataインスタンスを渡します。このコンストラクタに、プロパティが変更された場合のコールバックメソッド(PropertyChangedCallback)を渡すことができます。

...
FrameworkPropertyMetadata( PropertyChangedCallback )
...

PropertyMetadata のコンストラクタは柔軟で、入力値を強制(coerce)するコールバックを渡したりもできるようです。DependencyProperty.Register で validate コールバックを渡すこともできるようだけど、今回はどちらも割愛。

以下のようにプロパティ変更時のコールバックだけを渡します。

Name.cs 改(部分)

        #region 依存関係プロパティ dependency properties

        public static readonly DependencyProperty FirstProperty =
            DependencyProperty.Register("First", typeof(string), typeof(Name), 
                // メタデータのインスタンスを作成
                new FrameworkPropertyMetadata(
                    // コールバックメソッドを指定
                    new PropertyChangedCallback(OnFirstPropertyChanged)
                )
            );
        public static readonly DependencyProperty LastProperty =
            DependencyProperty.Register("Last", typeof(string), typeof(Name),
                new FrameworkPropertyMetadata(
                    new PropertyChangedCallback(OnLastPropertyChanged)
                )
            );

        #region property-changedコールバック

        private static void OnFirstPropertyChanged(
            DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            d.SetValue(FirstLastProperty, 
                (string)e.NewValue + " " + (string)d.GetValue(LastProperty));
            d.SetValue(LastFirstProperty,
                (string)d.GetValue(LastProperty) + " " + (string)e.NewValue);
        }
        private static void OnLastPropertyChanged(
            DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            d.SetValue(FirstLastProperty, 
                (string)d.GetValue(FirstProperty) + " " + (string)e.NewValue);
            d.SetValue(LastFirstProperty,
                (string)e.NewValue + " " + (string)d.GetValue(FirstProperty));
        }

        #endregion // property-changedコールバック

        public static readonly DependencyProperty FirstLastProperty =
            DependencyProperty.Register("FirstLast", typeof(string), typeof(Name));
        public static readonly DependencyProperty LastFirstProperty =
            DependencyProperty.Register("LastFirst", typeof(string), typeof(Name));

        #endregion // 依存関係プロパティ dependency properties

これで基礎クラスは完成。これを XAML にバインドします。

まず前回と同じくコードでバインディングする方法から。

Window1.xaml

<Window x:Class="DepPropBinding.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="200" Width="300">
    <!-- コード側でデータコンテキストを指定するときはName属性が必要 -->
    <StackPanel Margin="10" Name="panel">
        <TextBlock Text="First"/>
        <!-- 同じパスにバインドしたものはどちらを変更しても同期する -->
        <TextBox><Binding Path="First"/></TextBox>
        <TextBox><Binding Path="First"/></TextBox>
        <TextBlock Text="Last"/>
        <TextBox><Binding Path="Last"/></TextBox>
        <TextBlock Text="First-Last"/>
        <TextBox><Binding Path="FirstLast"/></TextBox>
        <TextBlock Text="Last-First"/>
        <TextBox><Binding Path="LastFirst"/></TextBox>
    </StackPanel>
</Window>

Window1.xaml.cs

using System.Windows;

namespace DepPropBinding
{
    /// <summary>
    /// Window1.xaml の相互作用ロジック
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            panel.DataContext = new Name();  // ここでバインドソースを指定してる
        }
    }
}

たとえば panel.DataContext へ入れる前に Name のプロパティを変更してやると、反映されます。

        public Window1()
        {
            InitializeComponent();
            var n = new Name();
            n.Last = "Bush";
            panel.DataContext = n;
        }

他方、XAML だけでバインディングを完結するなら以下。コード側からインスタンス(mydp)を触るのが面倒になりますが、XAML を読むだけでバインディングの全体像を把握できるメリットもある。

Window1.xaml

<Window x:Class="DepPropBinding.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:dp="clr-namespace:DepPropBinding"
    Title="Window1" Height="200" Width="300">
    <Window.Resources>
        <dp:Name x:Key="mydp"/>
    </Window.Resources>
    <!-- 静的リソースをデータコンテキストに指定、子要素に引き継がれる -->
    <StackPanel Margin="10" DataContext="{StaticResource mydp}">
        <TextBlock Text="First"/>
        <!-- 同じパスにバインドしたものはどちらを変更しても同期する -->
        <TextBox><Binding Path="First"/></TextBox>
        <TextBox><Binding Path="First"/></TextBox>
        <TextBlock Text="Last"/>
        <TextBox><Binding Path="Last"/></TextBox>
        <TextBlock Text="First-Last"/>
        <TextBox><Binding Path="FirstLast"/></TextBox>
        <TextBlock Text="Last-First"/>
        <TextBox><Binding Path="LastFirst"/></TextBox>
    </StackPanel>
</Window>

Window1.xaml.cs

自動生成そのままでOK。

using System.Windows;

namespace DepPropBinding
{
    /// <summary>
    /// Window1.xaml の相互作用ロジック
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
        }
    }
}

感想

とりあえず動くサンプルが作れたけど、今回の Name クラスは View べったりというかんじで、MVVM 的に言えば Model と ViewModel を兼ねています。フルスクラッチするならいいかもしれませんが、分離のメリットは消えてしまいます。いわゆる「レガシーな CLR クラス」を依存オブジェクトとしてラッピングして再利用できないかと考えても、結局 INotifyPropertyChanged が欲しいケースが増えてくる気がします(掘り下げきれてないけど)。値の検証と修正、変更の通知もいちいちコールバックを書くより setter に書けるほうがわかりやすいし。

依存オブジェクトは基本的に、既存のコントロールを拡張するケースでの利用を想定してるんでしょうね。

そんなところで、自分としては Update Controls .NET を使いたい場面は多そうだという結論に。ただオーバーヘッドは気になるところですが、それはまた別の機会に。