添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
备案 控制台
学习
实践
活动
专区
工具
TVP
写文章
专栏首页 walterlv - 吕毅的博客 WPF 让普通 CLR 属性支持 XAML 绑定(非依赖属性),这样 MarkupExtension 中定义的属性也能使用绑定了
2 0

海报分享

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=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();

现在虽然可以编译通过,但是我们会遇到两个问题:

  1. ValueProperty 的变更通知的回调函数中,我们只能找到 _dependencyObject 的实例,而无法找到外面的类型 WalterlvExtension 的实例;这几乎使得 Value 的变更通知完全失效。
  2. 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);
            SetValue(_attachedProperty, value);
    public static void ValueChangeCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        ((ClrBindingExchanger) d)._valueChangeCallback?.Invoke(e.OldValue, e.NewValue);
}

这段代码的意思是这样的:

  1. 构造函数中的 owner 参数完全没有用,我只是拿来备用,你可以删掉。
  2. 构造函数中的 attachedProperty 参数是需要定义的附加属性。
    • 因为前面我们说过,有一个附加属性才可以编译通过,所以附加属性是一定要定义的
    • 既然一定要定义附加属性,那么就可以用起来,接下来会用
  3. 构造函数中的 valueChangeCallback 参数是为了指定变更通知的,因为前面我们说变更通知不好做,于是就这样代理做变更通知。
  4. GetValue SetValue 这两个方法是用来代替 DependencyObject 自带的 GetValue SetValue 的,目的是执行我们希望特别执行的方法。
  5. SetValue 中我们需要自己考虑绑定对象,如果发现是绑定,那么就真的进行一次绑定。
  6. 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;