読者です 読者をやめる 読者になる 読者になる

空談録

http://artfulplace.net/blogs/ からひっこしつつ

DependencyObjectに独自DataContextを追加してBindingしたいと思った

思った。思ったができるとは言っていない。

世の中の奇特なふぁぼツバメとかいうやつが言いました。
「Bindingするのにx:Staticでソース指定するのだるくね?」
ええそうですね。どう考えても対応できないことが分かりますがクレーマーを放置してもしょうがないので対処します。

実は知らなかった「なぜSourceに初期値を入れないとDataContextをSourceに使用するのか」という話も含めてやっていきましょう。

そもそも独自DataContextって有効なの?

DataContextっていうDependencyPropertyを追加したらBindingのデフォルトのソースになるのか?という点からやっていきます。
FrameworkElementのDataContextを参考にしてこんなものを追加します。

public abstract class DataContextBase : DependencyObject
{
    public object DataContext
    {
        get { return (object)GetValue(DataContextPropert
        set { SetValue(DataContextProperty, value); }
    }


    // Using a DependencyProperty as the backing store for DataContext.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty DataContextProperty =
        DependencyProperty.Register("DataContext", typeof(object), typeof(DataContextBase), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, (d, e) => ((DataContextBase)d).DataContext = e.NewValue));
}

で、これで hoge={Binding x}とか書いたら解決してくれる! なんていうことはありませんでした。
このオレオレDataContextちゃんを反映させるべく、BindingのデフォルトにDataContextを使ってくれるのはなぜ~っていうのを追っていきます

DataContextとBindingExpressionの関係

なぜも何もDataContextを使うようにしているからだろ!って言われそうですが、ではDataContextの利用はどうやっているのでしょうか。Reflection? って話については全く知りません。
BindingのデフォルトパラメータとかでDataContextを指定してるならそれを追従すれば独自DataContext生やせるよね!ということで追ってみましょう。

ざっくりいくと、実際はBindingExpressionでこのDataContextへの対応付けを行っています。
BindingでもFrameworkElementでもないです。ここがいまいちわかりづらい。

BindingExpressionにDataSourceというプロパティが存在します。これがSourceオブジェクトを返すプロパティです。
https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Data/BindingExpression.cs,166

ReferenceSourceが開けないとかのためにソースの一部をコピペするとこんな感じ。

/// <summary>The data source actually used by this BindingExpression</summary>
internal object DataSource
{
    get
    {
        // ~~~~~

        // if we're using DataContext, find the source for the DataContext
        if (_ctxElement != null)
            return GetDataSourceForDataContext(ContextElement);
 
        // otherwise use the explicit source
    }
}

ContextElementはFrameworkElementであることが保証されている?感じだと思います。詳しくは追えなかった…
ContextElementがnullでなければGetDataSourceForDataContextとかいうそれっぽい名前の関数にいきます。

https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Data/BindingExpression.cs,2694

object GetDataSourceForDataContext(DependencyObject d)
{
    // look for ancestor that contributed the inherited value
    DependencyObject ancestor;
    BindingExpression b = null;
 
    for (ancestor = d;
         ancestor != null;
         ancestor = FrameworkElement.GetFrameworkParent(ancestor))
    {
        if (HasLocalDataContext(ancestor))
        {
            b = BindingOperations.GetBindingExpression(ancestor, FrameworkElement.DataContextProperty) as BindingExpression;
            break;
        }
    }
 
    if (b != null)
        return b.DataSource;
 
    return null;
}

FrameworkElement.DataContextProperty

グエーーーッ

というわけでDataContextが使われる理由は、.NETのコード内でFrameworkElement.DataContextPropertyを基にBindingOperations.GetBindingExpressionsを利用したソース解決を行うコードがあるからのようです。

つまりどういうことだってばよ? というと、オレオレDataContextちゃんがFrameworkElement.DataContextでない限り、既定の処理では解決してくれないということです。

なんとしてでもDataContextを解決させる

ぶっちゃけデフォルトのBindingでは不可能です。
しかしどうしてもDataContextを自動でやってくれる夢が見たい!

ならばBindingそのものをいじればいいじゃない!ということでBindingを変えましょう。私のように完全にPresentationFrameworkなコントロール群と独立した環境であれば、既存のBindingを使う強いメリットも存在しません。
(デフォルトのxmlnsとpresentationが別になるので、普通のBindingを使おうとするとp:Bindingとかになる。)

なのでBindingを派生してBindingクラスを作ります。コードはこんな感じ。

public class Binding : System.Windows.Data.Binding
{
    public Binding()
        : base()
    {
        if (Source == null && RelativeSource == null && string.IsNullOrEmpty(ElementName))
        {
            this.RelativeSource = new System.Windows.Data.RelativeSource(System.Windows.Data.RelativeSourceMode.Self);
            if (this.Path == null)
            {
                this.Path = new System.Windows.PropertyPath("DataContext");
            }
            else
            {
                this.Path = new System.Windows.PropertyPath("DataContext." + this.Path.Path);
            }
        }
    }

    public Binding(string path)
        :base(path)
    {
        if (Source == null && RelativeSource == null && string.IsNullOrEmpty(ElementName))
        {
            this.RelativeSource = new System.Windows.Data.RelativeSource(System.Windows.Data.RelativeSourceMode.Self);
            if (this.Path == null)
            {
                this.Path = new System.Windows.PropertyPath("DataContext");
            }
            else
            {
                this.Path = new System.Windows.PropertyPath("DataContext." + this.Path.Path);
            }
        }
    }
}

Sourceに何も入っていない場合は、RelativeSourceで自分自身を参照して、DataContextを見に行くようにします。
Pathがすでにある場合はDataContext.Pathとなるように文字列結合で対応します。力技だ!

これをすることで今まではこう書いていたXAML

<CustomUI x:Class="NereidTestAddin.RibbonData"
             xmlns="clr-namespace:artfulplace.Nereid;assembly=artfulplace.Nereid"
             xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:NereidTestAddin">
    <Ribbon>
        <Tabs>
            <Tab IdMso="TabAddIns">
                <Group Id="MyGroup" Label="My Group">
                    <Button Id="aaaa" Label="{p:Binding TestButton1Label, Source={x:Static local:MainViewModel.Instance}}" />
                </Group>
            </Tab>
        </Tabs>
    </Ribbon>
</CustomUI>

こうできます!(DataContextはコードビハインドで設定)

<CustomUI x:Class="NereidTestAddin.RibbonData"
             xmlns="clr-namespace:artfulplace.Nereid;assembly=artfulplace.Nereid"
             xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:NereidTestAddin">
    <Ribbon>
        <Tabs>
            <Tab IdMso="TabAddIns">
                <Group Id="MyGroup" Label="My Group">
                    <Button Id="aaaa" Label="{Binding TestButton1Label}" />
                </Group>
            </Tab>
        </Tabs>
    </Ribbon>
</CustomUI>

やべえ!画期的だ!という誰も得しない解決方法でなんとか対応します。

ところで実は最初のDataContextだと動かなかったりします。XAMLでDataContextを設定していればいいのですが、設定していない場合は動きません。
というわけでこんなあれそれをDataContextの値が変更されたら呼び出します。

public static void UpdateBindingAll(this DependencyObject target)
{
    LocalValueEnumerator localValueEnumerator = target.GetLocalValueEnumerator();
    while (localValueEnumerator.MoveNext())
    {
        LocalValueEntry current = localValueEnumerator.Current;
        if (System.Windows.Data.BindingOperations.IsDataBound(target, current.Property))
        {
            var bind = System.Windows.Data.BindingOperations.GetBinding(target, current.Property);
            if (bind.Source == null)
            {
                System.Windows.Data.BindingOperations.GetBindingExpression(target, current.Property).UpdateTarget();
            }
        }
    }

    // if target has children, TODO: update children's Bindings.
}

UpdateTargetをしないとDataContextを変更したタイミングで値が書き換わらないことへの対処です。

まとめ

FrameworkElementを継承した方がいい


独自DataContextについて誰かやってないのかなぁと思って調べていたのですが、誰もやってなかったのはなんでだろうなーと思ったらこれはできないわ…という感じでした。
結局BindingそのものはできますがオレオレBindingだしなぁ…

もう少しRibbon XAMLネタは続きそうです。ひさびさにブログが書きまくれている気がする

この辺で