一、引言
我觉得我最近颓废了。我已经很久没学会写博客了。出于内疚,我今天强烈强迫自己更新WPF系列。虽然我最近看到了一篇关于WPF技术是否老的文章,但我仍然无法阻止我系统地学习WPF。今天,我将继续分享WPF中最重要的知识点之一——依赖属性。
二、依赖属性的综合分析听到依赖属性,自然会想到C#中属性的概念。C#中等属性是抽象模型的核心部分,依赖属性是专门基于WPF创建的。在WPF库的实现中,依赖属性使用普通的C#属性进行包装,使我们能够以与以前相同的方式使用依赖属性,但必须明确的是,我们大多数人在WPF中使用依赖属性,而不是使用属性。依赖属性的重要性在于,依赖属性需要用于WPF的核心特性,如动画、数据绑定和风格。由于WPF引入了依赖属性,自然也有其引入的原因。WPF的依赖属性主要有以下三个优点:
- 依赖属性增加了属性变更通知、限制、验证等功能。这使得我们更容易实现应用程序,并大大减少了代码的数量。WPF中可以很容易地实现许多以前需要编写许多代码的功能。
- 节省内存:在Winform中,每个UI控制器的属性都给出了初始值,以便每个相同的控制器在内存中保存一个初始值。WPF依赖属性很好地解决了这个问题。它实现了哈希表存储机制的内部使用,只保存了多个相同控制器的相同属性值。关于如何依赖属性来节省内存的更多参考:WPF依赖属性是如何节省内存的
- 支持各种提供对象:依赖属性的值可以通过多种方式设置。依赖属性的设置值可以与表达式、样式和绑定相结合。
以上介绍了依赖属性的好处。这时,问题又来了。如何定义依赖属性?C#属性的定义大家都很熟悉。以下是依赖属性的定义,通过将C#属性重写为依赖属性。以下是属性的定义:
1 public class Person2 {3 public string Name { get; set; }6 }
在将上述属性改写为依赖属性之前,下面总结了定义依赖属性的步骤:
- 从Dependendencyobject类继承依赖属性的类型。
- 使用public static 声明DependencyProperty的变量,这是真正依赖属性的变量。
- 依赖属性的元数据注册是通过Register方法在类型的静态构造函数中完成的。
- 通过这一属性,提供依赖属性的包装属性,完成依赖属性的读写操作。
根据以上四个步骤,将Name属性改写为依赖属性,具体实现代码如下:
// 1. 类型继承Dependencyobject类型 public class Person : DependencyObject { // 2. 声明DependencyProperty,静态只读 字段 public static readonly DependencyProperty nameProperty; static Person() { // 3. 注册定义的依赖属性 nameProperty = DependencyProperty.Register("Name", typeof(string), typeof(Person), new PropertyMetadata("Learning Hard",OnValueChanged)); } // 4. 属性包装器,通过它来读取和设置我们刚刚注册的依赖属性 public string Name { get { return (string)GetValue(nameProperty); } set { SetValue(nameProperty, value); } } private static void OnValueChanged(DependencyObject dpobj, DependencyPropertyChangedEventArgs e) { // 当只发生变化时,回调的方法 } }
从上面的代码可以看出,依赖属性是通过调用Dependencyobject的Getvalue和Setvalue来读写的。它用哈希表存储,相应的Key是属性的HashCode值,值(Value)注册DependencyPropery;C#中的属性是类私有字段的封装,可以通过操作字段来读写。综上所述,属性是字段包装,WPF使用属性包装依赖属性。
2.2 依赖属性的优先级WPF允许在多个地方设置依赖属性的值,这自然涉及到依赖属性获取值的优先级。例如,在以下XMAL代码中,我们在三个地方设置了按钮的背景颜色,那么最终的按钮会读取设置的值呢?是Gren、Yelllow还是Red?
<Window x:Class="DPSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <Button x:Name="myButton" Background="Green" Width="100" Height="30"> <Button.Style> <Style TargetType="{x:Type Button}"> <Setter Property="Background" Value="Yellow"/> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="Red" /> </Trigger> </Style.Triggers> </Style> </Button.Style> Click Me </Button> </Grid></Window>
上面按钮的背景颜色是Green。背景颜色之所以是Green,是因为WPF每次访问一个依赖属性时,都会按照以下顺序从高处理值。具体优先级如下图所示:
在上述XAML中,Green设置了按钮的本地值,自定义Style Trigger设置为Red,自定义Style Setter设置为Yellow,因为这里的本地值优先级最高,所以按钮的背景色或Green值。如果此时去掉本地值Green,此时按钮的背景颜色是Yellow而不是Red。尽管Style在这里 与Style相比,Triger的优先级 Setter很高,但因为Styleer很高 Trigger的Ismouseover属性是false,即鼠标没有移动到按钮上,一旦鼠标移动到按钮上,按钮的颜色就是Red。这时,Stylele就会体现出来 与Style相比,Triger的优先级 Setter优先级高。所以上图中的优先级比较理想,往往需要具体分析。
2.3 依赖属性的继承可以继承依赖属性,即父元素的相关设置会自动传递给所有子元素。以下代码演示了依赖属性的继承。
<Window x:Class="Custom_DPInherited.DPInherited" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" FontSize="18" Title=“依赖属性的继承”> <StackPanel > <Label Content=“继承自Window的Fontsize” /> <Label Content=“Fontsize显式设置” TextElement.FontSize="36"/> <StatusBar>Statusbar没有继承Window的FontSizee</StatusBar> </StackPanel></Window>
上述代码的运行效果如下图所示:
在上述XAML代码中。Window.FontSize设置会影响所有内部元素的字体大小,这取决于属性的继承。例如,第一个Label没有定义Fontsize,所以它继承了Window.FontSize值。但一旦子元素提供了显式设置,这种继承就会被打断,所以Window.FontSize值不再对第二个Label起作用。
此时,您可能已经发现了一个问题:Statusbar没有显式设置fontsize值,但其字体大小没有继承window.FontSize的值保持了系统的默认值。原因是什么?事实上,并非所有元素都支持属性值继承,比如Statusbarr、Tooptip和Menu控件。此外,Statusbar等控件截获了从父元素继承的属性,该属性不会影响Statusbar控件的子元素。例如,如果我们在Statusbar中添加一个button。那么Button的FontSize属性也不会改变,其值为默认值。
上面介绍了依赖属性的继承,那么我们如何将自定义的依赖属性设置为其他控制器的继承呢?依赖属性的继承可以通过Adddower方法进行。具体的实现代码如下:
1 public class CustomStackPanel : StackPanel 2 { 3 public static readonly DependencyProperty MinDateProperty; 4 5 static CustomStackPanel() 6 { 7 MinDateProperty = DependencyProperty.Register("MinDate", typeof(DateTime), typeof(CustomStackPanel), new FrameworkPropertyMetadata(DateTime.MinValue, FrameworkPropertyMetadataOptions.Inherits)); 8 } 9 10 public DateTime MinDate11 {12 get { return (DateTime)GetValue(MinDateProperty); }13 set { SetValue(MinDateProperty, value); }14 }15 }16 17 public class CustomButton :button18 {19 private static readonly DependencyProperty MinDateProperty;20 21 static CustomButton()22 {23 // Adddowner方法指定依赖属性的所有者,从而实现依赖属性的继承,即CustomstackPanel的Mindate属性由CustomButton控件继承。24 // 注意FrameworkPropertyMetadataoptions的值为Inherits25 MinDateProperty = CustomStackPanel.MinDateProperty.AddOwner(typeof(CustomButton), new FrameworkPropertyMetadata(DateTime.MinValue, FrameworkPropertyMetadataOptions.Inherits));26 }27 28 public DateTime MinDate29 {30 get { return (DateTime)GetValue(MinDateProperty); }31 set { SetValue(MinDateProperty, value); }32 }33 }
接下来,您可以在XAML中进行测试,具体的XAML代码如下:
<Window x:Class="Custom_DPInherited.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Custom_DPInherited" xmlns:sys="clr-namespace:System;assembly=mscorlib" Title=实现自定义依赖属性的继承” Height="350" Width="525"> <Grid> <local:CustomStackPanel x:Name="customStackPanle" MinDate="{x:Static sys:DateTime.Now}"> <!--Customstackpanel依赖属性--> <ContentPresenter Content="{Binding Path=MinDate, ElementName=customStackPanle}"/> <local:CustomButton Content="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=MinDate}" Height="25"/> </local:CustomStackPanel> </Grid></Window>
在上述XAML代码中,设置CustomstackPanel的Mindate值显示,而在CustomButton中没有显式设置其Mindate值。CustomButton的Content属性值是通过绑定Mindate属性获得的,更多关于绑定的内容将在后面的文章中分享。MinDate的值并没有设置在CustomButton中,但CustomButton的Content值是当前时间,因此可以看出,此时CustomButton的MinDate属性继承了CustomStackPanel的MinDate值,从而设置了Content属性。最终效果如下图所示:
2.4 只读依赖属性在C#属性中,我们可以设置只读属性,防止外界恶意改变属性值。同样,只读依赖属性也可以设置在WPF中。例如,ISMouseover是一种只读依赖属性。那么,我们如何创建一个只读依赖属性呢?事实上,仅读的依赖属性的定义方法与一般依赖属性的定义方法基本相同。只阅读依赖属性,只使用DependencyProperty.RegisterReadonly取代了DependencyProperty.只是Register。以下代码实现了只读依赖属性。
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 7 // Setvalue用于内部设置值 8 SetValue(counterKey, 8); 9 }10 11 // 属性包装器,只提供Getvalue,您还可以设置privateSetvalue进行限制。12 public int Counter13 {14 get { return (int)GetValue(counterKey.DependencyProperty); }15 }16 17 // 用RegisterReadonly代替Register注册只读的依赖属性18 private static readonly DependencyPropertyKey counterKey =19 DependencyProperty.RegisterReadOnly("Counter",20 typeof(int),21 typeof(MainWindow),22 new PropertyMetadata(0));23 }
对应的XAML代码为:
<Window x:Class="ReadOnlyDP.MainWindow" Name="ThisWin" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="ReadOnly Dependency Property" Height="350" Width="525"> <Grid> <Viewbox> <TextBlock Text="{Binding ElementName=ThisWin, Path=Counter}"/> </Viewbox> </Grid></Window>
此时,Counter包装的counterkey是一种只读依赖属性,因为它被定义为private,所以不能在类外使用dependencyobject.Setvalue法对其值,而包装的counter属性只提供Getvalue法,因此类外只能读取依赖属性,而不能赋值。运行效果如下图所示。
2.5 附加属性WPF中还有一种特殊属性-附加属性。附加是一种特殊的依赖属性。它允许给一个对象添加一个值,而这个对象可能对这个值一无所知。附加属性最常见的例子是Dock附加属性和Grid类Row和Column附加属性。问题又来了。我们如何在自己的类别中定义一个附加属性?其实定义附加属性和定义一般依赖属性没什么区别,只是用Registeratached代替Register方法。下面的代码演示了附加属性的定义。
public class AttachedPropertyClass { // 使用Registerattached注册附加属性 public static readonly DependencyProperty IsAttachedProperty = DependencyProperty.RegisterAttached("IsAttached", typeof(bool), typeof(AttachedPropertyClass), 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); } }
在上述代码中,Isattached是一个附加属性,它不使用CLR属性进行包装,而是使用静态Setisatached方法和Getisatached方法来访问Isatached值。这两种静态方法也使用Setvalue和Getvalue来读写附加属性。
2.6 依靠属性验证和强制性在定义任何类型的属性时,都需要考虑错误设置属性的可能性。对于传统的CLR属性,属性值可以在属性设置器中验证,不符合条件的值可以抛出异常。但是这种方法不适合依赖属性,因为依赖属性是通过Setvalue直接设置的。然而,WPF有其替代方法,WPF提供了验证依赖属性值的两种方法。
- ValidateValueCallback:回调函数可以接受或拒绝新值。该值可用作DependencyProperty。.Register方法的参数。
- CoerceValueCallback:该回调函数可以将新值强制修改为可接受值。例如,依赖属性Age的值范围为0至120。在回调函数中,可以强制修改设定值,并将不符合条件的值强制修改为符合条件的值。当设置为负值时,可强制修改为0。该回调函数可以作为PropertyMetadata构造函数参数传输。
当应用程序设置依赖属性时,所涉及的验证过程如下:
- 首先,Coercevaluecalback方法可以修改提供的值或返回Dependencyproperty.UnsetValue。
- 如果Coercevaluecalback方法强制修改提供的值,此时将激活validatevaluecalback方法进行验证。如果该方法返回到true,则表示该值合法且被认为可接受,否则将拒绝该值。与coercevaluecallback方法不同,validatevaluecalback方法无法访问设置属性的实际对象,这意味着您无法检查其他属性值。也就是说,该方法不能访问类别的其他属性值。
- 如果以上两个阶段都成功了,PropertyChangedCalback方法最终会触发依赖属性值的变化。
以下代码演示了基本流程。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 SimpleDPClass sDPClass = new SimpleDPClass(); 6 sDPClass.SimpleDP = 2; 7 Console.ReadLine(); 8 } 9 }10 11 public class SimpleDPClass : dependencyobject12 {13 public static readonly DependencyProperty SimpleDPProperty =14 DependencyProperty.Register("SimpleDP", typeof(double), typeof(SimpleDPClass),15 new FrameworkPropertyMetadata((double)0.0,16 FrameworkPropertyMetadataOptions.None,17 new PropertyChangedCallback(OnValueChanged),18 new CoerceValueCallback(CoerceValue)),19 new ValidateValueCallback(IsValidValue));20 21 public double SimpleDP2 {23 get { return (double)GetValue(SimpleDPProperty); }24 set { SetValue(SimpleDPProperty, value); }25 }26 27 private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)28 {29 Console.WriteLine(“当值发生变化时,我们可以在这里定义一些我们可以做的操作: {0}", e.NewValue);30 }31 32 private static object CoerceValue(DependencyObject d, object value)33 {34 Console.WriteLine(“限制对值,强制值: {0}", value);35 return value;36 }37 38 private static bool IsValidValue(object value)39 {40 Console.WriteLine()验证值是否通过,返回bool值,如果返回True表示验证通过,否则会以异常形式暴露: {0}", value);41 return true;42 }43 }
运行结果如下图所示:
从操作结果可以看出,Coerce在Validate之前并没有按照上述流程的顺序执行,这可能是WPF内部的一些特殊处理。当属性发生变化时,Validate将首先调用Validate来判断输入的Value是否有效,如果无效,则不会继续后续操作。而且CoerceValue背后没有直接调用PropertyChanged,运行ValidateValue。这是因为coercevalue操作没有强制改变属性值,而且这个值之前已经验证过了,所以没有必要操作valudate方法进行验证。但是,如果Value的值在Coerce中发生变化,则将再次调用Valudate操作来验证值是否合法。
2.7 依靠属性监听我们可以用两种方法来监控依赖属性的变化。这两种方法是:
- 使用DependencyPropertyDescriptor
- 使用OverrideMetadata。
这两种方法分别用于实现对依赖属性的监控。
第一种方法:定义一个源于依赖属性的类别,然后重写依赖属性的元数据,传递Propertychangedcalback参数。具体实现如下代码所示:
1 public class MyTextBox : TextBox 2 { 3 public MyTextBox() 4 : base() 5 { 6 } 7 8 static MyTextBox() 9 {10 //第一种方法,通过Overridemetadata TextProperty.OverrideMetadata(typeof(MyTextBox), new FrameworkPropertyMetadata(new PropertyChangedCallback(TextPropertyChanged)));12 }13 14 private static void TextPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)15 {16 MessageBox.Show("", "Changed");17 }18 }
第二种方法:这种方法更简单,获取DependencyPropertyDescriptor,并调用Adddvaluechange绑定回调函数。具体实现代码如下:
public MainWindow() { InitializeComponent(); //第二种方法,通过Overridemtadatata DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(TextBox.TextProperty, typeof(TextBox)); descriptor.AddValueChanged(tbxEditMe, tbxEditMe_TextChanged); } private void tbxEditMe_TextChanged(object sender, EventArgs e) { MessageBox.Show("", "Changed"); }
三、总结在这里,依赖属性的介绍就结束了。WPF中的依赖属性通过静态只读字段定义,并在静态构造函数中注册,最后通过.包装NET的传统属性,使其与传统一起使用.NET属性没有什么不同。在下一篇文章中,我们将分享WPF中新的事件机制-路由事件。
下载本文所有源代码:DependencyPropertyDemo.zip