WPF 依赖属性

依赖属性的基本介绍

WPF带来了很多新的特性,它的一大亮点是引入了一种新的属性机制——依赖属性。依赖属性基本应用在了WPF的所有需要设置属性的元素。依赖属性根据多个提供对象来决定它的值(可以是动画、父类元素、绑定、样式和模板等),同时这个值也能及时响应变化。所以WPF拥有了依赖属性后,代码写起来就比较得心应手,功能实现上也变得非常容易了。如果没有依赖属性,我们将不得不编写大量的代码。关于WPF的依赖属性,主要有下面三个优点:

1、新功能的引入:加入了属性变化通知,限制、验证等等功能,这样就可以使我们更方便的实现我们的应用,同时也使代码量大大减少了,许多之前不可能的功能都可以轻松的实现了。
2、节约内存:在WinForm等项目开发中,你会发现UI控件的属性通常都是赋予的初始值,为每一个属性存储一个字段将是对内存的巨大浪费。WPF依赖属性解决了这个问题,它内部使用高效的稀疏存储系统,仅仅存储改变了的属性,即默认值在依赖属性中只存储一次。
3、支持多个提供对象:我们可以通过多种方式来设置依赖属性的值。同时其内部可以储存多个值,配合Expression、Style、Animation等可以给我们带来很强的开发体验。
  在.NET当中,属性是我们很熟悉的,封装类的字段,表示类的状态,编译后被转化为对应的get和set方法,属性可以被类或结构等使用。

属性是我们再熟悉不过的了,那么究竟依赖属性怎么写呢?依赖属性和属性到底有什么区别和联系呢?其实依赖属性的实现很简单,只要做以下步骤就可以实现:
第一步: 让所在类型继承自 DependencyObject基类,在WPF中,我们仔细观察框架的类图结构,你会发现几乎所有的 WPF 控件都间接继承自DependencyObject类型。
第二步:使用 public static 声明一个 DependencyProperty的变量,该变量才是真正的依赖属性 ,看源码就知道这里其实用了简单的单例模式的原理进行了封装(构造函数私有),只暴露Register方法给外部调用。
第三步:在静态构造函数中完成依赖属性的元数据注册,并获取对象引用,看代码就知道是把刚才声明的依赖属性放入到一个类似于容器的地方。
第四步:在前面的三步中,我们完成了一个依赖属性的注册,那么我们怎样才能对这个依赖属性进行读写呢?答案就是提供一个依赖属性的实例化包装属性,通过这个属性来实现具体的读写操作。

 

public class SampleDPClass : DependencyObject
{
    //声明一个静态只读的DependencyProperty字段
    public static readonly DependencyProperty SampleProperty;

    static SampleDPClass()
    {
        //注册我们定义的依赖属性Sample
        SampleProperty = DependencyProperty.Register("Sample", typeof(string), typeof(SampleDPClass),
            new PropertyMetadata("What's that!", OnValueChanged));
    }

    private static void OnValueChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        //当值改变时,我们可以在此做一些逻辑处理
    }

    //属性包装器,通过它来读取和设置我们刚才注册的依赖属性
    public string Sample
    {
        get { return (string)GetValue(SampleProperty); }
        set { SetValue(SampleProperty, value); }
    }
}
 
 
我们一般.NET属性是直接对类的一个私有属性进行封装,所以读取值的时候,也就是直接读取这个字段;而依赖属性则是通过调用继承自DependencyObject的GetValue()和SetValue来进行操作,它实际存储在DependencyProperty的一个IDictionary的键-值配对字典中,所以一条记录中的键(Key)就是该属性的HashCode值,而值(Value)则是我们注册的DependencyProperty。
 

依赖属性的优先级

优先级按从高到低排序:

  1. 属性系统强制转换。强制转换和动画都作用于称为“基值”的值。便于动画不受别的设置影响。
  2. 活动动画或具有 Hold 行为的动画。为了获得任何实用效果,属性的动画必须优先于基(未动画)值,即使该值是在本地设置的情况下也将如此。
  3. 本地值。本地值可以通过“包装”属性 (Property) 的便利性进行设置,这也相当于在 XAML 中设置Attribute 或 Property 元素,或者使用特定实例的属性调用 SetValue API。如果您使用绑定或资源来设置本地值,则每个值都按照直接设置值的优先级顺序来应用。
  4. TemplatedParent 模板属性。如果元素是作为模板(ControlTemplate 或 DataTemplate)的一部分创建的,则具有 TemplatedParent。在模板中,按以下优先级顺序应用:
    1. 来自 TemplatedParent 模板的触发器。
    2. TemplatedParent 模板中的属性 (Property) 集。(通常通过 XAML 属性 (Attribute) 进行设置。)
  5. 隐式样式。仅应用于Style属性。Style属性是由任何样式资源通过与其类型匹配的键来填充的。该样式资源必须存在于页面或应用程序中;查找隐式样式资源不会进入到主题中。
  6. 样式触发器。来自页面或应用程序上的样式中的触发器。(这些样式可以是显式或隐式样式,但不是来自优先级较低的默认样式。)
  7. 模板触发器。来自样式中的模板或者直接应用的模板的任何触发器。
  8. 样式 Setter来自页面或应用程序的样式中的 Setter 的值。
  9. 默认(主题)样式。在默认样式中,按以下优先级顺序应用:
    1. 主题样式中的活动触发器。
    2. 主题样式中的 Setter。
  10. 继承。有几个依赖项属性从父元素向子元素继承值,因此不需要在应用程序中的每个元素上专门设置这些属性。
  11. 来自依赖项属性元数据的默认值。任何给定的依赖项属性都具有一个默认值,它由该特定属性的属性系统注册来确定。而且,继承依赖项属性的派生类具有按照类型重写该元数据(包括默认值)的选项。因为继承是在默认值之前检查的,所以对于继承的属性,父元素的默认值优先于子元素。因此,如果任何地方都没有设置可继承的属性,将使用在根元素或父元素中指定的默认值,而不是子元素的默认值。

 

依赖属性的继承

依赖属性继承的最初意愿是父元素的相关设置会自动传递给所有层次的子元素 ,即元素可以从其在树中的父级继承依赖项属性的值。这个我们在编程当中接触得比较多,如当我们修改窗体父容器控件的字体设置时,所有级别的子控件都将自动使用该字体设置 (前提是该子控件未做自定义设置),如下面的代码:

<Window x:Class="Using_Inherited_Dps.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    WindowStartupLocation="CenterScreen" 
    FontSize="20"
    Title="依赖属性的继承" Height="400" Width="578">
    <StackPanel >
        <Label Content="继承自Window的FontSize" />
        <Label Content="重写了继承" 
               TextElement.FontSize="36"/>
        <StatusBar>没有继承自Window的FontSize,Statusbar</StatusBar>
    </StackPanel>
</Window> 
 

Window.FontSize 设置会影响所有的内部元素字体大小,这就是所谓的属性值继承,如上面代码中的第一个Label没有定义FontSize ,所以它继承了Window.FontSize的值。但一旦子元素提供了显式设置,这种继承就会被打断,如第二个Label定义了自己的FontSize,所以这个时候继承的值就不会再起作用了。

  这个时候你会发现一个很奇怪的问题:虽然StatusBar没有重写FontSize,同时它也是Window的子元素,但是它的字体大小却没有变化,保持了系统默认值。那这是什么原因呢?作为初学者可能都很纳闷,官方不是说了原则是这样的,为什么会出现表里不一的情况呢?其实仔细研究才发现并不是所有的元素都支持属性值继承。还会存在一些意外的情况,那么总的来说是由于以下两个方面:

1、有些Dependency属性在用注册的时候时指定Inherits为不可继承,这样继承就会失效了。

2、有其他更优先级的设置设置了该值,在前面讲的的“依赖属性的优先级”你可以看到具体的优先级别。

  这里的原因是部分控件如StatusBar、Tooptip和Menu等内部设置它们的字体属性值以匹配当前系统。这样用户通过操作系统的控制面板来修改它们的外观。这种方法存在一个问题:StatusBar等截获了从父元素继承来的属性,并且不影响其子元素。比如,如果我们在StatusBar中添加了一个Button。那么这个Button的字体属性会因为StatusBar的截断而没有任何改变,将保留其默认值。

 

附加属性

附加属性是一种特殊的依赖属性。他允许给一个对象添加一个值,而该对象可能对此值一无所知。

  附加属性的一个用途是允许不同的子元素为 实际在父元素中定义 的属性指定唯一值。如Canvas需要Top和left来布局,DockPanel需要Dock来布局。

下面代码中的Button 就是用了Canvas的Canvas.Top和Canvas.Left="20" 来进行布局定位,那么这两个就是传说中的附加属性。

<Canvas>
<Button Canvas.Top="20" Canvas.Left="20"/>
</Canvas>
如何创建附加属性:

1. 声明一个 DependencyProperty 类型的 public static readonly 字段,将附加属性定义为一个依赖项属性。
2. 使用 RegisterAttached 方法的返回值来定义此字段。

public class OwerClass : DependencyObject
    {

        public static string GetAttachedPropertyName(DependencyObject obj)
        {
            return (string)obj.GetValue(AttachedPropertyNameProperty);
        }

        public static void SetAttachedPropertyName(DependencyObject obj, string value)
        {
            obj.SetValue(AttachedPropertyNameProperty, value);
        }

        public static readonly DependencyProperty AttachedPropertyNameProperty =
            DependencyProperty.RegisterAttached("AttachedPropertyName"typeof(string), typeof(OwerClass), new UIPropertyMetadata(0));
    }

  

变更通知

    1. 当依赖属性的值改变了,WPF就会自动根据属性的元数据(metadata)触发一系列动作,这就叫做变更通知。
    2. 属性触发器是变更通知的特性之一。即当属性值改变时,执行自定义动作,而不需要更改任何过程式代码。
    1. 属性触发器不能直接应用到元素上,它只能在Style对象内部应用。
    1. 除了属性触发器,还有数据触发器和事件触发器。事件触发器会通过声明方式指定动作,该动作在路由事件触发时生效。

 

例如:

<Trigger Property="IsMouseOver" Value="True">

<Setter Property="Foreground" Value="Blue" />

</Trigger>

上面触发器基于Button的IsMouseOver属性工作,在MouseEnter触发时,IsMouseOver属性变为true,触发器将Foreground设置为Blue;在MouseLeave触发时,IsMouseOver属性变为false,WPF自动将触发器的改动还原。

例如:

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

Title="About WPF Unleashed"

Background="OrangeRed">

<StackPanel>

<Button Width="100">

OK

<Button.Style>

<Style TargetType="{x:Type Button}">

<Style.Triggers>

<Trigger Property="IsMouseOver" Value="True">

<Setter Property="Foreground" Value="Blue" />

</Trigger>

</Style.Triggers>

</Style>

</Button.Style>

</Button>

</StackPanel>

</Page>

非元素属性的变更通知通过实现INotifyPropertyChanged

public class Student : INotifyPropertyChanged
    {
        private string studentID;
        public string StudentID
        {
            get { return studentID; }
            set
            {
                studentID = value;
                NotifyPropertyChange("StudentID");
            }
        }
        private string studentName;
        public string StudentName
        {
            get { return studentName; }
            set
            {
                studentName = value;
                NotifyPropertyChange("StudentName");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChange(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
posted @ 2012-08-20 15:47  Sam.Sun  阅读(698)  评论(0)    收藏  举报