当前位置: 首页 > 图灵资讯 > 技术篇> WPF快速入门系列(2)——深入解析依赖属性

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

来源:图灵教育
时间:2023-06-06 09:26:55

一、引言

  我觉得我最近颓废了。我已经很久没学会写博客了。出于内疚,我今天强烈强迫自己更新WPF系列。虽然我最近看到了一篇关于WPF技术是否老的文章,但我仍然无法阻止我系统地学习WPF。今天,我将继续分享WPF中最重要的知识点之一——依赖属性。

二、依赖属性的综合分析

  听到依赖属性,自然会想到C#中属性的概念。C#中等属性是抽象模型的核心部分,依赖属性是专门基于WPF创建的。在WPF库的实现中,依赖属性使用普通的C#属性进行包装,使我们能够以与以前相同的方式使用依赖属性,但必须明确的是,我们大多数人在WPF中使用依赖属性,而不是使用属性。依赖属性的重要性在于,依赖属性需要用于WPF的核心特性,如动画、数据绑定和风格。由于WPF引入了依赖属性,自然也有其引入的原因。WPF的依赖属性主要有以下三个优点:

  • 依赖属性增加了属性变更通知、限制、验证等功能。这使得我们更容易实现应用程序,并大大减少了代码的数量。WPF中可以很容易地实现许多以前需要编写许多代码的功能。
  • 节省内存:在Winform中,每个UI控制器的属性都给出了初始值,以便每个相同的控制器在内存中保存一个初始值。WPF依赖属性很好地解决了这个问题。它实现了哈希表存储机制的内部使用,只保存了多个相同控制器的相同属性值。关于如何依赖属性来节省内存的更多参考:WPF依赖属性是如何节省内存的
  • 支持各种提供对象:依赖属性的值可以通过多种方式设置。依赖属性的设置值可以与表达式、样式和绑定相结合。
2.1 依赖属性的定义

  以上介绍了依赖属性的好处。这时,问题又来了。如何定义依赖属性?C#属性的定义大家都很熟悉。以下是依赖属性的定义,通过将C#属性重写为依赖属性。以下是属性的定义:

1 public class Person2     {3         public string Name { get; set; }6     }

  在将上述属性改写为依赖属性之前,下面总结了定义依赖属性的步骤:

  1. 从Dependendencyobject类继承依赖属性的类型。
  2. 使用public static 声明DependencyProperty的变量,这是真正依赖属性的变量。
  3. 依赖属性的元数据注册是通过Register方法在类型的静态构造函数中完成的。
  4. 通过这一属性,提供依赖属性的包装属性,完成依赖属性的读写操作。

  根据以上四个步骤,将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每次访问一个依赖属性时,都会按照以下顺序从高处理值。具体优先级如下图所示:

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

  在上述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>

上述代码的运行效果如下图所示:

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

  在上述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属性。最终效果如下图所示:

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

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法,因此类外只能读取依赖属性,而不能赋值。运行效果如下图所示。

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

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构造函数参数传输。

  当应用程序设置依赖属性时,所涉及的验证过程如下:

  1. 首先,Coercevaluecalback方法可以修改提供的值或返回Dependencyproperty.UnsetValue。
  2. 如果Coercevaluecalback方法强制修改提供的值,此时将激活validatevaluecalback方法进行验证。如果该方法返回到true,则表示该值合法且被认为可接受,否则将拒绝该值。与coercevaluecallback方法不同,validatevaluecalback方法无法访问设置属性的实际对象,这意味着您无法检查其他属性值。也就是说,该方法不能访问类别的其他属性值。
  3. 如果以上两个阶段都成功了,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     }

  运行结果如下图所示:

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

  从操作结果可以看出,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