◆ リフレクションで 隠された BindingExpression を取得して更新する

久々に WPF を使いました
新しい環境で使うと WPF ってヘルパークラスがないとつらい ととても感じます

小さい画面 1 つでコントロールもちょっとだけというものを作るだけだったので 普段使いのライブラリはなしで作っていたのですが コマンドや INotifyPropertyChanged 実装済みのベースクラスは必須だと思いました


でも
ここまで来たらライブラリのプロジェクト追加とか面倒だし ViewModel 的なものは完全にデータ置き場のクラスにして いくつかの Binding に対して自力で 「ソース→ターゲット」のアップデートするだけで解決したい
ということでこんな感じにしてました

private void updateBindng(FrameworkElement fe, DependencyProperty dp)
{
fe.GetBindingExpression(dp)?.UpdateTarget();
}

private void update()
{
this.updateBinding(this.textbox, TextBox.TextProperty);
this.updateBinding(this.label, Label.ContentProperty);
}

DataTrigger の手動 Update 手段が用意されてない

TextBox の Text プロパティなど FrameworkElement の DependencyProperty に Binding してる場合は BindingExpression を取得して手動で Update 可能です

ですが DataTrigger の場合は trigger の Binding プロパティはちょっと特殊で プロパティに Binding を設定してるというより Binding 型のプロパティに Binding 型のインスタンスを代入しているようなものなので 同じように BindingExpression が取得できません

BindingExpression から Binding は簡単に取得できますが 逆方向には参照を持っていないようで難しいです
Window に関連する全 BindingExpression の取得ができれば 目的の Bindng に関連付いてるのを探せますが BindingExpression を全取得する機能もなさそうです

DependencyProperty で出来るなら DataTrigger でも出来ていいように思うのですが ググってもそれらしいのは全然見当たりませんでした
完全に DataTrigger の手動更新はサポートしてないようです

無理矢理やる

だけどここまで来たらどうにかやりたいんです!

ということで Reference Source
Framework のソースを見て リフレクションで無理矢理やります


ソース読むこと数時間……どうにか BindingExpression を取得して Update できました

<StackPanel>
<Label x:Name="label">
<Label.Style>
<Style TargetType="Label">
<Style.Triggers>
<DataTrigger Binding="{Binding flag}" Value="True">
<Setter Property="Content" Value="flag is true"/>
</DataTrigger>
<DataTrigger Binding="{Binding flag}" Value="False">
<Setter Property="Content" Value="flag is false"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
<Button Click="Button_Click1">TOGGLE</Button>
<Button Click="Button_Click2">UPDATE</Button>
</StackPanel>

public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
this.DataContext = new Windows1Data();
}

private void Button_Click1(object sender, RoutedEventArgs e)
{
var data = (this.DataContext as Windows1Data);
data.flag = !data.flag;
}

private void Button_Click2(object sender, RoutedEventArgs e)
{
var sh = Type.GetType("System.Windows.StyleHelper, PresentationFramework, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = 31bf3856ad364e35");
var sdf = sh.GetField("StyleDataField", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
var ensureInstanceData = sh.GetMethods(BindingFlags.Static | BindingFlags.NonPublic).First(m =>
{
return m.Name == "EnsureInstanceData" && m.GetParameters().Length == 3;
});
var label_values = ensureInstanceData.Invoke(null, new object[] { sdf, this.label, 0 }) as IDictionary;

foreach (var trigger in this.label.Style.Triggers)
{
if(trigger is DataTrigger data_trigger)
{
var binding_expression = label_values[data_trigger.Binding] as BindingExpressionBase;
binding_expression.UpdateTarget();
}
}
}
}

public class Windows1Data
{
public bool flag { get; set; } = false;
}

Button_Click1 のクリックで DataContext の値 (Source) を書き換えて
Button_Click2 のクリックで DataTrigger を更新します

.NET Framework のバージョンを変えなければ動かなくなることはないはず……なのですけど なんか不安のあるコードなんですよね 

一応 4.5 と 4.7 では問題なく動くことは確認しました
参考にしたソースバージョンは 4.7.1 です
いまさらこんな Style 系のコア部分のソースは変更されなそうですし アップデートあっても基本は大丈夫なのかな


どうしても DataTrigger を手動更新したい場合はこのリフレクションで頑張る処理
嫌なら素直に INotifyPropertyChanged を実装して通知しましょう


おまけで Gist に別バージョンおいてます