今天我们来学习WPF一个比较重要的概念:依赖属性。这里推荐大家看看周永恒大哥的文章,讲的确实很不错。我理解的没那么深入,只能发表一下自己的浅见。提到依赖属性,不得不说我们经常使用的传统的.net属性,大家都比较了解,一般拥有get和set访问器,它只是一个语法糖,在CLR层面上其实是两个方法(传统属性也叫CLR属性)和一个私有的字段,由于实例方法在内存中只有一份,所以属性不会过多增加内存负担。和CLR属性相比,依赖属性有哪些特点呢?首先我们来自定义一个具有IsTransparent的Button。

自定义依赖属性

public class MyButton:Button
{
//第一步:声明并注册依赖属性,设置默认值为false
public static readonly DependencyProperty IsTransparentProperty =
DependencyProperty.Register("IsTransparent", typeof(bool), typeof(MyButton), new FrameworkPropertyMetadata(defaultValue: false, propertyChangedCallback: new PropertyChangedCallback(IsTransparentChanged)));
//第二步:为依赖属性提供.net包装器
public bool IsTransparent
{
get { return (bool)GetValue(IsTransparentProperty); }
set {SetValue(IsTransparentProperty, value);}
}
public static void IsTransparentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//Value Change
}
}

使用propdp这个Code Snippet可以快速创建一个依赖属性。

这里有一个命名约定,依赖属性的名字要以Property结尾来表明它是一个依赖属性。

声明注册的依赖属性是static readonly类型,保证了唯一性。.net包装器不是必须的。

DependencyProperty类采用了Singleton设计模式设计,由DependencyProperty.Register方法返回一个DependencyProperty实例,该方法有三个重载方法:

//
// 摘要:
// 使用指定的属性名称、属性类型和所有者类型注册依赖项属性。
//
// 参数:
// name:
// 要注册的依赖项对象的名称。在所有者类型的注册命名空间内,名称必须是唯一的。
//
// propertyType:
// 属性的类型。
//
// ownerType:
// 正注册依赖项对象的所有者类型。
//
// 返回结果:
// 一个依赖项对象标识符,应使用它在您的类中设置 public static readonly 字段的值。然后,在以后使用该标识符引用依赖项对象,用于某些操作,例如以编程方式设置其值,或者获取元数据。
[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public static DependencyProperty Register(string name, Type propertyType, Type ownerType);
//
// 摘要:
// 使用指定的属性名称、属性类型、所有者类型和属性元数据注册依赖项属性。
//
// 参数:
// name:
// 要注册的依赖项对象的名称。
//
// propertyType:
// 属性的类型。
//
// ownerType:
// 正注册依赖项对象的所有者类型。
//
// typeMetadata:
// 依赖项对象的属性元数据。
//
// 返回结果:
// 一个依赖项对象标识符,应使用它在您的类中设置 public static readonly 字段的值。然后,在以后使用该标识符引用依赖项对象,用于某些操作,例如以编程方式设置其值,或者获取元数据。
[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata);
//
// 摘要:
// 使用指定的属性名称、属性类型、所有者类型、属性元数据和属性的值验证回调来注册依赖项属性。
//
// 参数:
// name:
// 要注册的依赖项对象的名称。
//
// propertyType:
// 属性的类型。
//
// ownerType:
// 正注册依赖项对象的所有者类型。
//
// typeMetadata:
// 依赖项对象的属性元数据。
//
// validateValueCallback:
// 对回调的引用,除了典型的类型验证之外,该引用还应执行依赖项对象值的任何自定义验证。
//
// 返回结果:
// 一个依赖项对象标识符,应使用它在您的类中设置 public static readonly 字段的值。然后,在以后使用该标识符引用依赖项对象,用于某些操作,例如以编程方式设置其值,或者获取元数据。
public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback);

我们来看下参数最全的一个重载方法:

public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback);
  • name参数:指定以哪个CLR属性来作为该依赖属性的包装器。一般以依赖属性去掉Property后的命名作为该值。
  • propertyType参数:指定该依赖属性的注册类型。
  • ownerType参数:指定该依赖属性要注册关联的类型。
  • typeMetadata参数:指定依赖属性的属性元数据,用来告诉WPF如何处理该属性、如何处理属性值改变的回调、强制值的转换以及如何验证。常用的PropertyMetadata有三个:PropertyMetadata、UIPropertyMetadata和FrameworkPropertyMetadata,按顺序存在继承关系。
  • validateValueCallback参数:delegate类型,指定用于验证属性的回调函数。

我们来具体说下最为复杂的FrameworkPropertyMetadata,它有这样一些属性:

还有两个方法:

  • Merge方法:当子类调用DependencyProperty实例的OverrideMetadata方法时调用
  • OnApply方法:当此元数据已经应用到一个属性时(这表明正在密封元数据)调用

工作原理简单剖析

前面我们在声明依赖属性的时候用的是Static类型,当把值直接存在该dp里面时,所有的拥有该dp的do的该dp的值都是一样的,这是不合实际的。那dp的值Set到哪里了呢?

原来在dp的内部维护了一个全局的map,key是由上面的name参数和ownerType参数各自的HashCode取异或得到的(保证唯一性)。这样还是没有解决问题,同一个类的不同实例的相同依赖属性的值在内存中还是只有一份。dp是依赖do的。在do中引入EffectiveValueEntry数组用来存储修改过的依赖属性值,在dp内部维护一个PropertyIndex,通过它去找该依赖属性修改值。

    internal struct EffectiveValueEntry
{
internal int PropertyIndex { get; set; }
internal object Value { get; set; }
}

然后,我们可以在DependencyProperty.Register的第四个PropertyMetadata类型参数中设置默认值。这样,当依赖属性修改后,我们去EffectiveValueEntry数组中去取值;当依赖属性未修改时,我们去取它的默认值。这自然节省了内存的占用。

变更通知

无论什么时候,只有属性值改变了,WPF就会自动根据属性的metadata触发一系列操作。这些动作例如有重新呈现适当的元素、更新当前布局等,它们是由metadata属性来决定的。内建的变更通知最有趣的特性之一是属性触发器,它可以在属性值改变时执行自定义操作而不用更改任何过程式代码。这个很好理解,可以直接在XAML页面使用属性触发器,而不用在过程式代码中写事件处理程序。我们来看个例子:

<local:MyButton Content="Hello,WPF" x:Name="btn" IsTransparent="True" Width="" Height="" Click="MyButton_Click">
<local:MyButton.Style>
<Style TargetType="{x:Type local:MyButton}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Foreground" Value="Blue" />
</Trigger>
</Style.Triggers>
</Style>
</local:MyButton.Style>
</local:MyButton>

当触发MouseEnter时,IsMouseOver为true,Button的前景色变蓝;当触发MouseLeave时,IsMouseOver为false,Button的前景色恢复黑色。还有数据触发器(DataTrigger)和事件触发器(EventTrigger)我们将在将style时再详细说明。

属性值继承

属性值继承并不是指传统的面向对象的类继承,而是指属性值沿着元素树自顶向下传递。举个例子说明:

<Window x:Class="DependencyPropertyDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300" FontSize="20">
<Grid>
<StackPanel>
<TextBlock Text="WPF" FontSize="50"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="WF" Width="100px"/>
<TextBlock Text="SliverLight" />
</StackPanel>
<StatusBar>WPF is Working!</StatusBar>
</StackPanel>
</Grid>
</Window>

效果如下:

我们在Window设置了FontSize="20",Window下第一个TextBlock的FontSize值,我们进行了显式地设置,重载了继承的值20。其余的TextBlock的FontSize都变成了20。然而,并不是所有的子元素都会继承这个FontSize值,例如StatusBar,虽然StatusBar具有FontSize属性。属性值的继承行为由以下两个因素决定:

1.并不是每个依赖属性都参与属性继承(从其内部来讲,依赖属性会通过传递FrameWorkMatadataOptions.Inherits给DependencyProperty.Register方法注册来完成继承)

2.有其它优先级更高的源来设置这些属性值。

有一些控件例如StatusBar、Menu、Tooltip等,其内部会将字体属性设置为当前系统设置。而且,它们会阻止继承沿着元素树继续向下传递。属性值继承在其它地方的应用:属性值继承并不只是发生在逻辑树或者可视树的子元素,也发生在元素的触发器或任何属性(不只是Content和Children属性),只要继承自Freezable类就行。

对多个提供程序的支持

WPF有很多强大的机制可以独立地去尝试设置依赖属性的值。

基础值(BaseValue)的来源很多,通过优先级来判断BaseValue。优先级从高到低如下排列:

  1. 本地值(LocalValue):通过调用SetValue方法设值,表现为在XAML页面直接赋值或者通过过程式代码赋值
  2. 样式触发器
  3. 模板触发器
  4. 样式设置程序
  5. 主题样式触发器:主题样式就是WPF系统内置的一些样式
  6. 主题样式设置程序
  7. 属性值继承:子类从父类继承过来的依赖属性值
  8. 默认值:在注册时设置的初始值

如果属性值是一个表达式的话,会转换成具体的值。

如果属性值是一个动画的话,它可以改变或者替代当前的属性值。

如果注册时给出了CoerceValueCallBack,会调用该回调函数,返回一个基于自定义逻辑的值。例如ProgressBar当Value小于Minimum时,Value等于Minimum;当Value大于Maximum时,Value等于Maximum。

如果注册时给出了ValidateValueCallBack,就会将值传入来判断是否有效。

当无法判断属性值的来源时,可以使用DependencyPropertyHelper.GetValueSource方法来获取一个ValueSource结构:

  • BaseValueSource:它是一个枚举值,反应上面的基础值的来源。
  • IsExpression:判断是否是一个表达式。
  • IsAnimated:判断是否是执行动画。
  • IsCoerced:判断是否是强制值转换。

我们来看下在属性值继承的那个例子,来看下为什么TextBlock会继承Window的FontSize属性而StatusBar不会。

ValueSource vs = DependencyPropertyHelper.GetValueSource(this.tb1, TextBlock.FontSizeProperty);
MessageBox.Show((vs.BaseValueSource == BaseValueSource.Local).ToString());//true,在XAML中直接赋值 ValueSource vs1 = DependencyPropertyHelper.GetValueSource(this.tb2, TextBlock.FontSizeProperty);
MessageBox.Show((vs1.BaseValueSource == BaseValueSource.Inherited).ToString());//true,继承自Window ValueSource vs2 = DependencyPropertyHelper.GetValueSource(this.sb1, StatusBar.FontSizeProperty);
MessageBox.Show((vs2.BaseValueSource == BaseValueSource.DefaultStyle).ToString());//true,系统内置样式

另外,我们可以使用DependencyObject.ClearValue()方法来清除某依赖属性的Local Value,让该依赖属性重新确认BaseValue,还以上面为例:

this.tb1.ClearValue(TextBlock.FontSizeProperty);
ValueSource vs = DependencyPropertyHelper.GetValueSource(this.tb1, TextBlock.FontSizeProperty);
MessageBox.Show((vs.BaseValueSource == BaseValueSource.Inherited).ToString());//true,恢复继承自Window

依赖属性的共享

当某一个类需要和其它类共享某一依赖属性,而这些类并不一定要有继承关系,我们就可以用DependentyProperty的AddOwner方法来实现共享该依赖属性。在WPF的类库实现中,TextBlock和Control的FontFamilyProperty属性就共享了TextElement的FontFamilyProperty属性。

public class TextBlock
{
public static readonly DependencyProperty FontFamilyProperty =
TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock), new UIPropertyMetadata(null));
}

当我们在自定义元素时,可以这种方式方便地实现依赖属性的重用。在WPF内部大量使用了这种方式。其实,不仅依赖属性可以共享,在后面要说的路由事件也同样可以使用RoutedEvent的AddOwner方法共享。

附加属性

附加属性也是依赖属性,是依赖属性的一种特殊形式,可以被有效地添加到任何对象中。先来看下附加属性的声明注册:

public class MyTooltip
{
public static bool GetAttached(DependencyObject obj)
{
return (bool)obj.GetValue(AttachedProperty);
} public static void SetAttached(DependencyObject obj, bool value)
{
obj.SetValue(AttachedProperty, value);
} // Using a DependencyProperty as the backing store for Attached. This enables animation, styling, binding, etc...
public static readonly DependencyProperty AttachedProperty =
DependencyProperty.RegisterAttached("Attached", typeof(bool), typeof(MyButton), new UIPropertyMetadata(false, new PropertyChangedCallback(OnPropertyChanged))); private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
Slider slider = sender as Slider;
if (slider != null)
{
Button btn = new Button();
btn.Content = "Ok";
TextBlock tb = new TextBlock(new Run("This is a Tooltip!"));
StackPanel sp = new StackPanel();
sp.Children.Add(tb);
sp.Children.Add(btn);
slider.ToolTip = sp;
}
}
}
}

这里我们用propa这个Code Snippet快速构建了一个AttachedProperty附加属性,通过DependencyProperty.RegisterAttached方法来声明注册,该方法签名和声明注册依赖属性的方法签名一样,最大的不同是依赖属性宿主是依赖对象,而附加属性宿主任意,还有不同的就是依赖属性通过CLR属性进行了封装,而附加属性则通过静态方法封装。

我们这样来使用上面的代码:

<Slider local:MyTooltip.Attached="True" Minimum="" Maximum=""/>

效果如下所示:

假如有这样一种场景,在属性值继承那里的例子中,我们要求最里面的StackPanel中的所有TextBlock采用Script MT字体。

很明显,在TextBlock上直接设置不是民智之举,因为可能有很多。这时想到属性值继承,在StackPanel上设置。然而不幸的是,StackPanel没有FontFamily属性(也不需要)。这时,附加属性粉墨登场了。

<Window x:Class="DependencyPropertyDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300" FontSize="20">
<Grid>
<StackPanel>
<TextBlock x:Name="tb1" Text="WPF" FontSize="50"/>
<StackPanel Orientation="Horizontal" TextElement.FontFamily="Script MT">
<TextBlock x:Name="tb2" Text="WF" Width="100px"/>
<TextBlock Text="SliverLight" />
</StackPanel>
<StatusBar x:Name="sb1">WPF is Working!
<Button Content="Pass"></Button>
</StatusBar>
</StackPanel>
</Grid>
</Window>

前面说过TextBlock和Control的一些文本相关的属性都是通过TextElement共享的方式获得的。这里将TextElement的FontFamily属性附加到StackPanel上,然后TextBlock进行了属性值的继承。当XAML的解析器或编译器遇到这样的语法时,会先去要求附加属性的提供者(TextElement)提供SetFontFamily这样的静态方法来设置相应的属性值。过程式代码这么实现:

TextElement.SetFontFamily(this.sp1, new FontFamily("Script MT"));

效果图如下:

现在是不是觉得附加属性也并不神奇了。说到底,依赖属性是附加在DependencyObject上,而附加属性是附加在任意对象上,本质上是一样的。

总结

依赖属性和依赖对象在WPF中举足轻重,重点是要了解依赖属性内部的工作机制。

WPF学习(5)依赖属性的更多相关文章

  1. WPF学习笔记——依赖属性(Dependency Property)

    1.什么是依赖属性 依赖属性是一种可以自己没有值,并且通过Binding从数据源获得值(依赖在别人身上)的属性,拥有依赖属性的对象被称为"依赖对象". 依赖项属性通过调用 Regi ...

  2. WPF 精修篇 依赖属性

    原文:WPF 精修篇 依赖属性 依赖属性使用场景 1. 希望可在样式中设置属性. 2. 希望属性支持数据绑定. 3. 希望可使用动态资源引用设置属性. 4. 希望从元素树中的父元素自动继承属性值. 5 ...

  3. WPF中的依赖属性

    1. WPF中的依赖属性 依赖属性是专门基于WPF创建的.在WPF库实现中,依赖属性使用普通的C#属性进行了包装,使用方法与普通的属性是相同的. 1.1 依赖属性提供的属性功能 资源 数据绑定 样式 ...

  4. WPF教程:依赖属性

    一.什么是依赖属性 依赖属性就是一种自己可以没有值,并且可以通过绑定从其他数据源获取值.依赖属性可支持WPF中的样式设置.数据绑定.继承.动画及默认值. 将所有的属性都设置为依赖属性并不总是正确的解决 ...

  5. [No000012D]WPF(5/7)依赖属性

    介绍 WPF带来了很多传统 Windows 应用程序没有的新特性和选择.我们已经讨论了一些 WPF 的特性,是时候更进一步介绍其他特性了.当你读完这个系列之前的文章,我希望你已经或多或少地了解了 WP ...

  6. (原创)2. WPF中的依赖属性之二

    1 依赖属性 1.1 依赖属性最终值的选用 WPF属性系统对依赖属性操作的基本步骤如下: 第一,确定Base Value,对同一个属性的赋值可能发生在很多地方.还用Button的宽度来进行举例,可能在 ...

  7. 【转】【WPF】关于依赖属性的ValidateValueCallback,PropertyChangedCallback和CoerceValueCallback的执行顺序

    三个回调对应依赖属性的验证过程,改变过程和强制转换过程. class Dobj : DependencyObject { //依赖属性包装 public int MyProperty { get { ...

  8. WPF usercontrol 自定义依赖属性

    1.依赖属性不同意一般属性,一般属性主要定义在对象中,而依赖属性是存在一个特殊的依赖属性表中.2.当我们触发改变值时,需要通过SetValue这种方式进行触发. UserControl1.xaml: ...

  9. WPF 主动触发依赖属性的 PropertyChanged

    需求背景 需要显示 ViewModel 中的 Message/DpMessage,显示内容根据其某些属性来确定.代码结构抽象如下: // Model public class Message : IN ...

  10. WPF快速入门系列(2)——深入解析依赖属性

    一.引言 感觉最近都颓废了,好久没有学习写博文了,出于负罪感,今天强烈逼迫自己开始更新WPF系列.尽管最近看到一篇WPF技术是否老矣的文章,但是还是不能阻止我系统学习WPF.今天继续分享WPF中一个最 ...

随机推荐

  1. java正則表達式的坑

    java中正則表達式比較有意思,这里列举几个常见的坑 1.[]符号,中括号表示当中的数据都是或的关系 假设[\\w+]是匹配条件 abc能否够匹配的到呢? 首先\\w(注意这里没有中括号)表示a-z ...

  2. zigbee学习:示例程序SampleApp中按键工作流程

    zigbee学习:示例程序SampleApp中按键工作流程 本文博客链接:http://blog.csdn.net/jdh99,作者:jdh,转载请注明. 环境: 主机:WIN7 开发环境:IAR8. ...

  3. Coder的利器

    Coder的利器记载 工作近三年,使用PC快六年,拥抱Mac整一年,投具器石榴裙三年.14年第一次被同事推荐Everything,开启了JeffJade对工具的折腾之旅,并乐此不疲.时去两年,这必然是 ...

  4. windows phone 加速计(5)

    原文:windows phone 加速计(5) 在windows phone 中存在着加速计,我们可以利用加速计获得用户手机的状态,根据手机状态调整我们的程序,这样会更人性化:windows phon ...

  5. SharePoint发展 - 使用Session(代码更改webconfig)

    博客地址 http://blog.csdn.net/foxdave SharePoint启用Session能够使用Powershell,戳这里:能够改动webconfig. 本篇叙述的重点是通过fea ...

  6. hdu2457 Trie图+dp

    hdu2457 给定n个模式串, 和一个文本串 问如果修改最少的字符串使得文本串不包含模式串, 输出最少的次数,如果不能修改成功,则输出-1 dp[i][j] 表示长度为i的字符串, 到达状态j(Tr ...

  7. siwft初学(一)

    今天刚開始学习swift语言.首先须要下载xcode6 beta版本号.正式版本号然后会公布.自己学习总结一下,假设有误.请大家指出. 创建project的时候.language选择swift语言. ...

  8. 【Nginx】epoll事件驱动模块

    Linux 2.4之前的内核版本号,Nginx事件驱动的方法是使用poll.select功能.过程必须等待一个事件发生在连接上(接收数据)时间,部连接都告诉内核,由内核找出哪些连接上有事件发生.因为须 ...

  9. [Windows Phone] 如何撰写连接 Wifi、蓝芽、网路、飞航模式的网路设定功能

    原文:[Windows Phone] 如何撰写连接 Wifi.蓝芽.网路.飞航模式的网路设定功能 前言 为了可以使自己的 APP 具备操作网路的功能,在本文分享研究心得,包含在 Windows Pho ...

  10. Swift String length property

    Swift的String居然没有length属性,好难受,每次要获取String的字符串长度都要借助全局函数countElements. 没办法.仅仅有扩展String结构体,给它加入一个属性了. i ...