WPF の DataTrigger を強制 update する
◆ リフレクションで 隠された BindingExpression を取得して更新する
久々に WPF を使いました
新しい環境で使うと WPF ってヘルパークラスがないとつらい ととても感じます
小さい画面 1 つでコントロールもちょっとだけというものを作るだけだったので 普段使いのライブラリはなしで作っていたのですが コマンドや INotifyPropertyChanged 実装済みのベースクラスは必須だと思いました
でも
ここまで来たらライブラリのプロジェクト追加とか面倒だし ViewModel 的なものは完全にデータ置き場のクラスにして いくつかの Binding に対して自力で 「ソース→ターゲット」のアップデートするだけで解決したい
ということでこんな感じにしてました
ですが DataTrigger の場合は trigger の Binding プロパティはちょっと特殊で プロパティに Binding を設定してるというより Binding 型のプロパティに Binding 型のインスタンスを代入しているようなものなので 同じように BindingExpression が取得できません
BindingExpression から Binding は簡単に取得できますが 逆方向には参照を持っていないようで難しいです
Window に関連する全 BindingExpression の取得ができれば 目的の Bindng に関連付いてるのを探せますが BindingExpression を全取得する機能もなさそうです
DependencyProperty で出来るなら DataTrigger でも出来ていいように思うのですが ググってもそれらしいのは全然見当たりませんでした
完全に DataTrigger の手動更新はサポートしてないようです
ということで Reference Source
Framework のソースを見て リフレクションで無理矢理やります
ソース読むこと数時間……どうにか BindingExpression を取得して Update できました
Button_Click1 のクリックで DataContext の値 (Source) を書き換えて
Button_Click2 のクリックで DataTrigger を更新します
.NET Framework のバージョンを変えなければ動かなくなることはないはず……なのですけど なんか不安のあるコードなんですよね
どうしても DataTrigger を手動更新したい場合はこのリフレクションで頑張る処理
嫌なら素直に INotifyPropertyChanged を実装して通知しましょう
おまけで Gist に別バージョンおいてます
新しい環境で使うと 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);
}
{
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>
<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;
}
{
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 系のコア部分のソースは変更されなそうですし アップデートあっても基本は大丈夫なのかな
いまさらこんな Style 系のコア部分のソースは変更されなそうですし アップデートあっても基本は大丈夫なのかな
どうしても DataTrigger を手動更新したい場合はこのリフレクションで頑張る処理
嫌なら素直に INotifyPropertyChanged を実装して通知しましょう
おまけで Gist に別バージョンおいてます