WPF的依賴屬性和附加屬性(用法解釋較全)

轉:https://www.cnblogs.com/zhili/p/WPFDependencyProperty.html

1、引言

  感受最近都頹廢了,很久沒有學習寫博文了,出於負罪感,今天強烈逼迫本身開始更新WPF系列。儘管最近看到一篇WPF技術是否老矣的文章,可是仍是不能阻止我係統學習WPF。今天繼續分享WPF中一個最重要的知識點——依賴屬性。html

2、依賴屬性的全面解析

  聽到依賴屬性,天然聯想到C#中屬性的概念。C#中屬性是抽象模型的核心部分,而依賴屬性是專門基於WPF建立的。在WPF庫實現中,依賴屬性使用普通的C#屬性進行了包裝,使得咱們能夠經過和之前同樣的方式來使用依賴屬性,但咱們必須明確,在WPF中咱們大多數都在使用依賴屬性,而不是使用屬性。依賴屬性重要性在於,在WPF核心特性,如動畫、數據綁定以及樣式中都須要使用到依賴屬性。既然WPF引入了依賴屬性,也天然有其引入的道理。WPF中的依賴屬性主要有如下三個優勢:express

  • 依賴屬性加入了屬性變化通知、限制、驗證等功能。這樣可使咱們更方便地實現應用,同時大大減小了代碼量。許多以前須要寫不少代碼才能實現的功能,在WPF中能夠輕鬆實現。
  • 節約內存:在WinForm中,每一個UI控件的屬性都賦予了初始值,這樣每一個相同的控件在內存中都會保存一份初始值。而WPF依賴屬性很好地解決了這個問題,它內部實現使用哈希表存儲機制,對多個相同控件的相同屬性的值都只保存一份。關於依賴屬性如何節約內存的更多內容參考:WPF的依賴屬性是怎麼節約內存的
  • 支持多種提供對象:能夠經過多種方式來設置依賴屬性的值。能夠配合表達式、樣式和綁定來對依賴屬性設置值。

2.1 依賴屬性的定義

  上面介紹了依賴屬性所帶來的好處,這時候,問題又來了,怎樣本身定義一個依賴屬性呢?C#屬性的定義你們再熟悉不過了。下面經過把C#屬性進行改寫成依賴屬性的方式來介紹依賴屬性的定義。下面是一個屬性的定義:windows

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

  在把上面屬性改寫爲依賴屬性以前,下面總結下定義依賴屬性的步驟:ide

  1. 讓依賴屬性的所在類型繼承自DependencyObject類。
  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中使用屬性對依賴屬性進行包裝。oop

2.2 依賴屬性的優先級

   WPF容許在多個地方設置依賴屬性的值,則天然就涉及到依賴屬性獲取值的優先級問題。例以下面XMAL代碼,咱們在三個地方設置了按鈕的背景顏色,那最終按鈕會讀取那個設置的值呢?是Green、Yellow仍是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 Trigger的優先級比Style Setter高,可是因爲此時Style Trigger的IsMouseOver屬性爲false,即鼠標沒有移到按鈕上,一旦鼠標移到按鈕上時,此時按鈕的顏色就爲Red。此時纔會體現出Style Trigger的優先級比Style 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的FontSize</StatusBar>
    </StackPanel>
</Window>
複製代碼

上面的代碼的運行效果以下圖所示:

  在上面XAML代碼中。Window.FontSize設置會影響全部內部子元素字體大小,這就是依賴屬性的繼承。如第一個Label沒有定義FontSize,因此它繼承了Window.FontSize值。但一旦子元素提供了顯式設置,這種繼承就會被打斷,因此Window.FontSize值對於第二個Label再也不起做用。

  這時,你可能已經發現了問題:StatusBar沒有顯式設置FontSize值,但它的字體大小沒有繼承Window.FontSize的值,而是保持了系統的默認值。那這是什麼緣由呢?其實致使這樣的問題:並非全部元素都支持屬性值繼承的,如StatusBar、Tooptip和Menu控件。另外,StatusBar等控件截獲了從父元素繼承來的屬性,而且該屬性也不會影響StatusBar控件的子元素。例如,若是咱們在StatusBar中添加一個Button。那麼這個Button的FontSize屬性也不會發生改變,其值爲默認值。

  前面介紹了依賴屬性的繼承,那咱們如何把自定義的依賴屬性設置爲可被其餘控件繼承呢?經過AddOwer方法能夠依賴屬性的繼承。具體的實現代碼以下所示:

複製代碼
 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 MinDate
11         {
12             get { return (DateTime)GetValue(MinDateProperty); }
13             set { SetValue(MinDateProperty, value); }
14         }
15     }
16 
17     public class CustomButton :Button
18     {
19         private static readonly DependencyProperty MinDateProperty;
20 
21         static CustomButton()
22         {
23             // AddOwner方法指定依賴屬性的全部者,從而實現依賴屬性的繼承,即CustomStackPanel的MinDate屬性被CustomButton控件繼承。
24             // 注意FrameworkPropertyMetadataOptions的值爲Inherits
25             MinDateProperty = CustomStackPanel.MinDateProperty.AddOwner(typeof(CustomButton), new FrameworkPropertyMetadata(DateTime.MinValue, FrameworkPropertyMetadataOptions.Inherits));
26         }
27 
28         public DateTime MinDate
29         {
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屬性來進行獲取的,關於綁定的更多內容會在後面文章中分享。在這裏CustomButton中並無設置MinDate的值,可是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,你也能夠設置一個private的SetValue進行限制。
12         public int Counter
13         {
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中還有一類特殊的屬性——附加屬性。附加是一種特殊的依賴屬性。它容許給一個對象添加一個值,而該對象可能對這個值一無所知。附加屬性最多見的例子就是佈局容器中DockPanel類中的Dock附加屬性和Grid類中Row和Column附加屬性。那問題又來了,咱們怎樣在本身的類中定義一個附加屬性呢?其實定義附加屬性和定義通常的依賴屬性同樣沒什麼區別,只是用RegisterAttached方法代替了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屬性進行封裝,而是使用靜態SetIsAttached方法和GetIsAttached方法來存取IsAttached值。這兩個靜態方法內部同樣是調用SetValue和GetValue來對附加屬性讀寫的。

2.6 依賴屬性驗證和強制

   在定義任何類型的屬性時,都須要考慮錯誤設置屬性的可能性。對於傳統的CLR屬性,能夠在屬性的設置器中進行屬性值的驗證,不知足條件的值能夠拋出異常。但對於依賴屬性來講,這種方法不合適,由於依賴屬性經過SetValue方法來直接設置其值的。然而WPF有其代替的方式,WPF中提供了兩種方法來用於驗證依賴屬性的值。

  • ValidateValueCallback:該回調函數能夠接受或拒絕新值。該值可做爲DependencyProperty.Register方法的一個參數。
  • CoerceValueCallback:該回調函數可將新值強制修改成可被接受的值。例如某個依賴屬性Age的值範圍是0到120,在該回調函數中,能夠對設置的值進行強制修改,對於不知足條件的值,強制修改成知足條件的值。如當設置爲負值時,可強制修改成0。該回調函數可做爲PropertyMetadata構造函數參數進行傳遞。

  當應用程序設置一個依賴屬性時,所涉及的驗證過程以下所示:

  1. 首先,CoerceValueCallback方法能夠修改提供的值或返回DependencyProperty.UnsetValue
  2. 若是CoerceValueCallback方法強制修改了提供的值,此時會激活ValidateValueCallback方法進行驗證,若是該方法返回爲true,表示該值合法,被認爲可被接受的,不然拒絕該值。不像CoerceValueCallback方法,ValidateValueCallback方法不能訪問設置屬性的實際對象,這意味着你不能檢查其餘屬性值。即該方法中不能對類的其餘屬性值進行訪問。
  3. 若是上面兩個階段都成功的話,最後會觸發PropertyChangedCallback方法來觸發依賴屬性值的更改。

  下面代碼演示了基本的流程。

複製代碼
 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 : DependencyObject
12     {
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 SimpleDP
22         {
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來判斷傳入的value是否有效,若是無效就不繼續後續操做。而且CoerceValue後面並無運行ValidateValue,而是直接調用PropertyChanged。這是由於CoerceValue操做並無強制改變屬性的值,而前面對這個值已經驗證過了,因此也就沒有必要再運行Valudate方法來進行驗證了。可是若是在Coerce中改變了Value的值,那麼還會再次調用Valudate操做來驗證值是否合法。

2.7  依賴屬性的監聽

  咱們能夠用兩種方法對依賴屬性的改變進行監聽。這兩種方法是:

  下面分別使用這兩種方式來實現下對依賴屬性的監聽。

  第一種方式:定義一個派生於依賴屬性所在的類,而後重寫依賴屬性的元數據並傳遞一個PropertyChangedCallback參數便可,具體的實現以下代碼所示:

複製代碼
 1 public class MyTextBox : TextBox
 2     {
 3         public MyTextBox()
 4             : base()
 5         {
 6         }
 7 
 8         static MyTextBox()
 9         {
10             //第一種方法,經過OverrideMetadata
11             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並調用AddValueChange方法爲其綁定一個回調函數。具體實現代碼以下所示:

複製代碼
 public MainWindow()
        {
            InitializeComponent();
            //第二種方法,經過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");
        }
複製代碼

3、總結

   到這裏,依賴屬性的介紹就結束了。WPF中的依賴屬性經過一個靜態只讀字段進行定義,而且在靜態構造函數中進行註冊,最後經過.NET傳統屬性進行包裝,使其使用與傳統的.NET屬性並沒有兩樣。在後面一篇文章將分享WPF中新的事件機制——路由事件。

  本文全部源碼下載:DependencyPropertyDemo.zip

相關文章
相關標籤/搜索