WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)
[2] WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)
[3] WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)
[4] WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)
[5] WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)
[6] WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)
五. 依赖属性基本介绍
前面废话了这么久,到现在才真正进入今天的主题,对此感到非常抱歉,如果各位不喜欢,可以直接跳到这里阅读。大家都知道WPF带来了很多新的特性,它的一大亮点是引入了一种新的属性机制——依赖属性。依赖属性基本应用在了WPF的所有需要设置属性的元素。依赖属性根据多个提供对象来决定它的值(可以是动画、父类元素、绑定、样式和模板等),同时这个值也能及时响应变化。所以WPF拥有了依赖属性后,代码写起来就比较得心应手,功能实现上也变得非常容易了。如果没有依赖属性,我们将不得不编写大量的代码。关于WPF的依赖属性,主要有下面三个优点,我们的研究也重点放在这三点上:
1、新功能的引入:加入了属性变化通知,限制、验证等等功能,这样就可以使我们更方便的实现我们的应用,同时也使代码量大大减少了,许多之前不可能的功能都可以轻松的实现了。
2、节约内存:在WinForm等项目开发中,你会发现UI控件的属性通常都是赋予的初始值,为每一个属性存储一个字段将是对内存的巨大浪费。WPF依赖属性解决了这个问题,它内部使用高效的稀疏存储系统,仅仅存储改变了的属性,即默认值在依赖属性中只存储一次。
3、支持多个提供对象:我们可以通过多种方式来设置依赖属性的值。同时其内部可以储存多个值,配合Expression、Style、Animation等可以给我们带来很强的开发体验。
在.NET当中,属性是我们很熟悉的,封装类的字段,表示类的状态,编译后被转化为对应的get和set方法(在JAVA里面没有属性的概念,通常都是写相应的方法来对字段进行封装)。属性可以被类或结构等使用。 一个简单的属性如下,也是我们常用的写法:
private string sampleProperty;
public string SampleProperty
{
get
{
return sampleProperty;
}
set
{
if (value != null)
{
sampleProperty = value;
}
else
{
sampleProperty = "Knights Warrior!";
}
}
}
属性是我们再熟悉不过的了,那么究竟依赖属性怎么写呢?依赖属性和属性到底有什么区别和联系呢?其实依赖属性的实现很简单,只要做以下步骤就可以实现:
第一步: 让所在类型继承自 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("Knights Warrior!", 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。
六. 依赖属性的优先级
由于WPF 允许我们可以在多个地方设置依赖属性的值,所以我们就必须要用一个标准来保证值的优先级别。比如下面的例子中,我们在三个地方设置了按钮的背景颜色,那么哪一个设置才会是最终的结果呢?是Black、Red还是Azure呢?
<Window x:Class="WpfApplication1.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">
<Grid>
<Button x:Name="myButton" Background="Azure">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="Black"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
Click
</Button>
</Grid>
</Window>
通过前面的简单介绍,我们了解了简单的依赖属性,每次访问一个依赖属性,它内部会按照下面的顺序由高到底处理该值。详细见下图
由于这个流程图偏理想化,很多时候我们会遇到各种各样的问题,这里也不可能一句话、两句话就能够把它彻底说清楚,所以我们就不过多纠缠。等遇到问题之后要仔细分析,在找到原因之后也要不断总结、举一反三,只有这样才能逐渐提高。
七. 依赖属性的继承
依赖属性继承的最初意愿是父元素的相关设置会自动传递给所有层次的子元素 ,即元素可以从其在树中的父级继承依赖项属性的值。这个我们在编程当中接触得比较多,如当我们修改窗体父容器控件的字体设置时,所有级别的子控件都将自动使用该字体设置 (前提是该子控件未做自定义设置),如下面的代码:
<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的截断而没有任何改变,将保留其默认值。所以大家在使用的时候要特别注意这些问题。
前面我们看了依赖属性的继承,当我们自定义的依赖属性,应该如何处理继承的关系呢? 请看下面的代码(注释很详细,我就不再费口水了):
public class MyCustomButton : Button
{
static MyCustomButton()
{
//通过MyStackPanel依赖属性MinDateProperty的AddOwner方式实现继承,注意FrameworkPropertyMetadataOptions的值为Inherits
MinDateProperty = MyStackPanel.MinDateProperty.AddOwner(typeof(MyCustomButton),
new FrameworkPropertyMetadata(DateTime.MinValue, FrameworkPropertyMetadataOptions.Inherits));
}
public static readonly DependencyProperty MinDateProperty;
public DateTime MinDate
{
get { return (DateTime)GetValue(MinDateProperty); }
set { SetValue(MinDateProperty, value); }
}
}
public class MyStackPanel : StackPanel
{
static MyStackPanel()
{
//我们在MyStackPanel里面注册了MinDate,注意FrameworkPropertyMetadataOptions的值为Inherits
MinDateProperty = DependencyProperty.Register("MinDate",
typeof(DateTime),
typeof(MyStackPanel),
new FrameworkPropertyMetadata(DateTime.MinValue, FrameworkPropertyMetadataOptions.Inherits));
}
public static readonly DependencyProperty MinDateProperty;
public DateTime MinDate
{
get { return (DateTime)GetValue(MinDateProperty); }
set { SetValue(MinDateProperty, value); }
}
}
那么就可以在XAML中进行使用了:
<Window x:Class="Custom_Inherited_DPs.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Custom_Inherited_DPs"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
WindowStartupLocation="CenterScreen"
Title="使用自动以依赖属性继承" Height="300" Width="300">
<Grid>
<local:MyStackPanel x:Name="myStackPanel" MinDate="{x:Static sys:DateTime.Now}">
<!-- myStackPanel的依赖属性 -->
<ContentPresenter Content="{Binding Path=MinDate, ElementName=myStackPanel}"/>
<!-- 继承自myStackPanel的依赖属性 -->
<local:MyCustomButton
Content="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=MinDate}"
Height="20"/>
</local:MyStackPanel>
</Grid>
</Window>
最后的效果如下: