WPF依赖属性
简介
这是一篇记录笔者阅读学习刘铁猛老师的《深入浅出WPF》的读书笔记,如果文中内容阅读不畅,推荐购买正版书籍详细阅读。
什么是属性
概念:属性是一种成员,它提供灵活的机制来读取、写入或计算私有字段的值。 属性可用作公共数据成员,但它们实际上是称为访问器的特殊方法。 这使得可以轻松访问数据,还有助于提高方法的安全性和灵活性。
理解:属性其实就是外界访问私有字段的入口,属性本身不保存任何数据,在对属性赋值和读取的时候其实就是操作的对应私有字段,通过反编译工具我们可以看出属性本质其实就是一个方法,通过get和set方法来操作对应的字段。
属性与字段的相同处:
- 它是命名的类成员。
- 它有类型。
- 它可以被赋值和读取。
属性和字段的不同处:
- 它不为数据存储分配内存!
- 它执行代码。
C#语言规定:对类有意义的字段和方法使用static关键字修饰、称为静态成员,通过类名加操作符(即“.”操作符)可以访问它们;对类的实例有意义的字段和方法不加static关键字,被称为非静态成员或实例成员。从语义来看:静态成员与非静态成员有很好的对称性,但从程序在内存中的结构来看,这种对称就被打破了。静态字段在内存中只会有一个拷贝,非静态字段则是每个实例拥有一个拷贝,无论方法是否伪静态的,在内存中只会有一份拷贝,区别只是你能通过类名来访问存放指令的内存,还是通过实例名来访问存放指令的内存。
什么是依赖属性
概念:Windows Presentation Foundation(WPF)提供了一组服务,可用于扩展类型属性的功能。这些服务通常统称为WPF属性系统。WPF属性系统支持的属性称为依赖项属性。
理解:依赖属性就是一种可以自己没有值,并且能通过使用Binding从数据源获得值(依赖在别人身上)的属性。拥有依赖属性的对象被称为“依赖对象”。与传统CLR属性和面向对象思想相比依赖属性有很多新颖之处,其中包括:
- 节省实例对内存的开销。
- 属性值可以通过Binding依赖在其他对象上。
依赖属性对内存的使用方式
CLR属性内存使用方式:实例的每个CLR属性都包装着一个非静态的字段(或者说由一个非静态的字段在后台支持),思考这样一个问题:TextBox有138个属性,假设每个CLR属性都包装着一个4字节的字段,如果程序运行的时候创建了10列1000行的一个TextBox列表,那么这些字段将占用413810*1000≈5.26M内存!在这100个属性中最常用的也就是TextBox属性,这意味着大多数内存都会浪费掉。
依赖属性内存使用方式:WPF允许对象在被创建的时候并不包含用于存储数据的空间(即字段所占用的空间)、只保留在需要用到数据时能够获得的默认值、借用其他对象数据或实时分配空间的能力—这种对象就被称为依赖对象(Dependency Object)而它这种实时获取数据的能力则依靠依赖属性(Dependency Property)来实现。WPF开发中,必须使用依赖对象作为依赖属性的宿主,使二者结合起来,才能形成完整的Binding目标被数据所驱动。
理解:一个登山队员,他的全部装备有很多,包括登山服、登山靴、登山杖、护目镜、绳索、无线电、水、食品甚至还有氧气瓶等。倘若使去登穆朗玛峰,这些装备都需要带上,要是去登香山呢?如果也背着氧气瓶起步怪哉!所以,实际一点的办法是用得着就带上,用不着就不带,有必要得时候可以借别人得用一下。这就是WPF中依赖属性的原理。
WPF系统中依赖对象的概念被DependencyObject 类所实现,依赖属性的概念则由DependencyProperty类所实现。DependencyObject具有GetValue和SetValue两个方法:
//
// 摘要:
// 表示参与依赖属性系统的对象。
[NameScopeProperty("NameScope", typeof(NameScope))]
public class DependencyObject : DispatcherObject
{
//
// 摘要:
// 对 System.Windows.DependencyObject 的此实例返回依赖属性的当前有效值。
//
// 参数:
// dp:
// 要检索其值的属性的 System.Windows.DependencyProperty 标识符。
//
// 返回结果:
// 返回当前有效值。
//
// 异常:
// T:System.InvalidOperationException:
// 指定 dp 或其值无效,或者指定 dp 不存在。
public object GetValue(DependencyProperty dp);
//
// 摘要:
// 设置依赖属性的本地值,该值由其依赖属性标识符指定。
//
// 参数:
// dp:
// 要设置的依赖项属性的标识符。
//
// value:
// 新的本地值。
//
// 异常:
// T:System.InvalidOperationException:
// 尝试修改只读依赖属性或密封 System.Windows.DependencyObject 上的属性。
//
// T:System.ArgumentException:
// value 的类型不是为 dp 属性注册时使用的正确类型。
public void SetValue(DependencyProperty dp, object value);
}
这两个方法都以DependencyProperty对象作为参数,GetValue方法通过DependencyProperty对象获取数据;SetValue通过DependencyProperty对象存储值—正式这两个方法把DependencyObject和DependencyProperty紧密结合在一起。
DependencyObject是WPF系统中相当底层的一个基类,WPF的所有UI控件都是依赖对象,UI控件的绝大多数属性都已经依赖化了。
声明和使用依赖属性
- 声明依赖属性,定义表示属性的对象,它是DependencyProperty的实例。属性信息应该始终保持可用,甚至可能需要在多个类之间共享这些信息(在WPF元素中这是十分普遍的)。因此,必须将DependencyProperty对象定义为与其相关联的类的静态字段。依赖属性就是这个由 public static readonly修饰的DependencyProperty实例。
- 注册依赖项属性,这一步骤需要在任何使用属性的代码之前完成,因此必须在与其关联的类的静态构造函数中进行。
- 添加CLR属性包装器
语法糖:propdp
DependencyProperty实例的声明特点很鲜明—引用变量由public static readonly三个修饰符修饰,实例并非使用new操作符得到而是使用DependencyProperty.Register方法生成
//声明、注册依赖属性、添加CLR属性包装器
public class Student:DependencyObject
{
//01 声明依赖属性
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register("Name",typeof(string),typeof(Student)); //02 注册依赖属性
//03 添加CLR包装器
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty,value); }
}
}
//使用自定义依赖属性
private void Button_Click(object sender, RoutedEventArgs e)
{
Student stu = new Student();
stu.Name = this.textBox1.Text;
this.textBox2.Text = stu.Name;
}
//
// 摘要:
// 使用指定的属性名称、属性类型、所有者类型和属性元数据注册依赖属性。
//
// 参数:
// name:
// 要注册的依赖属性的名称。 用这个参数指明以那个CLR属性作为这个依赖属性的包装器,或者说此依赖属性支持(back)的是哪个CLR属性。因此参数值为Name
//
// propertyType:
// 属性的类型。 依赖属性用来存储什么类型的值,学生的姓名是Steing类型,因以参数为typeof(string)
//
// ownerType:
// 正在注册依赖属性的所有者类型。 依赖属性的宿主是什么类型,或者说DependencyProperty.Register方法将把这个依赖属性注册关联到那个类型上。本例中的意图是为Student类准备一个可依赖的名称属性,所以需要把NameProperty注册成与Student关联,因此参数值为typeof(Student)
//
// typeMetadata:
// 依赖属性的属性元数据。 这个参数的类型是PropertyMetadata,作用是给依赖属性的DefaultMetadata属性赋值。顾名思义,DefaultMetadata的作用是向依赖属性的调用者提供一些基本信息,这些信息包括:
// CoercerValueCallback:依赖属性被强制改变时,此委托会被调用,此委托可关联一个影响的函数。
// DefaultValue:依赖属性未被显示赋值时,若读取之则获得此默认值,不设此值会抛异常。
// IsSealed:控制PropertyMetadata的属性值是否可以更改,默认值为true。
// PropertyChangedCallback:依赖属性的值被改变之后此委托会被调用,此委托可关联一个影响函数。
//
// 返回结果:
// 一个依赖属性标识符,应使用它来设置类中 public static readonly 字段的值。 稍后将此标识符用来引用依赖属性,从而实现以编程方式设置其值或获取元数据等操作。
public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata);
依赖属性值存取的秘密
一句话概括DependencyProperty对象的创建与注册,那就是:创建也给DependencyProperty实例并用它的CLR属性名和宿主类型名生成hash code,最后把hash code和DependencyProperty实例作为Key-Value对存入全局的、名为PropertyFromName的Hashtable中。这样,WPF属性系统通过CLR属性名和宿主类型名就可以从这个全局的Hashtable中检索处对应的DependencyProperty实例。最后,生成的DependencyProperty实例被当作返回值交还。
其中被static关键字所修饰的依赖属性对象其作用是用来检索真正的属性值而不是存储值;被用作检索键值的实际上是依赖属性的GlobalIndex属性(本质是其hash code,而hash code又被CLR包装器名和宿主类型名共同决定)为了保证GlobalIndex属性值的稳定性,我们声明的时候又使用了readonly关键字进行修饰。
WPF系统的设计理念,即以public static 类型的变量作为标记,并以这个标记为索引进行对象的存储访问、修改、删除等操作。(后面的路由事件、命令系统等也都会用到这样的理念)。同时,我们也可以理解为什么WPF在性能上还不尽如意。
什么是附加属性
概念:附加属性是XAML定义的概念。附加属性旨在用作可在任何对象上设置的全局属性的类型。在Windows Presentation Foundation(WPF)中,通常将附加属性定义为不具有常规属性“包装器”的依赖项属性的一种特殊形式。
理解:附加属性的本质就是依赖属性,二者尽在注册和包装器上有一点区别。附加属性是说一个属性本来不属于某个对象,但由于某种需求而被后来附加上。也就是把对象放入一个特定环境后对象才具有的属性(表现出来的就是被环境赋予的属性)就称为附加属性。
作用:附加属性的作用就是将属性与数据类型(宿主)解耦,让数据类型的设计更加灵活。
语法糖:propa
应用场景:附加属性的目的之一是允许不同的子元素为在父元素中定义的属性指定唯一值。此场景的特定应用是让子元素通知父元素如何在用户界面(UI)中呈现它们。一个示例是DockPanel.Dock属性。所述DockPanel.Dock因为它被设计为在其被包含在内的元件设置场所被作为附加属性创建DockPanel中,而不是在DockPanel中本身。该DockPanel中类定义静态的DependencyProperty命名的字段DockProperty,然后提供GetDock和SetDock方法作为附加属性的公共访问器。
示例:
// 使用语法糖propa 然后完形填空即可
public static int GetGrade(DependencyObject obj)
{
return (int)obj.GetValue(GradeProperty);
}
public static void SetGrade(DependencyObject obj, int value)
{
obj.SetValue(GradeProperty, value);
}
// Using a DependencyProperty as the backing store for Grade. This enables animation, styling, binding, etc...
public static readonly DependencyProperty GradeProperty =
DependencyProperty.RegisterAttached("Grade", typeof(int), typeof(School), new PropertyMetadata(0));
可以明显地看出,GradeProperty就是一个DependencyProperty类型成员变量,声明式一样使用public static readonly 三个关键字共同修饰。唯一的不同就是注册附加属性使用的是名为RegisterAttached的方法,但参数却与使用Register方法无异。附加属性的包装器也与依赖属性不同—依赖属性使用CLR属性对GetValue和SetValue两个方法进行包装,附加属性则使用两个方法分别进行包装—这样做完全是为了在使用的时候保持语句行文上的通畅。