内容简介:如果你写了一个本文将给出解决方案,让你能够在任意的类型中写出支持 XAML 绑定的属性;而不一定要依赖对象(
如果你写了一个 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=blog.walterlv.com" />
但不能绑定:
<TextBox x:Name="SourceTextBox" Text="blog.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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 如何在WPF控件上绑定本地属性
- WPF 让普通 CLR 属性支持 XAML 绑定(非依赖属性),这样 MarkupExtension 中定义的属性也能使用绑...
- 聊聊spring boot tomcat jdbc pool的属性绑定
- WPF 中如何绑定附加属性?XAML 中记得加括号,C# 中记得不能用字符串
- Golang Echo数据绑定中time.Time类型绑定失败
- 如何在Symfony的表单中添加一个未绑定字段,否则绑定到一个实体?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。