WPF 主動觸發依賴屬性的 PropertyChanged
一、需求背景
需要顯示 ViewModel 中的 Message/DpMessage,顯示內容根據其某些屬性來確定。程式碼結構抽象如下:
// Model public class Message : INotifyPropertyChanged { public string MSG; public string Stack; } // ViewModel public class MessageViewModel : INotifyPropertyChanged { public Message { get; set; } public static readonly DpMessageProperty = DependencyProperty.Register(...) }
<TextBox Text="{Binding Message, Converter={x:static ShowDetailMessageConverter}}"/>
<TextBox Text="{Binding DpMessage, Converter={x:static ShowDetailMessageConverter}}"/>
以上程式碼,注意,兩個 Text 繫結的目標都是 MessageViewModel,繫結的 Path 分別是 Message、DpMessage。
當 Message 或者 DpMessage 變化(注意,變化指的是重新賦值,即,引用了新的 Message
問題來了:當 Message 或者 DpMessage 的屬性變化了呢,Message 類是實現了 INotifyPropertyChanged 的,屬性變化能觸發自身的變化(MessageViewModel.INotifyPropertyChanged)通知嗎?答案是,不能。即,Message .MSG 者 Message .Stack 變化了,View 不能得到更新通知!這不符合需求。
- 當然,這個問題可以通過更改繫結物件避過,即,將繫結物件直接設定為 Message 或者 DpMessage,然後使用 MultiBinding,將需要的屬性都繫結過去,也可以同樣實現需求,但是,這種方式的缺點多:繁瑣
二、Message 屬性(實現 INotifyPropertyChanged)的解決方法
在 Message .PropertyChanged 中監測屬性變化,變化時主動呼叫 MessageViewModel.OnPropertyChanged。這是很簡單的。
三、DpMessage 依賴屬性的解決方法
不同於 INotifyPropertyChanged,依賴屬性無法通過 OnPropertyChanged 函式觸發屬性變更通知,這個函式僅作為回撥函式使用。因此,檢視 原始碼,看看.Net如何去觸發的通知,找到函式如下:
/// <summary>
/// This is to enable some performance-motivated shortcuts in property
/// invalidation. When this is called, it means the caller knows the
/// value of the property is pointing to the same object instance as
/// before, but the meaning has changed because something within that
/// object has changed.
/// </summary>
/// <remarks>
/// Clients who are unaware of this will still behave correctly, if not
/// particularly performant, by assuming that we have a new instance.
/// Since invalidation operations are synchronous, we can set a bit
/// to maintain this knowledge through the invalidation operation.
/// This would be problematic in cross-thread operations, but the only
/// time DependencyObject can be used across thread in today's design
/// is when it is a Freezable object that has been Frozen. Frozen
/// means no more changes, which means no more invalidations.
///
/// This is being done as an internal method to enable the performance
/// bug #1114409. This is candidate for a public API but we can't
/// do that kind of work at the moment.
/// </remarks>
[FriendAccessAllowed] // Built into Base, also used by Framework.
internal void InvalidateSubProperty(DependencyProperty dp)
{
// when a sub property changes, send a Changed notification
// with old and new value being the same, and with
// IsASubPropertyChange set to true
NotifyPropertyChange(new DependencyPropertyChangedEventArgs(dp,
dp.GetMetadata(DependencyObjectType), GetValue(dp)));
}
注意函式說明部分,when a sub property changes, send a Changed notification with old and new value being the same, and with IsASubPropertyChange set to true,這完全符合我們的需求!!!這個函式本意是作為 Public API 的,但是由於效能的 bug #1114409,將其內部化了。那麼,我可以通過反射去呼叫它:
{
this.InvokeInternal<DependencyObject>("NotifySubPropertyChange", new object[] { DpColorProperty });
}
/// <summary>
/// 反射呼叫指定型別的 Internal 方法。
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="caller"></param>
/// <param name="method"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public static object InvokeInternal<T>(this T caller, string method, object[] parameters)
{
MethodInfo methodInfo = typeof(T).GetMethod(method, BindingFlags.Instance | BindingFlags.NonPublic);
return methodInfo?.Invoke(caller, parameters);
}
好了,完美解決!