前面废话了这么久,到现在才真正进入今天的主题,对此感到非常抱歉,如果各位不喜欢,可以直接跳到这里阅读。大家都知道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,StatusbarStatusBar> 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}"> <ContentPresenter Content="{Binding Path=MinDate, ElementName=myStackPanel}"/> <local:MyCustomButton Content="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=MinDate}" Height="20"/> local:MyStackPanel> Grid> Window>
最后的效果如下:
我们以前在对简单属性的封装中,经常会对那些希望暴露给外界只读操作的字段封装成只读属性,同样在WPF中也提供了只读属性的概念,如一些WPF控件的依赖属性是只读的,它们经常用于报告控件的状态和信息,像IsMouseOver等属性, 那么在这个时候对它赋值就没有意义了。 或许你也会有这样的疑问:为什么不使用一般的.Net属性提供出来呢?一般的属性也可以绑定到元素上呀?这个是由于有些地方必须要用到只读依赖属性,比如Trigger等,同时也因为内部可能有多个提供者修改其值,所以用.Net属性就不能完成天之大任了。
那么一个只读依赖属性怎么创建呢?其实创建一个只读的依赖属性和创建一个一般的依赖属性大同小异(研究源码你会发现,其内部都是调用的同一个Register方法)。仅仅是用DependencyProperty.RegisterReadonly替换了DependencyProperty.DependencyProperty而已。和前面的普通依赖属性一样,它将返回一个DependencyPropertyKey。该键值在类的内部暴露一个赋值的入口,同时只提供一个GetValue给外部,这样便可以像一般属性一样使用了,只是不能在外部设置它的值罢了。
下面我们就用一个简单的例子来概括一下:
public partial class Window1 : Window { public Window1() { InitializeComponent(); //内部用SetValue的方式来设置值 DispatcherTimer timer = new DispatcherTimer(TimeSpan.FromSeconds(1), DispatcherPriority.Normal, (object sender, EventArgs e)=> { int newValue = Counter == int.MaxValue ? 0 : Counter + 1; SetValue(counterKey, newValue); }, Dispatcher); } //属性包装器,只提供GetValue,这里你也可以设置一个private的SetValue进行限制 public int Counter { get { return (int)GetValue(counterKey.DependencyProperty); } } //用RegisterReadOnly来代替Register来注册一个只读的依赖属性 private static readonly DependencyPropertyKey counterKey = DependencyProperty.RegisterReadOnly("Counter", typeof(int), typeof(Window1), new PropertyMetadata(0)); }
XAML中代码:
<Window x:Name="winThis" x:Class="WpfApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Read-Only Dependency Property" Height="300" Width="300"> <Grid> <Viewbox> <TextBlock Text="{Binding ElementName=winThis, Path=Counter}" /> Viewbox> Grid> Window>
效果如下图所示:
前面我们讲了依赖属性。现在我们再继续探讨另外一种特殊的Dependency属性——附加属性。附加属性是一种特殊的依赖属性。他允许给一个对象添加一个值,而该对象可能对此值一无所知。
最好的例子就是布局面板。每一个布局面板都需要自己特有的方式来组织它的子元素。如Canvas需要Top和left来布局,DockPanel需要Dock来布局。当然你也可以写自己的布局面板(在上一篇文章中我们对布局进行了比较细致的探讨,如果有不清楚的朋友也可以再回顾一下)。
下面代码中的Button 就是用了Canvas的Canvas.Top和Canvas.Left="20" 来进行布局定位,那么这两个就是传说中的附加属性。
<Canvas> <Button Canvas.Top="20" Canvas.Left="20" Content="Knights Warrior!"/> Canvas>
在最前面的小节中,我们是使用DependencyProperty.Register来注册一个依赖属性,同时依赖属性本身也对外提供了 DependencyProperty.RegisterAttached方法来注册附加属性。这个RegisterAttached的参数和 Register是完全一致的,那么Attached(附加)这个概念又从何而来呢?
其实我们使用依赖属性,一直在Attached(附加)。我们注册(构造)一个依赖属性,然后在DependencyObject中通过 GetValue和SetValue来操作这个依赖属性,也就是把这个依赖属性通过这样的方法关联到了这个DependencyObject上,只不过是通过封装CLR属性来达到的。那么RegisterAttached又是怎样的呢?
下面我们来看一个最简单的应用:首先我们注册(构造)一个附加属性
public class AttachedPropertyChildAdder { //通过使用RegisterAttached来注册一个附加属性 public static readonly DependencyProperty IsAttachedProperty = DependencyProperty.RegisterAttached("IsAttached", typeof(bool), typeof(AttachedPropertyChildAdder), new FrameworkPropertyMetadata((bool)false)); //通过静态方法的形式暴露读的操作 public static bool GetIsAttached(DependencyObject dpo) { return (bool)dpo.GetValue(IsAttachedProperty); } //通过静态方法的形式暴露写的操作 public static void SetIsAttached(DependencyObject dpo, bool value) { dpo.SetValue(IsAttachedProperty, value); } }
在XAML中就可以使用刚才注册(构造)的附加属性了:
在上面的例子中,AttachedPropertyChildAdder 中并没有对IsAttached采用CLR属性形式进行封装,而是使用了静态SetIsAttached方法和GetIsAttached方法来存取IsAttached值,当然如果你了解它内部原理,你就会看到实际上还是调用的SetValue与GetValue来进行操作(只不过拥有者不同而已)。这里我们不继续深入下去,详细在后面的内容会揭开谜底。
在很多时候,由于我们的业务逻辑和UI操作比较复杂,所以一个庞大的页面会进行很多诸如动画、3D、多模板及样式的操作,这个时候页面的值已经都被改变了,如果我们想让它返回默认值,可以用ClearValue 来清除本地值,但是遗憾的是,很多时候由于WPF依赖属性本身的设计,它往往会不尽如人意(详细就是依赖属性的优先级以及依赖属性EffectiveValueEntry 的影响)。ClearValue 方法为在元素上设置的依赖项属性中清除任何本地应用的值提供了一个接口。但是,调用 ClearValue 并不能保证注册属性时在元数据中指定的默认值就是新的有效值。值优先级中的所有其他参与者仍然有效。只有在本地设置的值才会从优先级序列中移除。例如,如果您对同时也由主题样式设置的属性调用 ClearValue,主题值将作为新值而不是基于元数据的默认值进行应用。如果您希望取消过程中的所有属性值,而将值设置为注册的元数据默认值,则可以通过查询依赖项属性的元数据来最终获得默认值,然后使用该默认值在本地设置属性并调用 SetValue来实现,这里我们得感得PropertyMetadata类为我们提供了诸如DefaultValue这样的外部可访问的属性。
上面讲了这么多,现在我们就简单用一个例子来说明上面的原理(例子很直观,相信大家能很容易看懂)
XAML中代码如下:
<Window x:Class="WpfApplication1.DPClearValue" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="400" Width="400"> <StackPanel Name="root"> <StackPanel.Resources> <Style TargetType="Button"> <Setter Property="Height" Value="20"/> <Setter Property="Width" Value="250"/> <Setter Property="HorizontalAlignment" Value="Left"/> Style> <Style TargetType="Ellipse"> <Setter Property="Height" Value="50"/> <Setter Property="Width" Value="50"/> <Setter Property="Fill" Value="LightBlue"/> Style> <Style TargetType="Rectangle"> <Setter Property="Height" Value="50"/> <Setter Property="Width" Value="50"/> <Setter Property="Fill" Value="MediumBlue"/> Style> <Style x:Key="ShapeStyle" TargetType="Shape"> <Setter Property="Fill" Value="Azure"/> Style> StackPanel.Resources> <DockPanel Name="myDockPanel"> <Ellipse Height="100" Width="100" Style="{StaticResource ShapeStyle}"/> <Rectangle Height="100" Width="100" Style="{StaticResource ShapeStyle}" /> DockPanel> <Button Name="RedButton" Click="MakeEverythingAzure" Height="39" Width="193">改变所有的值Button> <Button Name="ClearButton" Click="RestoreDefaultProperties" Height="34" Width="192"> 清除本地值Button> StackPanel> Window>
后台代码:
public partial class DPClearValue { //清除本地值,还原到默认值 void RestoreDefaultProperties(object sender, RoutedEventArgs e) { UIElementCollection uic = myDockPanel.Children; foreach (Shape uie in uic) { LocalValueEnumerator locallySetProperties = uie.GetLocalValueEnumerator(); while (locallySetProperties.MoveNext()) { DependencyProperty propertyToClear = (DependencyProperty)locallySetProperties.Current.Property; if (!propertyToClear.ReadOnly) { uie.ClearValue(propertyToClear); } } } } //修改本地值 void MakeEverythingAzure(object sender, RoutedEventArgs e) { UIElementCollection uic = myDockPanel.Children; foreach (Shape uie in uic) { uie.Fill = new SolidColorBrush(Colors.Azure); } } }
当按下”改变所有的值“按钮的时候,就会把之前的值都进行修改了,这个时候按下”清除本地值“就会使原来的所有默认值生效。
前面我们看到一个依赖属性的注册最全的形式是下面这样子的:
public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback);
第一个参数是该依赖属性的名字,第二个参数是依赖属性的类型,第三个参数是该依赖属性的所有者的类型,第五个参数就是一个验证值的回调委托,那么最使我们感兴趣的还是这个可爱的 PropertyMetadata ,也就是我们接下来要讲的元数据。 提到WPF属性元数据,大家可能第一想到的是刚才的PropertyMetadata,那么这个类到底是怎样的呢?我们应该怎样使用它呢?首先我们看它的构造函数(我们选参数最多的来讲):
public PropertyMetadata(object defaultValue, PropertyChangedCallback propertyChangedCallback, CoerceValueCallback coerceValueCallback);
其中的第一个参数是默认值,最后两个分别是PropertyChanged(变化通知)以及Coerce(强制)的两个委托变量,我们在实例化的时候,只需要把这两个委托变量关联到具体的方法上即可。
事实上,除了PropertyMetadata以外,常见的还有FrameworkPropertyMetadata,UIPropertyMetadata。他们的继承关系是F->U->P。其中以FrameworkPropertyMetadata参数最多,亦最为复杂。
FrameworkPropertyMetadata的构造函数提供了很多重载,我们挑选最为复杂的重载来看它到底有哪些参数以及提供了哪些功能:
public FrameworkPropertyMetadata(object defaultValue, FrameworkPropertyMetadataOptions flags, PropertyChangedCallback propertyChangedCallback, CoerceValueCallback coerceValueCallback, bool isAnimationProhibited, UpdateSourceTrigger defaultUpdateSourceTrigger);
其中第一个参数是默认值,最后两个参数分别是是否允许动画,以及绑定时更新的策略(在Binding当中相信大家并不陌生),这个不详细解释了。重点看一下里第三、四两个参数,两个 CallBack的委托。结合前面Register的时候提到的ValidateValueCallback共组成三大”金刚“,这三个Callback分别代表Validate(验证),PropertyChanged(变化通知)以及Coerce(强制)。当然,作为 Metadata,FrameworkPropertyMetadata只是储存了该依赖属性的策略信息,WPF属性系统会根据这些信息来提供功能并在适当的时机回调传入的delegate,所以最重要的还是我们定义的这些方法,通过他们传入委托才能起到真正的作用。
上面讲了元数据暴露给我们的构造函数,其实在其内部还提供了两个方法,这个在做自定义控件的时候,也很值得注意:
protected virtual void Merge(PropertyMetadata baseMetadata, DependencyProperty dp) { // 实现元数据继承之间的合并 } protected virtual void OnApply(DependencyProperty dependencyProperty, Type targetType) { // 当元数据被这个属性应用,OnApply就会被触发,在此时元数据也将被密封起来。 }
前面讲了这么多,那么我们现在就来看看依赖属性回调、验证及强制值到底是怎么使用的呢?大家千万要坚持住,后面内容更加精彩!
我们通过下面的这幅图,简单介绍一下WPF属性系统对依赖属性操作的基本步骤:
前面我们讲了基本的流程,下面我们就用一个小的例子来进行说明:
namespace SampleProcess_DPs { class Program { static void Main(string[] args) { SimpleDPClass sDPClass = new SimpleDPClass(); sDPClass.SimpleDP = 8; Console.ReadLine(); } } public class SimpleDPClass : DependencyObject { public static readonly DependencyProperty SimpleDPProperty = DependencyProperty.Register("SimpleDP", typeof(double), typeof(SimpleDPClass), new FrameworkPropertyMetadata((double)0.0, FrameworkPropertyMetadataOptions.None, new PropertyChangedCallback(OnValueChanged), new CoerceValueCallback(CoerceValue)), new ValidateValueCallback(IsValidValue)); public double SimpleDP { get { return (double)GetValue(SimpleDPProperty); } set { SetValue(SimpleDPProperty, value); } } private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { Console.WriteLine("当值改变时,我们可以做的一些操作,具体可以在这里定义: {0}", e.NewValue); } private static object CoerceValue(DependencyObject d, object value) { Console.WriteLine("对值进行限定,强制值: {0}", value); return value; } private static bool IsValidValue(object value) { Console.WriteLine("验证值是否通过,返回bool值,如果返回True表示严重通过,否则会以异常的形式暴露: {0}", value); return true; } } }
结果如下:
当SimpleDP属性变化之后,PropertyChangeCallback就会被调用。可以看到结果并没有完全按照我们先前的流程先Coerce后Validate的顺序执行,有可能是WPF内部做了什么特殊处理,当属性被修改时,首先会调用Validate来判断传入的value是否有效,如果无效就不继续后续的操作,这样可以更好的优化性能。从上面的结果上看出,CoerceValue后面并没有立即ValidateValue,而是直接调用了PropertyChanged。这是因为前面已经验证过了value,如果在Coerce中没有改变value,那么就不用再验证了。如果在 Coerce中改变了value,那么这里还会再次调用ValidateValue操作,和前面的流程图执行的顺序一样,在最后我们会调用ValidateValue来进行最后的验证,这就保证最后的结果是我们希望的那样了(正如打游戏一样,打了小怪,在最后过总关的时候还是需要打大怪才能闯关的)。
上面简单介绍了处理流程,下面我们就以一个案例来具体看一看上面的流程到底有没有出入,这个例子改编于Sacha Barber 的Dependency Properties代码示例,我相信通过这段代码你会对这个上面讲的概念有更清晰地认识。
UI很简单,黄色部分显示当前值,我们在初始化的时候把它设置为100,然后它的最小值和最大值分别设置为0和500,按钮”设置为-100“企图把当前值设为-100,按钮”设置为1000“试图把当前值设为1000。具体大家看代码(我都写了注释,很容易理解的).
依赖属性代码文件如下:
namespace Callback_Validation_DPs { public class Gauge : Control { public Gauge() : base() { } //注册CurrentReading依赖属性,并添加PropertyChanged、CoerceValue、ValidateValue的回调委托 public static readonly DependencyProperty CurrentReadingProperty = DependencyProperty.Register( "CurrentReading", typeof(double), typeof(Gauge), new FrameworkPropertyMetadata( Double.NaN, FrameworkPropertyMetadataOptions.None, new PropertyChangedCallback(OnCurrentReadingChanged), new CoerceValueCallback(CoerceCurrentReading) ), new ValidateValueCallback(IsValidReading) ); //属性包装器,通过它来暴露CurrentReading的值 public double CurrentReading { get { return (double)GetValue(CurrentReadingProperty); } set { SetValue(CurrentReadingProperty, value); } } //注册MinReading依赖属性,并添加PropertyChanged、CoerceValue、ValidateValue的回调委托 public static readonly DependencyProperty MinReadingProperty = DependencyProperty.Register( "MinReading", typeof(double), typeof(Gauge), new FrameworkPropertyMetadata( double.NaN, FrameworkPropertyMetadataOptions.None, new PropertyChangedCallback(OnMinReadingChanged), new CoerceValueCallback(CoerceMinReading) ), new ValidateValueCallback(IsValidReading)); //属性包装器,通过它来暴露MinReading的值 public double MinReading { get { return (double)GetValue(MinReadingProperty); } set { SetValue(MinReadingProperty, value); } } //注册MaxReading依赖属性,并添加PropertyChanged、CoerceValue、ValidateValue的回调委托 public static readonly DependencyProperty MaxReadingProperty = DependencyProperty.Register( "MaxReading", typeof(double), typeof(Gauge), new FrameworkPropertyMetadata( double.NaN, FrameworkPropertyMetadataOptions.None, new PropertyChangedCallback(OnMaxReadingChanged), new CoerceValueCallback(CoerceMaxReading) ), new ValidateValueCallback(IsValidReading) ); //属性包装器,通过它来暴露MaxReading的值 public double MaxReading { get { return (double)GetValue(MaxReadingProperty); } set { SetValue(MaxReadingProperty, value); } } //在CoerceCurrentReading加入强制判断赋值 private static object CoerceCurrentReading(DependencyObject d, object value) { Gauge g = (Gauge)d; double current = (double)value; if (current < g.MinReading) current = g.MinReading; if (current > g.MaxReading) current = g.MaxReading; return current; } //当CurrentReading值改变的时候,调用MinReading和MaxReading的CoerceValue回调委托 private static void OnCurrentReadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { d.CoerceValue(MinReadingProperty); d.CoerceValue(MaxReadingProperty); } //当OnMinReading值改变的时候,调用CurrentReading和MaxReading的CoerceValue回调委托 private static void OnMinReadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { d.CoerceValue(MaxReadingProperty); d.CoerceValue(CurrentReadingProperty); } //在CoerceMinReading加入强制判断赋值 private static object CoerceMinReading(DependencyObject d, object value) { Gauge g = (Gauge)d; double min = (double)value; if (min > g.MaxReading) min = g.MaxReading; return min; } //在CoerceMaxReading加入强制判断赋值 private static object CoerceMaxReading(DependencyObject d, object value) { Gauge g = (Gauge)d; double max = (double)value; if (max < g.MinReading) max = g.MinReading; return max; } //当MaxReading值改变的时候,调用MinReading和CurrentReading的CoerceValue回调委托 private static void OnMaxReadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { d.CoerceValue(MinReadingProperty); d.CoerceValue(CurrentReadingProperty); } //验证value是否有效,如果返回True表示验证通过,否则会提示异常 public static bool IsValidReading(object value) { Double v = (Double)value; return (!v.Equals(Double.NegativeInfinity) && !v.Equals(Double.PositiveInfinity)); } } }
XAML代码如下:
<Window x:Class="Callback_Validation_DPs.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Callback_Validation_DPs" WindowStartupLocation="CenterScreen" Title="Callback_Validation_DPs" Height="400" Width="400"> <StackPanel Orientation="Vertical"> <local:Gauge x:Name="gauge1" MaxReading="100" MinReading="0" /> <Label Content="可以设置最小值为0和最小大值为500" Height="30"/> <StackPanel Orientation="Horizontal" Height="60"> <Label Content="当前值为 : "/> <Label Background="Yellow" BorderBrush="Black" BorderThickness="1" IsEnabled="False" Content="{Binding ElementName=gauge1, Path=CurrentReading}" Height="25" VerticalAlignment="Top" /> StackPanel> <Button x:Name="btnSetBelowMin" Content="设置为 -100" Click="btnSetBelowMin_Click"/> <Button x:Name="btnSetAboveMax" Content="设置为 1000" Click="btnSetAboveMax_Click"/> StackPanel> Window>
XAML的后台代码如下:
public partial class Window1 : Window { public Window1() { InitializeComponent(); //设置CurrentReading的值,这个时候会触发哪些变化?调试代码吧! gauge1.CurrentReading = 100; } private void btnSetBelowMin_Click(object sender, RoutedEventArgs e) { //设置CurrentReading的值,这个时候会触发哪些变化?调试代码吧! gauge1.CurrentReading = -100; } private void btnSetAboveMax_Click(object sender, RoutedEventArgs e) { //设置CurrentReading的值,这个时候会触发哪些变化?调试代码吧! gauge1.CurrentReading = 10000; } }
在上面的例子中,一共有三个依赖属性相互作用——CurrentReading、MinReading和MaxReading,这些属性相互作用,但它们的规则是MinReading≤CurrentReading≤MaxReading。根据这个规则,当其中一个依赖属性变化时,另外两个依赖属性必须进行适当的调整,这里我们要用到的就是CoerceValue这个回调委托,那么实现起来也非常的简单,注册MaxReading的时候加入CoerceValueCallback,在CoerceMaxReading函数中做处理:如果Maximum的值小于MinReading,则使MaxReading值等于MinReading;同理在CurrentReading中也加入了CoerceValueCallback进行相应的强制处理。然后在MinReading的ChangedValueCallback被调用的时候,调用CurrentReading和MaxReading的CoerceValue回调委托,这样就可以达到相互作用的依赖属性一变应万变的”千机变“。
换句话说,当相互作用的几个依赖属性其中一个发生变化时,在它的PropertyChangeCallback中调用受它影响的依赖属性的CoerceValue,这样才能保证相互作用关系的正确性。 前面也提高ValidateValue主要是验证该数据的有效性,最设置了值以后都会调用它来进行验证,如果验证不成功,则抛出异常。
如果想监听依赖属性的改变,可以用两种方法实现,在很多时候,我们两种方法都会用到:
用DependencyPropertyDescriptor 比较简便,在代码里面写起来也比较便捷;
用OverrideMetadata的方式主要在自定义控件以及处理一些类间关系的时候;
第一种方法:派生自这个类,然后定义它的属性,重写属性的原数据并传递一个PropertyChangedCallBack参数即可,如下代码:
public class MyTextBox : TextBox { public MyTextBox(): base() { } static MyTextBox() { //第一种方法,通过OverrideMetadata FlowDirectionProperty.OverrideMetadata(typeof(MyTextBox), new FrameworkPropertyMetadata(new PropertyChangedCallback(FlowDirectionPropertyChanged))); } private static void FlowDirectionPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { ((MyTextBox)sender).FontWeight = (((MyTextBox)sender).FlowDirection == FlowDirection.LeftToRight) ? FontWeights.Bold : FontWeights.Normal; } }
第二种方法:这个方法更加简单,获取DependencyPropertyDescriptor并调用AddValueChange()为其挂接一个回调函数,如下代码:
private void Window1_Loaded(object sender, RoutedEventArgs e) { //第二种方法,通过OverrideMetadata DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(TextBox.TextProperty, typeof(TextBox)); descriptor.AddValueChanged(tbxEditMe, tbxEditMe_TextChanged); } private void tbxEditMe_TextChanged(object sender, EventArgs e) { MessageBox.Show("", "Changed"); }
代码段可以说是一个非常普遍且实用的功能,我们可以利用它来简化和规范我们的代码。在项目当中我们通常会定义大量的代码段,如怎样写一个类、怎样定义一个方法、公用代码库等等都可以定义成代码段,今天不着重讲这一块,下面就来看看在默认的VS中有哪些代码段
上面看到的是visual basic的代码段(一不小心截图截错了,呵呵),但不幸的是针对C#的代码段却很少。不过没关系,既然默认没有提供那么多代码段,我们可以自己动手嘛,正所谓自己动手丰衣足食嘛!相信大家都有自定义代码段的经历,同时在网上也有很多好的代码段下载,我用得最多的是DrWPFSnippets,由于接触WPF和Silverlight是在07年,所以当时自己也定义过一些代码段,由于自己主要精力还是在技术架构、ASP.NET、WCF、OO等方面,所以在08年以后就开始使用网上的代码段资源了,当然由于之前项目也自己写了一些代码段,所以很多时候都是混合起来使用,大家可以到http://drwpf.com/blog/2010/04/30/updated-code-snippets/去下载,这个代码段包最早从2007年11月就提供下载了,在今年四月份进行了升级,同时支持VS2005/VS2008/VS2010,所以大家可以下载下来体验一下,很不错的哦!下载以后点击DrWPFSnippets.vsi就会自动安装,安装完成以后,你会看到如下界面,图中的Shortcut就是你要按的快捷键,不过生成的代码会出现有些帮助类找不到的情况,如RoutedEvent会生成一个RoutedEventHelper的类,这个是没有关系的,你到网上一搜就可以把这个类加入到你的代码当中。那么运行就十分正常了。在安装的时候提醒一下,最好一次安装成功,否则你会为众多的弹窗口感到十分厌恶,呵呵!
那么现在你就可以在项目当中使用了,如按下re+TAB键两次,你就会看到如下界面,然后选择你的选项即可生成需要的代码(这里re就是Routed event的简写)。
如下是生成的代码,你可以直接使用或者经过适当修改使用。
#region Swindle ////// Swindle Routed Event /// public static readonly RoutedEvent SwindleEvent = EventManager.RegisterRoutedEvent("Swindle", RoutingStrategy.Bubble, typeof(TrioEventHandler), typeof(Window1)); ////// Occurs when ... /// public event TrioEventHandler Swindle { add { AddHandler(SwindleEvent, value); } remove { RemoveHandler(SwindleEvent, value); } } ////// A helper method to raise the Swindle event. /// /// /// /// protected TrioEventArgs RaiseSwindleEvent(bool arg, bool arg2, bool arg3) { return RaiseSwindleEvent(this, arg, arg2, arg3); } ////// A static helper method to raise the Swindle event on a target element. /// /// UIElement or ContentElement on which to raise the event /// /// /// internal static TrioEventArgs RaiseSwindleEvent(DependencyObject target, bool arg, bool arg2, bool arg3) { if (target == null) return null; TrioEventArgs args = new TrioEventArgs(arg, arg2, arg3); args.RoutedEvent = SwindleEvent; RoutedEventHelper.RaiseEvent(target, args); return args; } #endregion
古人有”不入虎穴焉得虎子“的名句,我们今天也试着入一入虎穴,探探依赖属性里面到底藏着什么不可告人的秘密,在往下讲之前,我们先来看一下DependencyObject 、DependencyProperty 以及PropertyMetadata到底包含哪些功能,如下面三幅图
通过前面三幅图,我们就可以了解WPF依赖属性系统的大体结构以及主要功能,再者通过前面我们对它的使用,对它的内部实现也有一个相对比较清晰的认识,那么接下来要做的就是:借助Reflector+VS调试内部代码功能一起来研究其内部的实现原理。 本来想详细写清楚开发的过程,但是有点多,所以我打算直接讲这几个类。大家也可以通过这个思路来试一试,同时还可以参考Mono的源码、WF的依赖属性源码等。这里要推荐的是周永恒的博客,此人对技术的理解很是透彻,博文虽少,但每篇都堪称经典,所以他的文章,我都通读三遍。虽然大多概念都懂,并且读到深处也能产生共鸣,其最主要目的还是学习他这种”阐述问题的思路“,后来也和此人MSN聊过几次。所以这个依赖属性的框架在某些程度上也借鉴了他的一些写法。
有了前面的思路,首先定义DependencyProperty这个类,它里面存储前面我们提到希望抽出来的字段。DependencyProperty内部维护了一个全局的Map用来储存所有的DependencyProperty,对外暴露了一个Register方法用来注册新的DependencyProperty。当然,为了保证在Map中键值唯一,注册时需要根据传入的名字和注册类的的 HashCode取异或来生成Key。 所以我们就可以完成DependencyProperty类了,代码如下,介绍详见代码注释。:
public sealed class DependencyProperty { //全局的IDictionary用来储存所有的DependencyProperty internal static IDictionary<int, DependencyProperty> properties = new Dictionary<int, DependencyProperty>(); //存储元数据的集合 private List<PropertyMetadata> _metadataMap = new List<PropertyMetadata>(); private static int globalIndex = 0; private PropertyMetadata def_metadata; private bool attached; private string name; private int _index; private Type owner_type; private Type property_type; private Type validator_type; // 构造函数 private DependencyProperty() { } //构造函数私有,保证外界不会对它进行实例化 private DependencyProperty(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata) { this.name = name; property_type = propertyType; owner_type = ownerType; def_metadata = defaultMetadata; } // 常用属性 public PropertyMetadata DefaultMetadata { get { return def_metadata; } } public bool IsAttached { get { return attached; } } public int Index { get { return _index; } set { _index = value; } } public string Name { get { return name; } } public Type OwnerType { get { return owner_type; } } public Type PropertyType { get { return property_type; } } public Type ValidatorType { get { return validator_type; } } public override int GetHashCode() { return name.GetHashCode() ^ owner_type.GetHashCode(); } //注册依赖属性 public static DependencyProperty Register(string name, Type propertyType, Type ownerType) { return Register(name, propertyType, ownerType, new PropertyMetadata()); } //注册的公用方法,把这个依赖属性加入到IDictionary的键值集合中,Key为name和owner_type的GetHashCode取异,Value就是我们注册的DependencyProperty public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata) { DependencyProperty property = new DependencyProperty(name, propertyType, ownerType, defaultMetadata); globalIndex++; property.Index = globalIndex; if (properties.ContainsKey(property.GetHashCode())) { throw new InvalidOperationException("A property with the same name already exists"); } //把刚实例化的DependencyProperty添加到这个全局的IDictionary种 properties.Add(property.GetHashCode(), property); return property; } //注册只读依赖属性 public static DependencyProperty RegisterReadOnly(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata) { DependencyProperty property = Register(name, propertyType, ownerType, typeMetadata); return property; } //注册附加依赖属性 public static DependencyProperty RegisterAttached(string name, Type propertyType, Type ownerType) { return RegisterAttached(name, propertyType, ownerType, new PropertyMetadata(), null); } public static DependencyProperty RegisterAttached(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata) { return RegisterAttached(name, propertyType, ownerType, defaultMetadata, null); } public static DependencyProperty RegisterAttached(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, Type validatorType) { DependencyProperty property = Register(name, propertyType, ownerType, defaultMetadata); property.attached = true; property.validator_type = validatorType; return property; } //子类继承重写以及其他需要重写Metadata的时候使用 public void OverrideMetadata(Type forType, PropertyMetadata metadata) { metadata.Type = forType; _metadataMap.Add(metadata); } //获取元数据信息 public PropertyMetadata GetMetadata(Type type) { PropertyMetadata medatata = _metadataMap.FirstOrDefault((i) => i.Type == type) ?? _metadataMap.FirstOrDefault((i) => type.IsSubclassOf(i.Type)); if (medatata == null) { medatata = def_metadata; } return medatata; } }
有了DependencyProperty ,那么接下来就需要定义DependencyObject 来使用这个DependencyProperty 。首先使用DependencyProperty .Register方法注册了一个新的DependencyProperty ,然后提供了GetValue和SetValue两个方法来操作刚刚构造的DependencyProperty 。这个时候我们看到一个简单的依赖属性系统已初见端倪了,详见代码注释。
namespace Realize_DPs { public abstract class DependencyObject : IDisposable { //添加一个List来记录修改信息 private List<EffectiveValueEntry> _effectiveValues = new List<EffectiveValueEntry>(); //属性包装器,通过它来访问依赖属性 public object GetValue(DependencyProperty dp) { //首先通过判断是否改动过,以此来决定是读元数据的默认值还是改动了的值 EffectiveValueEntry effectiveValue = _effectiveValues.FirstOrDefault((i) => i.PropertyIndex == dp.Index); if (effectiveValue.PropertyIndex != 0) { return effectiveValue.Value; } else { PropertyMetadata metadata; metadata = DependencyProperty.properties[dp.GetHashCode()].DefaultMetadata; return metadata.DefaultValue; } } //属性包装器,通过它来设置依赖属性的值 public void SetValue(DependencyProperty dp, object value) { //首先通过判断是否改动过,以及改动过,则继续对改动过的元素赋值,否则对_effectiveValues增加元素 EffectiveValueEntry effectiveValue = _effectiveValues.FirstOrDefault((i) => i.PropertyIndex == dp.Index); if (effectiveValue.PropertyIndex != 0) { effectiveValue.Value = value; } else { effectiveValue = new EffectiveValueEntry() { PropertyIndex = dp.Index, Value = value }; _effectiveValues.Add(effectiveValue); } } public void Dispose() { //暂时还没有处理 } } internal struct EffectiveValueEntry { internal int PropertyIndex { get; set; } internal object Value { get; set; } } }
前面有了DependencyProperty 和DependencyObject 类,那我们现在来新建一个比较重要的类 PropertyMetadata ,它的作用和功能很强大,我们这里只是简单进行了构建,如下代码:
namespace Realize_DPs { public delegate void SetValueOverride(DependencyObject d, object value); public delegate object GetValueOverride(DependencyObject d); public class PropertyMetadata { private object default_value; private DependencyPropertyOptions options = DependencyPropertyOptions.Default; private bool _sealed = false; private SetValueOverride set_value; private GetValueOverride get_value; private Attribute[] attributes; private Type type; // 构造函数重载 public PropertyMetadata() { } public PropertyMetadata(object defaultValue) { default_value = defaultValue; } public PropertyMetadata(DependencyPropertyOptions options) { this.options = options; } public PropertyMetadata(params Attribute[] attributes) { this.attributes = attributes; } public PropertyMetadata(object defaultValue, params Attribute[] attributes) { default_value = defaultValue; this.attributes = attributes; } public PropertyMetadata(object defaultValue, DependencyPropertyOptions options) { default_value = defaultValue; this.options = options; } public PropertyMetadata(DependencyPropertyOptions options, params Attribute[] attributes) { this.options = options; this.attributes = attributes; } public PropertyMetadata(object defaultValue, DependencyPropertyOptions options, params Attribute[] attributes) { this.options = options; default_value = defaultValue; this.attributes = attributes; } public PropertyMetadata(object defaultValue, DependencyPropertyOptions options, GetValueOverride getValueOverride, SetValueOverride setValueOverride) { this.options = options; default_value = defaultValue; set_value = setValueOverride; get_value = getValueOverride; } public PropertyMetadata(object defaultValue, DependencyPropertyOptions options, GetValueOverride getValueOverride, SetValueOverride setValueOverride, params Attribute[] attributes) { this.options = options; default_value = defaultValue; set_value = setValueOverride; get_value = getValueOverride; this.attributes = attributes; } // 常用属性 public object DefaultValue { get { return default_value; } set { default_value = value; } } public GetValueOverride GetValueOverride { get { return get_value; } set { get_value = value; } } public bool IsMetaProperty { get { return (options & DependencyPropertyOptions.Metadata) == DependencyPropertyOptions.Metadata; } } public bool IsNonSerialized { get { return (options & DependencyPropertyOptions.NonSerialized) == DependencyPropertyOptions.NonSerialized; } } public bool IsReadOnly { get { return (options & DependencyPropertyOptions.Readonly) == DependencyPropertyOptions.Readonly; } } protected bool IsSealed { get { return _sealed; } } public DependencyPropertyOptions Options { get { return options; } set { options = value; } } public SetValueOverride SetValueOverride { get { return set_value; } set { set_value = value; } } public Type Type { get { return type; } set { type = value; } } protected virtual void Merge(PropertyMetadata baseMetadata, DependencyProperty dp) { // 实现元数据继承之间的合并 } protected virtual void OnApply(DependencyProperty dependencyProperty, Type targetType) { // 当元数据被这个属性应用,OnApply就会被触发,在此时元数据也将被密封起来。 } } }
前面我们实现了一个简单的依赖属性系统,现在就得先测试一下其功能,代码如下:
class Program : DependencyObject { public static readonly DependencyProperty CounterProperty; static Program() { //注册依赖属性Counter CounterProperty = DependencyProperty.Register("Counter", typeof(double), typeof(Program), new PropertyMetadata(8.0)); } //属性包装器,暴露读写接口 public double Counter { get { return (double)GetValue(CounterProperty); } set {SetValue(CounterProperty, value); } } static void Main(string[] args) { Program pro = new Program(); Console.WriteLine("读取元数据设置的默认值: "+pro.Counter.ToString()); Program pro2 = new Program(); pro2.Counter = 22.5; Console.WriteLine("通过SetValue设置改变了的值: " + pro2.Counter.ToString()); Console.ReadLine(); } }
那么测试结果为:
利用VS自带的类图,可以看到刚才我们实现的这个依赖属性类及类之间的关系图:
由于上面的代码在很多方面都很粗糙,所以希望大家能下载代码进行改造,同时也希望给出反馈。
这篇文章洋洋洒洒写了很多,我们现在简单回顾一下:在开篇之前我们会先介绍比本篇更重要的一些东西,然后插播了一段”云计算之旅“的广告(广告费很昂贵 ,所以格外小心),作为最近几个月执着研究的东西,终于可以在下周和大家见面了,所以心中甚是喜悦。在前面的两个内容之后我们正式进入本篇的主题——依赖属性。依赖属性是WPF的核心概念,所以我们花费了大量的时间和篇幅进行论述,首先从依赖属性基本介绍讲起,然后过渡到依赖属性的优先级、附加属性、只读依赖属性、依赖属性元数据、依赖属性回调、验证及强制值、依赖属性监听、代码段(自动生成) 等相关知识,最后我们模拟了一个WPF依赖属性的实现,对内部实现原理进行了一些研究。在接下来的三篇”剖析路由事件”、”剖析命令”、”剖析绑定”也会采用这篇文章的风格,希望能尽量说透,如果有误之处还希望各位能够批评指正!
在文章的最后,我们提供代码的下载,这几篇文章最重要的就是下载代码来细细研究,代码里面也添加了比较详细的注释,如果大家有什么问题,也可以和我联系,如果有不正确的地方也希望多多海涵并能给我及时反馈,我将感激不尽!