依赖属性和附加属性

    重混江湖后的第一篇文章,竟然有些手生......(惶恐+惭愧)ing,怕是套路也要有些变化了-_-


一.属性

    刚着手开始学习C#的时候,不明白为什么会有属性这个东西,不是已经有了字段了吗,你说属性里面有get和set方法对数据进行了封装,可以通过对方法的访问限定来控制该属性是否可以被赋值,但是不也有readonly这个关键字可以用来修饰字段吗,你又说可以通过在get或set方法里面对数据进行一系列的操作来对数据进行限制,可以将数据的获取和赋值分开进行不同的处理,好吧这个我是服气的。

你可以这样子写(写prop再按两次Tab就自动生成了):

public int Age { get; set; }       //则就是个很普通的属性了
public int Age { get; }       //可以不写set方法,等同于通过访问限定符private来对set方法进行修饰

还可以这样写:

        private int _age;
        public int Age
        {
            get { return _age; }
            //这就比单纯的字段对数据的处理要来得自由
            private set
            {
                if (value < 0 || value > 100)
                    return;
                _age = value;
            }
        }
其实这都是很基础的东西了,但还是觉得此处应有个引入,直接上货怕有些干......

那下面要切入正题了——>


二.依赖属性

    呐,名字里都有个属性说明它就是个属性,只是这个属性在方法里封装处理的并不是普通的字段,而是类型为DependencyProperty的一个对象,我们可以先来看它的普通写法(propdp按一次Tab自动生成,好方便的说):

        public int MyProperty
        {
            get { return (int)GetValue(MyPropertyProperty); }
            set { SetValue(MyPropertyProperty, value); }
        }

        //下面的注释是自动生成的,可以看到这个对象就是用于MyProperty的封装数据,它可以作用于动画,样式,绑定等等......
        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MyPropertyProperty =
            DependencyProperty.Register("MyProperty", typeof(int), typeof(ownerclass), new PropertyMetadata(0));

上面的MyProperty属性这里不做解释,看一下MyPropertyProperty,这里要说明的是依赖属性的命名约定为以Property作为后缀,该对象使用了DependencyProperty类里面的Register静态方法的返回值:




可以看到Register方法有三个重载,前三个参数

  • String:表示所要对其进行封装的属性的名称;
  • 第一个Type:表示该属性所属类型;
  • 第二个Type:表示该属性所有者的类型,也就是在哪个类里面定义的这个依赖属性;
后面的参数,PropertyMetadata对象



这个对象里面包含有依赖属性的元数据相关的信息,元数据也就是一些用来描述类型的信息,这里包含例如属性默认值通知回调和验证回调的引用,还有一些框架性特征(布局、数据绑定等),其中参数:

  • Object:表示要对该属性指定的默认值,例如string类型可以指定default(string);
  • PropertyChangedCallback:从名字就可以看出,这是一个在属性值被更改时所调用的回调函数,常用于绑定数据变更时处理;
  • CoerceValueCallback:Coerce意思是胁迫、强制,也就是这个方法用于在对属性值进行改变的时候,对赋值进行检查,强制对值进行赋值,返回值为Object类型,这个才是要赋给属性的值;
Register方法的最后一个参数是ValidateValueCallback,这个方法同样是对值的检查,该方法如下:
public delegate bool ValidateValueCallback(object value)
传入的参数value为Object类型,也就是要进行检查的属性值,这里会不会有疑问,既然已经有了CoerceValueCallback方法可以对值进行检查之后强制改变要进行的赋值,为什么还要有这个合法值得检查? 因为CoerceValueCallback方法无论如何检查,返回值为Object,都会给属性值进行一个赋值,然后会调用PropertyChangedCallback方法,但是在进行CoerceValueCallback方法的调用之前,就会调用ValidateValueCallback方法对值进行检查,可以看到该方法的返回值为一个bool类型,当值满足条件时返回true,这时就会去调用CoerceValueCallback对值进行进一步的处理,如果不满足条件的话返回false,则本次赋值失败告终,根本就没有后续CoerceValueCallback什么事,也就是ValidateValueCallback方法是值更新前检查方法,对值进行的最初的拦截检查,可以看如下图:



所以,在对依赖属性进行注册的时候可以根据需要选择不同的Register方法和Parameter构造器,实现需要的方法来对数据进行处理;

前面讲了一堆,可能没有接触的人还会觉得云里雾里,或者说那些东西我看教程都会用,到底为什么要有依赖属性这么个东西?
在进行WPF桌面编程的时候,我们都会用到各种各样的控件,这些控件都有一个高台阶似的继承关系,控件中的属性也都是一层层继承下来的,如果都是普通属性的话,每个属性在每个类中都是需要占有一定字节数,可能有些属性并不会被用到,但还是会保留从父类那里继承来的初始值,这样的话,到了底层的类就会有对象膨胀的问题,所以写到这里你应该可以想到依赖属性就是解决这个问题的吧,当然了,不然我说这些是为了铺垫晚饭吃什么吗-_-
依赖属性是如何解决普通属性因为继承带来的对象膨胀的问题就是接下来要研究的依赖属性的原理:
那就从最直观的使用DependencyProperty.Register方法来切入,Register注册,注册什么呢,注册属性数据啊,你不是因为继承每个基类里面都会有一份属性吗,那就将这些属性从类里面剥离出来,放到一个公共的区域,如果有需要就去里面进行值的注册,获取的时候去公共区里面查找,这不是很方便吗,没有用到的属性就不往里放,问题是不是就解决了?那好,我们用图解来说明:


这里的公共区是一个全局字典,里面用来存放所有的依赖属性,其中:

  • Key是关键词,这个是在注册方法里面由所给的属性名和所属类进行哈希异或得到的哈希值;
  • value很好理解,就是每一个属性所有值;
  • index这个东西,因为每个依赖属性都是静态的,当进行注册之后相同的依赖属性只会在字典中保存一份,如果一个值更改那么其他所拥有者的值也会相应的进行更改,这显然不是我们所期望的,所以,真正进行存储的时候,全局字典中只会存储注册时候给的初始值,而对于其他通过继承方式得到该依赖属性并对其进行赋值的时候,其实是存储在一个单独的链表中,链表的存储对象中才存放每一个拥有者所赋的不同的值,哎呀看图吧:


上图中的index是相同的,每一次进行值操作的时候,会现在全局的字典中通过Key值拿到对应的Property数据,获取其中的index,然后通过在链表中进行对应的index查找,如果找到了就使用链表中的值,如果没有就使用本身的默认值;

那,如果我并不想要父类的那个依赖属性,我想把它变成自己的,也换一个默认值怎么办?重载咯:



把Type换成自己的类,重新指定PropertyMetadata就好了,这其实是在字典中重新注册了一个依赖属性,也可以通过这个方法重新写该属性的元数据;

那,我另外的一个类也想使用那个依赖属性,就只能通过继承的方式吗,我只是想用一个属性而已啊,要按其他不需要的也都继承下来吗?没这个必要啊:



通过DependencyProperty的AddOwner方法,就可以把那个属性变为自己可以使用的了,同样也可以更改其默认值,也是在字典中重新注册了一个依赖属性;

差不多吧,主要要说的就是这些,剩下的依赖属性的使用,比如在动画啊,样式或者绑定什么的都是另外的话了,哦还有优先级什么的,后续有需要再补充吧。


三.附加属性

简单讲,附加属性就是依赖属性,但它存在的意义是什么呢,先看写法:

public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.RegisterAttached(
  "MyProperty",
  typeof(Boolean),
  typeof(MyClass),
  new FrameworkPropertyMetadata());
public static void SetMyProperty(UIElement element, Boolean value)
{
  element.SetValue(MyPropertyProperty, value);
}
public static Boolean GetMyProperty(UIElement element)
{
  return (Boolean)element.GetValue(MyPropertyProperty);
}
可以看到除了注册方法是RegisterAttached之外还有另外独立出来的静态方法SetValue和GetValue,在这里RegisterAttached和Register方法并没有什么不同,都是往全局字典中添加属性;

但是可以看到在两个方法中有传入参数UIElement,所以有人说“依赖属性是给自己用的,附加属性是给别人用的”,这句话是没错的,在使用附加属性的时候回将使用者的信息作为参数传入进来,所以会有一些布局控件例如Grid,Canvas等有附加属性,Grid.SetRow和Canvas.SetLeft都是直接调用了静态的方法,通过传入的参数来告知属性所有者是谁用了我的属性,把它设为了什么值,这样在布局的时候我就知道要把谁往哪里放,这也是依赖属性和附加属性的一个区别。而且像这样单独将两个方法暴露出来也是为了方便他人使用。

至于其他方面,其实都是一样的。


四.栗子时间

前面讲了那么多,都不如直接上个栗子看的实在(我也实在是写累了):

假设这里有个需求,需要在TextBox里面填写年龄,默认值为成年18岁,但是不能超过100岁,也不能低于18岁,也就是有如下需求:

  • 有默认值
  • 有上下限
  • 有值检查
  • 有错误提醒
而且有个儿童的勾选项,当选择了儿童之后,默认值就要更改为1,且上下限为1~18,如果取消勾选项恢复为成人设定,这里设计如下:
C#后台代码:
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();

            SetTextBinding();
        }

        private void SetTextBinding()
        {
            var binding = new Binding
            {
                Source = this,
                Path = new PropertyPath("MyAge")
            };
            TestTextBox.SetBinding(TextBox.TextProperty, binding);
        }

        public int MyAge
        {
            get { return (int)GetValue(MyAgeProperty); }
            set { SetValue(MyAgeProperty, value); }
        }

        //这里注册一个MyAge的依赖属性用于前台绑定,指定默认值为18
        public static readonly DependencyProperty MyAgeProperty =
            DependencyProperty.Register("MyAge", typeof(int), typeof(MainWindow),
                new PropertyMetadata(18, PropertyChangedCallback, CoerceValueCallback), ValidateValueCallback);
        
        //当赋值成功的时候就改变TextBox的边框颜色
        private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
        {
            var window = dependencyObject as Window;
            var textBox = window?.FindName("TestTextBox") as TextBox;
            if (textBox != null) textBox.BorderBrush = Brushes.Cyan;
        }

        //当进行赋值的时候,如果值不在18~100之间,强制将值恢复为18,表示该值不合要求
        private static object CoerceValueCallback(DependencyObject dependencyObject, object baseValue)
        {
            var value = (int)baseValue;
            return value < 18 ? 18 : value;
        }
        
        //在开始赋值前对值进行检查,如果值超过100就弹个错误框显示
        private static bool ValidateValueCallback(object o)
        {
            var value = (int)o;
            if (value > 100)
                MessageBox.Show("年龄有点大哈");
            return value < 100;
        }

        //当儿童选项被勾选的时候去更改属性的元数据
        private void ChildCheckBox_OnChecked(object sender, RoutedEventArgs e)
        {
            ChildAge.Instance.ChangeMetadata(sender);
        }

        //当取消勾选的时候,应该恢复为原来的属性值绑定才能正确显示
        private void ChildCheckBox_OnUnchecked(object sender, RoutedEventArgs e)
        {
            SetTextBinding();
        }
    }

    //重新定义一个Child类继承MainWindow
    public class ChildAge : MainWindow
    {
        public static ChildAge Instance
        {
            get { return _instance ?? (_instance = new ChildAge()); }
        }

        private static ChildAge _instance;

        private static Window _containerWindow;
        public void ChangeMetadata(object sender)
        {
            var item = sender as CheckBox;
            if (item == null) return;

            //更改属性元数据,默认值为1,对应的值处理检查方法也需重新定义
            MyAgeProperty.OverrideMetadata(typeof(ChildAge),
                new PropertyMetadata(1, ChildAgePropertyChangedCallback, ChildCoerceValueCallback));

            ///这里因为前台数据是和后台绑定的,因此如果对应的属性改变了,前台绑定的源也应该随之改变
            _containerWindow = GetWindow(item);
            if (_containerWindow != null)
            {
                var binding = new Binding
                {
                    Source = this,
                    Path = new PropertyPath("MyAge")
                };
                var textBox = _containerWindow.FindName("TestTextBox") as TextBox;
                textBox?.SetBinding(TextBox.TextProperty, binding);
            }
        }

        private static object ChildCoerceValueCallback(DependencyObject dependencyObject, object baseValue)
        {
            var value = (int)baseValue;
            return (value < 1 || value >= 18) ? 1 : value;
        }

        private static void ChildAgePropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
        {
            var textBox = _containerWindow?.FindName("TestTextBox") as TextBox;
            if (textBox != null) textBox.BorderBrush = Brushes.DarkGreen;
        }
    }


前台如下设计:



程序运行结果如下:


至此结束对依赖属性和附加属性的讨论,如果对该文章有任何疑问或者有错误之处请及时指出,蟹蟹。
更多更详细的内容可以参见MSDN:MSDN依赖属性和附加属性






  • 11
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值