属性和依赖属性

现在, 我们来解决另外一个概念问题, 可能看到上面, 你还是不太清楚属性和依赖属性它们的区别在哪里?

属性

很常见, 在C#中的标准属性,通常会由一个非静态类型的私有字段支持, 假设当前有一个对象, 它拥有100个标准属性,
并且背后都定义了一个4字节的字段, 如果我们初始化10000个这样的对象, 那么这些字段将占用100×4×10000= 3.81M 内存。
但是实际上, 我们并非使用到所有的属性, 这就意味着大多数内存会被浪费!

依赖属性

如何解决属性带来的问题? 我们回到现实生活当中想象一种场景, 假设老王和你的女朋友去旅游, 他们准备东西的时候大都是把必要的带上, 而不是说女朋友想喝水,要不然在行李箱里面放一箱水? 那么是不是意味着上厕所把纸带上? 洗发水? 沐浴露? 天呐, 这真是一场糟糕的旅行。
我们都知道, 水、厕所纸、洗发水、沐浴露这些酒店里面都有阿, 为什么要我们自己带? 所以我们懂了, 这些不必要带的东西我们可以依赖外部提供给我们。是的, 我们把这种思想带到编程当中。
所以, 这就是WPF当中的依赖属性的理念, 也许你在其它的地方都听过别人讲解过依赖属性, 并且他们都告诉你依赖属性本身没有值, 可以依赖绑定来源获得值。

一、CLR 属性#

程序的本质是“数据+算法”,或者说用算法来处理数据以期得到输出结果。在程序中,数据表现为各种各样的变量,算法则表现为各种各样的函数(操作符是函数的简记法)。
类的作用是把散落在程序中的变量和函数进行归档封装并控制它们的访问。被封装在类里的变量称为字段(Field),它表示的是类或实例的状态;被封装在类里的函数称为方法(Method),它表示类或实例的功能。
字段(Field)被封装在实例里,要么能被外界访问(非 Private修饰),要么不能(使用 Private 修饰),这种直接把数据暴露给外界的做法很不安全,很容易把错误的数值写入字段。为了解决此问题,.NET Framework 推出了属性(Property),这种 .NET Framework 属性又称为 CLR 属性。
属性是一种成员,它提供灵活的机制来读取、写入或计算私有字段的值。

  1. private double _seconds;
  2. public double Hours
  3. {
  4. get { return _seconds / 3600; }
  5. set {
  6. if (value < 0 || value > 24)
  7. throw new ArgumentOutOfRangeException(
  8. $"{nameof(value)} must be between 0 and 24.");
  9. _seconds = value * 3600;
  10. }
  11. }

二、依赖属性(Dependency Property)

实例中每个 CLR 都包装着一个非静态的字段(或者说由一个非静态的字段在后台支持)。如果一个 TextBox 有 100 个属性,每个属性都包装着一个 4 byte 的字段,那如果程序运行创建 10000 个 TexBox 时,属性将占用 100410000≈3.8M 的内存。在这 100 个属性中,最常用的是 Text 属性,这意味着大多数的内存都会被浪费掉。为了解决此问题,WPF 推出了依赖属性。
依赖属性(Dependency Property),就是一种可以自己没有值,但能通过 Binding 从数据源获得值(依赖在别人身上)的属性。拥有依赖属性的对象被称为“依赖对象”。
WPF 中允许对象在被创建的时候并不包含用于存储数据的空间(即字段所占用的空间)、只保留在需要用到数据时能够获得默认值、借用其他对象数据或实时分配空间的能力——这种对象被称为“依赖对象(Dependency Object)”,这种实时获取数据的能力依靠依赖属性(Dependency Property)来实现。
WPF 中,必须使用依赖对象作为依赖属性的宿主,使二者结合起来,才能形成完整的 Binding 目标被数据所驱动。依赖对象的概念由
DependencyObject 类实现,依赖属性的概念由 DependencyProperty 类实现。DependencyObject 类具有 GetValue 和 SetValue 两个方法。具体实现一个依赖属性如下图所示(在 Visual Studio 中可以使用 “propdp*
” 按 Tab 键快捷生成):

  1. public class StudentObject : DependencyObject
  2. {
  3. // CLR包装
  4. public int MyProperty
  5. {
  6. get { return (int)GetValue(MyPropertyProperty); }
  7. set { SetValue(MyPropertyProperty, value); }
  8. }
  9. // Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
  10. public static readonly DependencyProperty MyPropertyProperty =
  11. DependencyProperty.Register("MyProperty", typeof(int), typeof(StudentObject), new PropertyMetadata(0));
  12. }

WPF 中控件的属性大多数都为依赖属性,例如,Window 窗体的Title 属性,我们查看代码后如下:

  1. /// <summary>获取或设置窗口的标题。</summary>
  2. /// <returns>
  3. /// 一个 <see cref="T:System.String" /> ,其中包含窗口的标题。
  4. /// </returns>
  5. [Localizability(LocalizationCategory.Title)]
  6. public string Title
  7. {
  8. get
  9. {
  10. this.VerifyContextAndObjectState();
  11. return (string) this.GetValue(Window.TitleProperty);
  12. }
  13. set
  14. {
  15. this.VerifyContextAndObjectState();
  16. this.SetValue(Window.TitleProperty, (object) value);
  17. }
  18. }

DependencyObject 。即 WPF 中所有 UI 控件都是依赖对象,UI 控件的大多数属性都已经依赖化了。
当我们为依赖属性添加 CLR 包装时,就相当于为依赖对象准备了暴露数据的 Binding Path,即该依赖对象具备扮演数据源(Source)和数据目标(Target)的能力。该依赖对象虽然没有实现 INotifyPropertyChanged 接口,但当属性的值发生改变的时候与之关联的 Binding 对象依然可以得到通知,依赖属性默认带有这样的功能,具体如下:
我们声明一个自定义控件,控件的依赖属性为 DisplayText:

  1. public class MorTextBox : TextBox
  2. {
  3. public string DipalyText
  4. {
  5. get { return (string)GetValue(DipalyTextProperty); }
  6. set { SetValue(DipalyTextProperty, value); }
  7. }
  8. // Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
  9. public static readonly DependencyProperty DipalyTextProperty =
  10. DependencyProperty.Register("DipalyText", typeof(string), typeof(MorTextBox), new PropertyMetadata(""));
  11. public new BindingExpressionBase SetBinding(DependencyProperty dp, BindingBase binding)
  12. {
  13. return BindingOperations.SetBinding(this, dp, binding);
  14. }
  15. }

我们先把该依赖属性作为数据目标获取第一个 TextBox 的 Text 属性,然后把自己的 DisplayText 依赖属性的值作为第二个 TextBox 的 Text 属性的数据源:

  1. <StackPanel>
  2. <TextBox Margin="5" Height="50" x:Name="t1"></TextBox>
  3. <!--自定义依赖属性作为 Target-->
  4. <local:MorTextBox x:Name="t2" Visibility="Collapsed"
  5. DipalyText="{Binding ElementName=t1,Path=Text,UpdateSourceTrigger=PropertyChanged}">
  6. </local:MorTextBox>
  7. <!--自定义依赖属性作为 Source-->
  8. <local:MorTextBox Margin="5" Height="50"
  9. Text="{Binding ElementName=t2,Path=DipalyText,UpdateSourceTrigger=PropertyChanged}">
  10. </local:MorTextBox>
  11. </StackPanel>

当我们运行程序后,第二个 TextBox 的数值随着第一个 TextBox 数值的改变而改变。

三、附加属性(Attached Properties)

实际开发中,我们会经常遇到这样的情况,一个人在学校的时候需要记录班级等信息,在公司需要记录职业等信息,那么如果我们在设计 Human 类的时候,在类里面直接定义 Grade、Position 属性合适吗?

显然不合适!首先,当我们在学校上学的时候完全用不到公司等信息,那么Position 所占的内存就被浪费了。为了解决此问题,我们首先想到依赖属性,但解决了内存浪费问题,还存在一个问题,即一旦流程改变,那么 Human 类就需要做出改动,例如:当我们乘车的时候,有车次信息;去医院看病的时候,有排号信息等。这意味着应用场景的不断变化,导致我们所属的信息不断发生变化。为了解决此问题,.NET 推出了附加属性(Attached Properties)。

附加属性(Attached Properties)是说一个属性本来不属于某个对象,但由于某种需求而被后来附加上。也就是说把对象放入一个特定环境后对象才具有的属性(表现出来就是被环境赋予的某种属性)
上述例子,我们可以使用附加属性去解决这个问题(添加附加属性时,可以在 Visual studio 中输入 “propa“ 然后按 Tab 键快捷生成):

  1. class Human : DependencyObject
  2. {
  3. public string Name { get; set; }
  4. public int Age { get; set; }
  5. }
  6. class School : DependencyObject
  7. {
  8. public static string GetGrade(DependencyObject obj)
  9. {
  10. return (string) obj.GetValue(GradeProperty);
  11. }
  12. public static void SetGrade(DependencyObject obj, string value)
  13. {
  14. obj.SetValue(GradeProperty, value);
  15. }
  16. // Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
  17. public static readonly DependencyProperty GradeProperty =
  18. DependencyProperty.RegisterAttached("Grade", typeof(string), typeof(School), new PropertyMetadata(""));
  19. }
  20. class Company : DependencyObject
  21. {
  22. public static string GetPosition(DependencyObject obj)
  23. {
  24. return (string) obj.GetValue(PositionProperty);
  25. }
  26. public static void SetPosition(DependencyObject obj, string value)
  27. {
  28. obj.SetValue(PositionProperty, value);
  29. }
  30. // Using a DependencyProperty as the backing store for Position. This enables animation, styling, binding, etc...
  31. public static readonly DependencyProperty PositionProperty =
  32. DependencyProperty.RegisterAttached("PPosition", typeof(string), typeof(Company), new PropertyMetadata(""));
  33. }

使用依赖属性的方式如下:

  1. // Attached Properties
  2. {
  3. Human human0 = new Human() { Name = "John", Age = 10, };
  4. School.SetGrade(human0, "四年级二班");
  5. Human human1 = new Human() { Name = "Andy", Age = 26, };
  6. Company.SetPosition(human1, "软件工程师");
  7. Human human2 = new Human() { Name = "Kolity", Age = 25, };
  8. Company.SetPosition(human2, "产品经理");
  9. TextBoxAttached.Text += $"{human0.Name},{human0.Age},{School.GetGrade(human0)}\r\n";
  10. TextBoxAttached.Text += $"{human1.Name},{human1.Age},{Company.GetPosition(human1)}\r\n";
  11. TextBoxAttached.Text += $"{human2.Name},{human2.Age},{Company.GetPosition(human2)}\r\n";
  12. }

输出结果,如下所示:

  1. John10,四年级二班
  2. Andy26,软件工程师
  3. Kolity25,产品经理

从附加属性的实现中,我们可以看出附加属性(Attached Properties)的本质就是依赖属性(Dependency Property)。附加属性通过声明与依赖属性相关的 Get 与 Set 方法实现寄宿在宿主类(例如:Human)上,这意味宿主类也必须实现 DependencyObject 类。
其实,WPF 控件的布局控件的许多属性就为附加属性,例如:当把一个 TextBox 放入 Grid中时,对于 TextBox 而言我们可以使用 Grid 的 Row 和 Column 属性,如下:

  1. <Grid >
  2. <TextBox Grid.Row="0" Grid.Column="0"></TextBox>
  3. </Grid>

放入 Canvas 中,可以使用 Canvas 的 Left 等附加属性:

  1. <Canvas>
  2. <TextBox Canvas.Left="0" Canvas.Right="100" Canvas.Bottom="20" Canvas.Top="8"></TextBox>
  3. </Canvas>

附加属性(Attached Properties)的本质是依赖属性(Dependency Property),因此,附加属性也可以使用 Binding 依赖在其他对象的数据上,例如:我们通过两个 Slider 来控制矩形在 Canvas 中的横纵坐标:

  1. <Canvas x:Name="c1">
  2. <Slider x:Name="s1" Width="200" Height="50" Canvas.Top="10" Canvas.Left="50" Maximum="300"/>
  3. <Slider x:Name="s2" Width="200" Height="50" Canvas.Top="40" Canvas.Left="50" Maximum="400"/>
  4. <Rectangle Fill="CadetBlue" Width="30" Height="30" Canvas.Left="{Binding ElementName=s1,Path=Value}"
  5. Canvas.Top="{Binding ElementName=s2,Path=Value}"></Rectangle>
  6. </Canvas>