1. 程式人生 > >WPF 主動觸發依賴屬性的 PropertyChanged

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 分別是 MessageDpMessage

Message 或者 DpMessage 變化(注意,變化指的是重新賦值,即,引用了新的 Message

例項),繫結的目標會收到通知,更新 UI。

問題來了:當 Message 或者 DpMessage 的屬性變化了呢,Message 類是實現了 INotifyPropertyChanged 的,屬性變化能觸發自身的變化(MessageViewModel.INotifyPropertyChanged)通知嗎?答案是,不能。即,Message .MSG Message .Stack 變化了,View 不能得到更新通知!這不符合需求。

  • 當然,這個問題可以通過更改繫結物件避過,即,將繫結物件直接設定為 Message 或者 DpMessage,然後使用 MultiBinding,將需要的屬性都繫結過去,也可以同樣實現需求,但是,這種方式的缺點多:繁瑣
    (如果涉及的屬性數量非常大呢)、不直觀(目標是 Message 整體,卻綁定了其屬性)
    等。

二、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);
}

好了,完美解決!