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とかいうそれっぽい名前の関数にいきます。
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を変更したタイミングで値が書き換わらないことへの対処です。