WPF 让普通 CLR 属性支持 XAML 绑定(非依赖属性),这样 MarkupExtension 中定义的属性也能使用绑定了

原文: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;
    }
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

可以在 XAML 中直接赋值:

<Button Content="{local:Walterlv Value=walterlv.com" />


  • 1

但不能绑定:

<TextBox x:Name="SourceTextBox" Text="walterlv.com" />
<Button Content="{local:Walterlv Value={Binding Text, Source={x:Reference SourceTextBox}}}" />


  • 1
  • 2

因为运行时会报错,提示绑定必须被设置到依赖对象的依赖属性中。在设计器中也可以看到提示不能绑定。

解决

实际上这个问题是能够解决的(不过也花了我一些时间思考解决方案)。

既然绑定需要一个依赖属性,那么我们就定义一个依赖属性。非依赖对象中不能定义依赖属性,于是我们定义附加属性。

// 注意:这一段代码实际上是无效的。

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


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这里问题来了,获取和设置附加属性是需要一个依赖对象的,那么我们哪里去找依赖对象呢?直接定义一个新的就好了。

于是我们定义一个新的依赖对象:

// 注意:这一段代码实际上是无效的。

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

  1. ValueProperty 的变更通知的回调函数中,我们只能找到 _dependencyObject 的实例,而无法找到外面的类型 WalterlvExtension 的实例;这几乎使得 Value 的变更通知完全失效。
  2. Valueset 方法中得到的 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);
    }
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

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

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


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

对于一个属性来说,代码确实多了些,这实在是让人难受。可是,这可以达成目的呀!

解释一下:

  1. 定义一个 _valueExchanger,就是在使用我们刚刚写的那个新类。
  2. 在构造函数中对 _valueExchanger 进行初始化,因为要传入 this 和一个实例方法 OnValueChanged,所以只能在构造函数中初始化。
  3. 定义一个附加属性(前面我们说了,一定要有依赖属性才可以编译通过哦)。
    • 注意属性的变更通知方法,需要固定写成 ClrBindingExchanger.ValueChangeCallback
  4. 定义普通的 CLR 属性 Value
    • GetValue 方法要换成我们自定义的 GetValue
    • SetValue 方法也要换成我们自定义的 SetValue 哦,这样绑定才可以生效
  5. OnValueChanged 就是我们实际的变更通知,这里得到的 oldValuenewValue 就是你期望的值,而不是我面前面奇怪的绑定实例。

于是,绑定就这么在一个普通的类型和一个普通的 CLR 属性中生效了,而且还获得了变更通知。

参考资料

本文没有任何参考资料,所有方法都是我(walterlv)的原创方法,因为真的找不到资料呀!不过在找资料的过程中发现了一些没解决的文档或帖子:



我的博客会首发于 https://walterlv.com/,而 CSDN 和博客园仅从其中摘选发布,而且一旦发布了就不再更新。

如果在博客看到有任何不懂的内容,欢迎交流。我搭建了 dotnet 职业技术学院 欢迎大家加入。

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://walterlv.blog.csdn.net/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。


walter lv

博客专家

发布了382 篇原创文章 · 获赞 232 · 访问量 47万+

私信
关注

原文地址:https://www.cnblogs.com/lonelyxmas/p/12052030.html

时间: 2024-08-04 14:16:01

WPF 让普通 CLR 属性支持 XAML 绑定(非依赖属性),这样 MarkupExtension 中定义的属性也能使用绑定了的相关文章

Swift中的类型属性(静态变量)

http://blog.haohtml.com/archives/15098 Swift中的类型属性(静态变量) Posted on 2014/06/13 类型属性语法 在 C 或 Objective-C 中,静态常量和静态变量的定义是通过特定类型加上global关键字.在 Swift 编程语言中,类型属性是作为类型定义的一部分写在类型最外层的花括号内,因此它的作用范围也就在类型支持的范围内. 使用关键字static来定义值类型的类型属性,关键字class来为类(class)定义类型属性.下面的

对vue中的computed属性,watch监听,计算属性理解

自己的理解: computed用来监控自己定义的变量,该变量不在data里面声明,直接在computed里面定义,然后就可以在页面上进行双向数据绑定展示出结果或者用作其他处理: computed比较适合对多个变量或者对象进行处理后返回一个结果值,也就是数多个变量中的某一个值发生了变化则我们监控的这个值也就会发生变化,举例:购物车里面的商品列表和总金额之间的关系,只要商品列表里面的商品数量发生变化,或减少或增多或删除商品,总金额都应该发生变化.这里的这个总金额使用computed属性来进行计算是最

iOS开发——UI篇&amp;文字渐变效果:图层中的mask属性

文字渐变效果:图层中的mask属性 本次文章,主要讲述的是图层中的mask属性,利用它,可以做出文字渐变效果! 一.文字渐变效果: 二.文字渐变实现思路: 1.创建一个颜色渐变层,渐变图层跟文字控件一样大. 2.用文字图层裁剪渐变层,只保留文字部分,就会让渐变层只保留有文字的部分,相当于间接让渐变层显示文字,我们看到的其实是被裁剪过后,渐变层的部分内容. 注意:如果用文字图层裁剪渐变层,文字图层就不在拥有显示功能,这个图层就被弄来裁剪了,不会显示,在下面代码中也会有说明. 2.1 创建一个带有文

Android中xml中shape的属性助记

对于shape的一些属性老是忘记,每次都要去查找,为了方便我就自己记录下来,方便以后忘记了再查看这些属性. 在shape中有几个属性rectangle.oval.line.ring1.环形ring    在shape标签中把shape属性设置为ring类型    然后在shape标签中设置以下属性(这些属性只能在shape值为ring是使用)    android:innerRadius       内环的半径,尺寸单位是dp(不设置这个参数,会有个默认值)    android:thickne

Java静态属性、静态方法、非静态属性、非静态方法的继承问题简单了解

package com.book.web.controller; /**  * 父类  * @author liweihan  *  */ public class Parent { public String normalStr = "父类非静态属性"; public static String staticStr = "父类静态属性"; public String changeStr = "父类-变量"; public static Stri

javascript构造函数类和原型prototype定义的属性和方法的区别

1.把方法写在原型中比写在构造函数中消耗的内存更小,因为在内存中一个类的原型只有一个,写在原型中的行为可以被所有实例共享,实例化的时候并不会在实例的内存中再复制一份而写在类中的方法,实例化的时候会在每个实例中再复制一份,所以消耗的内存更高所以没有特殊原因,我们一般把属性写到类中,而行为写到原型中2.构造函数中定义的属性和方法要比原型中定义的属性和方法的优先级高,如果定义了同名称的属性和方法,构造函数中的将会覆盖原型中的代码如下: <!DOCTYPE html> <html> <

python在类中使用__slot__属性

在类中定义__slot__属性来限制实例的属性字段,在创建大量对象的场合可以减少内存占用. 创建大量对象是内存占用对比: 类中不使用__slot__ class MySlot:def __init__(self, a, b, c): self.a = a self.b = b self.c = c @profile() def main(): myObj_list = list() for i in range(50000): myObj = MySlot(i, i, i) myObj_list

WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)

一. 摘要 首先圣殿骑士非常高兴这个系列能得到大家的关注和支持.这个系列从七月份開始到如今才第七篇,上一篇公布是在8月2日,掐指一算有二十多天没有继续更新了,最主要原因一来是想把它写好,二来是由于近期几个月在筹备"云计算之旅"系列,所以一再推迟了公布进度. 之前一直都没有想过要录制视频.基本的原因还是怕自己知识有限,从而误导他人,所曾经几次浪曦和51CTO邀请录制视频,我都以工作忙.公司内部培训须要时间和自己有待提高等理由委婉的拒绝了,说实在的.自己也知道自己还有非常多地方有待提高.还

WPF之旅(二)- XAML

什么是XAML XAML(Extensible Application Markup Lanaguage的简写,发音“zammel”)是用于实例化.NET对象的标记语言.尽管XAML是一种可以用于诸多不同问题领域的技术,但其主要作用是构造WPF用户界面.换言之,XAML文档定义了在WPF应用程序中组成窗口的面板.按钮以及各种控件的布局. WPF之前的图形用户设计界面存在的问题 1. 每个图形元素(背景和按钮等)需要导出为单独的位图.这限制了组合位图的能力和使用动态效果的能力,如反锯齿.透明和阴影