WPF 讓普通 CLR 屬性支援 XAML 繫結(非依賴屬性),這樣 MarkupExtension 中定義的屬性也能使用綁...
如果你寫了一個 MarkupExtension
在 XAML 當中使用,你會發現你在 MarkupExtension
中定時的屬性是無法使用 XAML 繫結的,因為 MarkupExtension
不是一個 DependencyObject
。
本文將給出解決方案,讓你能夠在任意的型別中寫出支援 XAML 繫結的屬性;而不一定要依賴物件( DependencyObject
)和依賴屬性( DependencyProperty
)。
問題
下面是一個很簡單的 MarkupExtension
,使用者設定了什麼值,就返回什麼值。拿這麼簡單的型別只是為了避免額外引入複雜的理解難度。
public class WalterlvExtension : MarkupExtension { private object _value; public object Value { get => _value; set => _value = value; } public override object ProvideValue(IServiceProvider serviceProvider) { return Value; } }
可以在 XAML 中直接賦值:
<Button Content="{local:Walterlv Value=walterlv.com" />
但不能繫結:
<TextBox x:Name="SourceTextBox" Text="walterlv.com" /> <Button Content="{local:Walterlv Value={Binding Text, Source={x:Reference SourceTextBox}}}" />
因為執行時會報錯,提示繫結必須被設定到依賴物件的依賴屬性中。在設計器中也可以看到提示不能繫結。
解決
實際上這個問題是能夠解決的(不過也花了我一些時間思考解決方案)。
既然繫結需要一個依賴屬性,那麼我們就定義一個依賴屬性。非依賴物件中不能定義依賴屬性,於是我們定義附加屬性。
// 注意:這一段程式碼實際上是無效的。 public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached( "Value", typeof(object), typeof(WalterlvExtension), new PropertyMetadata(default(object))); public object Value { get => ???.GetValue(ValueProperty); set => ???.SetValue(ValueProperty, value); }
這裡問題來了,獲取和設定附加屬性是需要一個依賴物件的,那麼我們哪裡去找依賴物件呢?直接定義一個新的就好了。
於是我們定義一個新的依賴物件:
// 注意:這一段程式碼實際上是無效的。 public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached( "Value", typeof(object), typeof(WalterlvExtension), new PropertyMetadata(default(object))); public object Value { get => _dependencyObject.GetValue(ValueProperty); set => _dependencyObject.SetValue(ValueProperty, value); } private readonly DependencyObject _dependencyObject = new DependencyObject();
現在雖然可以編譯通過,但是我們會遇到兩個問題:
-
ValueProperty
的變更通知的回撥函式中,我們只能找到_dependencyObject
的例項,而無法找到外面的型別WalterlvExtension
的例項;這幾乎使得Value
的變更通知完全失效。 - 在
Value
的set
方法中得到的value
值是一個Binding
物件,而不是正常依賴屬性中得到的繫結的結果;這意味著我們無法直接使用Value
的值。
為了解決這兩個問題,我必須自己寫一個代理的依賴物件,用於幫助做屬性的變更通知,以及處理繫結產生的 Binding
物件。在正常的依賴物件和依賴屬性中,這些本來都不需要我們自己來處理。
方案
於是我寫了一個代理的依賴物件,我把它命名為 ClrBindingExchanger
,意思是將 CLR 屬性和依賴屬性的繫結進行交換。
程式碼如下:
public class ClrBindingExchanger : DependencyObject { private readonly object _owner; private readonly DependencyProperty _attachedProperty; private readonly Action<object, object> _valueChangeCallback; public ClrBindingExchanger(object owner, DependencyProperty attachedProperty, Action<object, object> valueChangeCallback = null) { _owner = owner; _attachedProperty = attachedProperty; _valueChangeCallback = valueChangeCallback; } public object GetValue() { return GetValue(_attachedProperty); } public void SetValue(object value) { if (value is Binding binding) { BindingOperations.SetBinding(this, _attachedProperty, binding); } else { SetValue(_attachedProperty, value); } } public static void ValueChangeCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((ClrBindingExchanger) d)._valueChangeCallback?.Invoke(e.OldValue, e.NewValue); } }
這段程式碼的意思是這樣的:
- 建構函式中的
owner
引數完全沒有用,我只是拿來備用,你可以刪掉。 - 建構函式中的
attachedProperty
引數是需要定義的附加屬性。- 因為前面我們說過,有一個附加屬性才可以編譯通過,所以附加屬性是一定要定義的
- 既然一定要定義附加屬性,那麼就可以用起來,接下來會用
- 建構函式中的
valueChangeCallback
引數是為了指定變更通知的,因為前面我們說變更通知不好做,於是就這樣代理做變更通知。 -
GetValue
和SetValue
這兩個方法是用來代替DependencyObject
自帶的GetValue
和SetValue
的,目的是執行我們希望特別執行的方法。 -
SetValue
中我們需要自己考慮繫結物件,如果發現是繫結,那麼就真的進行一次繫結。 -
ValueChangeCallback
是給附加屬性用的,因為用我的這種方法定義附加屬性時,只能寫出相同的程式碼,所以乾脆就提取出來。
而用法是這樣的:
public class WalterlvExtension : MarkupExtension { public WalterlvExtension() { _valueExchanger = new ClrBindingExchanger(this, ValueProperty, OnValueChanged); } private readonly ClrBindingExchanger _valueExchanger; public static readonly DependencyProperty ValueProperty = DependencyProperty.RegisterAttached( "Value", typeof(object), typeof(WalterlvExtension), new PropertyMetadata(null, ClrBindingExchanger.ValueChangeCallback)); public object Value { get => _valueExchanger.GetValue(); set => _valueExchanger.SetValue(value); } private void OnValueChanged(object oldValue, object newValue) { // 在這裡可以處理 Value 屬性值改變的變更通知。 } public override object ProvideValue(IServiceProvider serviceProvider) { return Value; } }
對於一個屬性來說,程式碼確實多了些,這實在是讓人難受。可是,這可以達成目的呀!
解釋一下:
- 定義一個
_valueExchanger
,就是在使用我們剛剛寫的那個新類。 - 在建構函式中對
_valueExchanger
進行初始化,因為要傳入this
和一個例項方法OnValueChanged
,所以只能在建構函式中初始化。 - 定義一個附加屬性(前面我們說了,一定要有依賴屬性才可以編譯通過哦)。
- 注意屬性的變更通知方法,需要固定寫成
ClrBindingExchanger.ValueChangeCallback
- 注意屬性的變更通知方法,需要固定寫成
- 定義普通的 CLR 屬性
Value
-
GetValue
方法要換成我們自定義的GetValue
哦 -
SetValue
方法也要換成我們自定義的SetValue
哦,這樣繫結才可以生效
-
-
OnValueChanged
就是我們實際的變更通知,這裡得到的oldValue
和newValue
就是你期望的值,而不是我面前面奇怪的繫結例項。
於是,繫結就這麼在一個普通的型別和一個普通的 CLR 屬性中生效了,而且還獲得了變更通知。
參考資料
本文沒有任何參考資料,所有方法都是我(walterlv)的原創方法,因為真的找不到資料呀!不過在找資料的過程中發現了一些沒解決的文件或帖子:
- How to use CLR property as binding target?
- CLR Object Binding In WPF
- wpf - MarkupExtension with binding parameters - Stack Overflow
- c# - Binding to dependency and regular properties in WPF - Stack Overflow
- c# - XAML bind to DependencyProperty instance held in a CLR property - Stack Overflow
- Tore Senneseth’s blog » Custom Markup Extension with bindable properties
- Markup Extensions for XAML Overview - Microsoft Docs
- Service Contexts Available to Type Converters and Markup Extensions - Microsoft Docs
本文會經常更新,請閱讀原文: https://walterlv.com/post/add-wpf-xaml-binding-support-for-clr-property.html ,以避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗。
本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名 呂毅 (包含連結:https://walterlv.com ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。如有任何疑問,請 與我聯絡 ([email protected]) 。