XAMLを使わないWPF入門44「DependencyProperty – 依存関係プロパティ」

コンピュータ

個人的に、WPFで一番難しいのはカスタムコントロールだと思います。これが作れるようになると、UIを思い通りに設計・拡張でき、WPFで作れるアプリの自由度が一気に上がります。
そして、そのカスタムコントロールの“核”が依存関係プロパティ(DependencyProperty)です。発想としては、「コントロール側に、バインディングで値が出入りする“受け口”を用意する」イメージ。
つまり、バインディングさせたいプロパティをDPとして定義する——これがカスタムコントロール設計の第一歩です。

:本稿は「NoXAML」シリーズですが、ここで扱う DependencyProperty(DP)とデータバインディングの考え方は XAML でも共通です。XAML/コードの違いは“書き方”だけで、バインディングの成立条件(ターゲットはDPであること等)は同じです。

DependencyProperty(DP)は日本語で「依存関係プロパティ」とよばれ、色々な機能があります。
DPを使うことでコントロールのプロパティをバインディング可能にすることが出来ます。

そのサンプルコードで、カスタムコントロールでバインディングさせたいプロパティをDPとして定義しています。

ファイル名:SliderWithValueView.cs

public class SliderWithValueView : Control
{
    private readonly Slider _slider;
    private readonly TextBlock _text;
    private readonly UIElement _content;

    // Value(双方向バインディング既定、Min/Maxでクランプ)
    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register(
            nameof(Value),
            typeof(double),
            typeof(SliderWithValueView),
            new FrameworkPropertyMetadata(
                0.0,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                OnValueChanged,
                CoerceValueInRange));

    public double Value
    {
        get => (double)GetValue(ValueProperty);
        set => SetValue(ValueProperty, value);
    }

通常のプロパティとしてpublic double Valueが定義されており、そのValueプロパティに対する、依存関係プロパティ(以下DP)としてpublic static readonly DependencyProperty ValuePropertyValuePropertyという名称で定義されています。

直後のコードがDependencyProperty.Register()と記述されていることから、何らかのシステムに登録(Register)されていることが予想されます。詳細は確認していませんが、とりあえず使い方だけ覚える事とします。

  1. 第1引数は、nameof(Value)でXAMLで参照される識別子として使われます。
  2. 第2引数は、typeof(double)で通常のプロパティの型を渡しています。
  3. 第3引数は、typeof(SliderWithValueView)でプロパティが所属するクラスの型を渡しています。
  4. 第4引数は、FrameworkPropertyMetadataのインスタンスを引数として渡しています。

第4引数は謎ですが、それ以外はプロパティを定義するために必要な情報を渡していることが、確認できます。

次にFrameworkPropertyMetadataのインタンスの生成部分に注目したいと思います。
コンストラクタ引数の内容

  1. 第1引数は、0.0がセットされ、これはValueの既定値です。doubleなのできちんと小数点付になっています。
  2. 第2引数は、BindsTwoWayByDefaultがセットされTwoWayですので双方向にバインドを表しています。
  3. 第3引数は、OnValueChangedがセットされ値変更時のコールバックのように見えます。
  4. 第4引数は、CoerceValueInRangeValueの入力値を範囲内に強制するようです。
  5. 第3引数と第4引数はコールバックの様なので呼び出されるメソッドを見ていきます。

    ・OnValueChangedメソッド

    private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var c = (SliderWithValueView)d;
        if (c._slider == null) return;
        var v = (double)e.NewValue;
        // クランプ後の実値で表示更新
        v = (double)CoerceValueInRange(c, v);
        if (c._slider.Value != v) c._slider.Value = v;
        c._text.Text = v.ToString("F0");
    }

    コードの内容はプロパティを更新するコードそのものです。
    その中でCoerceValueInRange()でセットされた値が条件に合っている場合のみプロパティを変更されるようにしています。

    ・CoerceValueInRangeメソッド

    private static object CoerceValueInRange(DependencyObject d, object baseValue)
    {
       var c = (SliderWithValueView)d;
       var v = (double)baseValue;
       var min = c.Minimum;
       var max = c.Maximum;
       if (v < min) v = min;
       if (v > max) v = max;
       return v;
    }

    こちらのコードは、引数の値(v)が最小値(min)と最大値(max)の範囲内にあるかチェックし、下回った場合は最小値、上回った場合最大値、そうで無い場合そのまま値を返すコードになっています。

    まとめ

    SliderWithValueView クラスでは、Value というプロパティを定義しており、このプロパティは 依存関係プロパティ(DP)として機能するため XAML でデータバインディングが可能です。
    また、DPの識別子として ValueProperty(DependencyProperty 型の静的フィールド)がペアで定義されています。

    プロパティのペアなんで冗長な感じもしますが、C#のクラスとは別にDPとして同じものをプロパティとして定義しているので、似たようなコードになる感じだと思います。深く考えずに、そういう物として使うのが良さそうです。使っているうちに理解できることもあるでしょう。

コメント