WPF中的依賴屬性和附加屬性


    重混江湖后的第一篇文章,竟然有些手生......(惶恐+慚愧)ing,怕是套路也要有些變化了-_-


一.屬性

    剛着手開始學習C#的時候,不明白為什么會有屬性這個東西,不是已經有了字段了嗎,你說屬性里面有get和set方法對數據進行了封裝,可以通過對方法的訪問限定來控制該屬性是否可以被賦值,但是不也有readonly這個關鍵字可以用來修飾字段嗎,你又說可以通過在get或set方法里面對數據進行一系列的操作來對數據進行限制,可以將數據的獲取和賦值分開進行不同的處理,好吧這個我是服氣的。

你可以這樣子寫(寫prop再按兩次Tab就自動生成了):

 

  1.  
    public int Age { get; set; } //則就是個很普通的屬性了
  2.  
    public int Age { get; } //可以不寫set方法,等同於通過訪問限定符private來對set方法進行修飾

還可以這樣寫:

 

 

  1.  
    private int _age;
  2.  
    public int Age
  3.  
    {
  4.  
    get { return _age; }
  5.  
    //這就比單純的字段對數據的處理要來得自由
  6.  
    private set
  7.  
    {
  8.  
    if ( value < 0 || value > 100)
  9.  
    return;
  10.  
    _age = value;
  11.  
    }
  12.  
    }
其實這都是很基礎的東西了,但還是覺得此處應有個引入,直接上貨怕有些干......

 

那下面要切入正題了——>

 

 

二.依賴屬性

    吶,名字里都有個屬性說明它就是個屬性,只是這個屬性在方法里封裝處理的並不是普通的字段,而是類型為DependencyProperty的一個對象,我們可以先來看它的普通寫法(propdp按一次Tab自動生成,好方便的說):

 

  1.  
    public int MyProperty
  2.  
    {
  3.  
    get { return ( int)GetValue(MyPropertyProperty); }
  4.  
    set { SetValue(MyPropertyProperty, value); }
  5.  
    }
  6.  
     
  7.  
    //下面的注釋是自動生成的,可以看到這個對象就是用於MyProperty的封裝數據,它可以作用於動畫,樣式,綁定等等......
  8.  
    // Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
  9.  
    public static readonly DependencyProperty MyPropertyProperty =
  10.  
    DependencyProperty.Register( "MyProperty", typeof( int), typeof(ownerclass), new PropertyMetadata( 0));

 

上面的MyProperty屬性這里不做解釋,看一下MyPropertyProperty,這里要說明的是依賴屬性的命名約定為以Property作為后綴,該對象使用了DependencyProperty類里面的Register靜態方法的返回值:

可以看到Register方法有三個重載,前三個參數

 

  • String:表示所要對其進行封裝的屬性的名稱;
  • 第一個Type:表示該屬性所屬類型;
  • 第二個Type:表示該屬性所有者的類型,也就是在哪個類里面定義的這個依賴屬性;
后面的參數,PropertyMetadata對象



 

這個對象里面包含有依賴屬性的元數據相關的信息,元數據也就是一些用來描述類型的信息,這里包含例如屬性默認值通知回調和驗證回調的引用,還有一些框架性特征(布局、數據綁定等),其中參數:

 

  • Object:表示要對該屬性指定的默認值,例如string類型可以指定default(string);
  • PropertyChangedCallback:從名字就可以看出,這是一個在屬性值被更改時所調用的回調函數,常用於綁定數據變更時處理;
  • CoerceValueCallback:Coerce意思是脅迫、強制,也就是這個方法用於在對屬性值進行改變的時候,對賦值進行檢查,強制對值進行賦值,返回值為Object類型,這個才是要賦給屬性的值;
Register方法的最后一個參數是ValidateValueCallback,這個方法同樣是對值的檢查,該方法如下:
public delegate bool ValidateValueCallback(object value)
傳入的參數value為Object類型,也就是要進行檢查的屬性值,這里會不會有疑問,既然已經有了CoerceValueCallback方法可以對值進行檢查之后強制改變要進行的賦值,為什么還要有這個合法值得檢查? 因為CoerceValueCallback方法無論如何檢查,返回值為Object,都會給屬性值進行一個賦值,然后會調用PropertyChangedCallback方法,但是在進行CoerceValueCallback方法的調用之前,就會調用ValidateValueCallback方法對值進行檢查,可以看到該方法的返回值為一個bool類型,當值滿足條件時返回true,這時就會去調用CoerceValueCallback對值進行進一步的處理,如果不滿足條件的話返回false,則本次賦值失敗告終,根本就沒有后續CoerceValueCallback什么事,也就是ValidateValueCallback方法是值更新前檢查方法,對值進行的最初的攔截檢查,可以看如下圖:



所以,在對依賴屬性進行注冊的時候可以根據需要選擇不同的Register方法和Parameter構造器,實現需要的方法來對數據進行處理;
 
前面講了一堆,可能沒有接觸的人還會覺得雲里霧里,或者說那些東西我看教程都會用,到底為什么要有依賴屬性這么個東西?
在進行WPF桌面編程的時候,我們都會用到各種各樣的控件,這些控件都有一個高台階似的繼承關系,控件中的屬性也都是一層層繼承下來的,如果都是普通屬性的話,每個屬性在每個類中都是需要占有一定字節數,可能有些屬性並不會被用到,但還是會保留從父類那里繼承來的初始值,這樣的話,到了底層的類就會有對象膨脹的問題,所以寫到這里你應該可以想到依賴屬性就是解決這個問題的吧,當然了,不然我說這些是為了鋪墊晚飯吃什么嗎-_-
依賴屬性是如何解決普通屬性因為繼承帶來的對象膨脹的問題就是接下來要研究的依賴屬性的原理:
那就從最直觀的使用DependencyProperty.Register方法來切入,Register注冊,注冊什么呢,注冊屬性數據啊,你不是因為繼承每個基類里面都會有一份屬性嗎,那就將這些屬性從類里面剝離出來,放到一個公共的區域,如果有需要就去里面進行值的注冊,獲取的時候去公共區里面查找,這不是很方便嗎,沒有用到的屬性就不往里放,問題是不是就解決了?那好,我們用圖解來說明:


 

這里的公共區是一個全局字典,里面用來存放所有的依賴屬性,其中:

 

  • Key是關鍵詞,這個是在注冊方法里面由所給的屬性名和所屬類進行哈希異或得到的哈希值;
  • value很好理解,就是每一個屬性所有值;
  • index這個東西,因為每個依賴屬性都是靜態的,當進行注冊之后相同的依賴屬性只會在字典中保存一份,如果一個值更改那么其他所擁有者的值也會相應的進行更改,這顯然不是我們所期望的,所以,真正進行存儲的時候,全局字典中只會存儲注冊時候給的初始值,而對於其他通過繼承方式得到該依賴屬性並對其進行賦值的時候,其實是存儲在一個單獨的鏈表中,鏈表的存儲對象中才存放每一個擁有者所賦的不同的值,哎呀看圖吧:


 

上圖中的index是相同的,每一次進行值操作的時候,會現在全局的字典中通過Key值拿到對應的Property數據,獲取其中的index,然后通過在鏈表中進行對應的index查找,如果找到了就使用鏈表中的值,如果沒有就使用本身的默認值;

那,如果我並不想要父類的那個依賴屬性,我想把它變成自己的,也換一個默認值怎么辦?重載咯:

把Type換成自己的類,重新指定PropertyMetadata就好了,這其實是在字典中重新注冊了一個依賴屬性,也可以通過這個方法重新寫該屬性的元數據;

那,我另外的一個類也想使用那個依賴屬性,就只能通過繼承的方式嗎,我只是想用一個屬性而已啊,要按其他不需要的也都繼承下來嗎?沒這個必要啊:

通過DependencyProperty的AddOwner方法,就可以把那個屬性變為自己可以使用的了,同樣也可以更改其默認值,也是在字典中重新注冊了一個依賴屬性;

差不多吧,主要要說的就是這些,剩下的依賴屬性的使用,比如在動畫啊,樣式或者綁定什么的都是另外的話了,哦還有優先級什么的,后續有需要再補充吧。

 

三.附加屬性

簡單講,附加屬性就是依賴屬性,但它存在的意義是什么呢,先看寫法:

 

  1.  
    public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.RegisterAttached(
  2.  
    "MyProperty",
  3.  
    typeof(Boolean),
  4.  
    typeof(MyClass),
  5.  
    new FrameworkPropertyMetadata());
  6.  
    public static void SetMyProperty(UIElement element, Boolean value)
  7.  
    {
  8.  
    element.SetValue(MyPropertyProperty, value);
  9.  
    }
  10.  
    public static Boolean GetMyProperty(UIElement element)
  11.  
    {
  12.  
    return (Boolean)element.GetValue(MyPropertyProperty);
  13.  
    }
可以看到除了注冊方法是RegisterAttached之外還有另外獨立出來的靜態方法SetValue和GetValue,在這里RegisterAttached和Register方法並沒有什么不同,都是往全局字典中添加屬性;

 

但是可以看到在兩個方法中有傳入參數UIElement,所以有人說“依賴屬性是給自己用的,附加屬性是給別人用的”,這句話是沒錯的,在使用附加屬性的時候回將使用者的信息作為參數傳入進來,所以會有一些布局控件例如Grid,Canvas等有附加屬性,Grid.SetRow和Canvas.SetLeft都是直接調用了靜態的方法,通過傳入的參數來告知屬性所有者是誰用了我的屬性,把它設為了什么值,這樣在布局的時候我就知道要把誰往哪里放,這也是依賴屬性和附加屬性的一個區別。而且像這樣單獨將兩個方法暴露出來也是為了方便他人使用。

至於其他方面,其實都是一樣的。

 

四.栗子時間

前面講了那么多,都不如直接上個栗子看的實在(我也實在是寫累了):

假設這里有個需求,需要在TextBox里面填寫年齡,默認值為成年18歲,但是不能超過100歲,也不能低於18歲,也就是有如下需求:

 

  • 有默認值
  • 有上下限
  • 有值檢查
  • 有錯誤提醒
而且有個兒童的勾選項,當選擇了兒童之后,默認值就要更改為1,且上下限為1~18,如果取消勾選項恢復為成人設定,這里設計如下:
C#后台代碼:
  1.  
    public partial class MainWindow
  2.  
    {
  3.  
    public MainWindow()
  4.  
    {
  5.  
    InitializeComponent();
  6.  
     
  7.  
    SetTextBinding();
  8.  
    }
  9.  
     
  10.  
    private void SetTextBinding()
  11.  
    {
  12.  
    var binding = new Binding
  13.  
    {
  14.  
    Source = this,
  15.  
    Path = new PropertyPath( "MyAge")
  16.  
    };
  17.  
    TestTextBox.SetBinding(TextBox.TextProperty, binding);
  18.  
    }
  19.  
     
  20.  
    public int MyAge
  21.  
    {
  22.  
    get { return ( int)GetValue(MyAgeProperty); }
  23.  
    set { SetValue(MyAgeProperty, value); }
  24.  
    }
  25.  
     
  26.  
    //這里注冊一個MyAge的依賴屬性用於前台綁定,指定默認值為18
  27.  
    public static readonly DependencyProperty MyAgeProperty =
  28.  
    DependencyProperty.Register( "MyAge", typeof( int), typeof(MainWindow),
  29.  
    new PropertyMetadata( 18, PropertyChangedCallback, CoerceValueCallback), ValidateValueCallback);
  30.  
     
  31.  
    //當賦值成功的時候就改變TextBox的邊框顏色
  32.  
    private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
  33.  
    {
  34.  
    var window = dependencyObject as Window;
  35.  
    var textBox = window?.FindName( "TestTextBox") as TextBox;
  36.  
    if (textBox != null) textBox.BorderBrush = Brushes.Cyan;
  37.  
    }
  38.  
     
  39.  
    //當進行賦值的時候,如果值不在18~100之間,強制將值恢復為18,表示該值不合要求
  40.  
    private static object CoerceValueCallback(DependencyObject dependencyObject, object baseValue)
  41.  
    {
  42.  
    var value = ( int)baseValue;
  43.  
    return value < 18 ? 18 : value;
  44.  
    }
  45.  
     
  46.  
    //在開始賦值前對值進行檢查,如果值超過100就彈個錯誤框顯示
  47.  
    private static bool ValidateValueCallback(object o)
  48.  
    {
  49.  
    var value = ( int)o;
  50.  
    if ( value > 100)
  51.  
    MessageBox.Show( "年齡有點大哈");
  52.  
    return value < 100;
  53.  
    }
  54.  
     
  55.  
    //當兒童選項被勾選的時候去更改屬性的元數據
  56.  
    private void ChildCheckBox_OnChecked(object sender, RoutedEventArgs e)
  57.  
    {
  58.  
    ChildAge.Instance.ChangeMetadata(sender);
  59.  
    }
  60.  
     
  61.  
    //當取消勾選的時候,應該恢復為原來的屬性值綁定才能正確顯示
  62.  
    private void ChildCheckBox_OnUnchecked(object sender, RoutedEventArgs e)
  63.  
    {
  64.  
    SetTextBinding();
  65.  
    }
  66.  
    }
  67.  
     
  68.  
    //重新定義一個Child類繼承MainWindow
  69.  
    public class ChildAge : MainWindow
  70.  
    {
  71.  
    public static ChildAge Instance
  72.  
    {
  73.  
    get { return _instance ?? (_instance = new ChildAge()); }
  74.  
    }
  75.  
     
  76.  
    private static ChildAge _instance;
  77.  
     
  78.  
    private static Window _containerWindow;
  79.  
    public void ChangeMetadata(object sender)
  80.  
    {
  81.  
    var item = sender as CheckBox;
  82.  
    if (item == null) return;
  83.  
     
  84.  
    //更改屬性元數據,默認值為1,對應的值處理檢查方法也需重新定義
  85.  
    MyAgeProperty.OverrideMetadata( typeof(ChildAge),
  86.  
    new PropertyMetadata( 1, ChildAgePropertyChangedCallback, ChildCoerceValueCallback));
  87.  
     
  88.  
    ///////////這里因為前台數據是和后台綁定的,因此如果對應的屬性改變了,前台綁定的源也應該隨之改變
  89.  
    _containerWindow = GetWindow(item);
  90.  
    if (_containerWindow != null)
  91.  
    {
  92.  
    var binding = new Binding
  93.  
    {
  94.  
    Source = this,
  95.  
    Path = new PropertyPath( "MyAge")
  96.  
    };
  97.  
    var textBox = _containerWindow.FindName( "TestTextBox") as TextBox;
  98.  
    textBox?.SetBinding(TextBox.TextProperty, binding);
  99.  
    }
  100.  
    }
  101.  
     
  102.  
    private static object ChildCoerceValueCallback(DependencyObject dependencyObject, object baseValue)
  103.  
    {
  104.  
    var value = ( int)baseValue;
  105.  
    return ( value < 1 || value >= 18) ? 1 : value;
  106.  
    }
  107.  
     
  108.  
    private static void ChildAgePropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
  109.  
    {
  110.  
    var textBox = _containerWindow?.FindName( "TestTextBox") as TextBox;
  111.  
    if (textBox != null) textBox.BorderBrush = Brushes.DarkGreen;
  112.  
    }
  113.  
    }
 
前台如下設計:
 

 
程序運行結果如下:

 
至此結束對依賴屬性和附加屬性的討論,如果對該文章有任何疑問或者有錯誤之處請及時指出,蟹蟹。
更多更詳細的內容可以參見MSDN:MSDN依賴屬性和附加屬性
 
 
 
         
 
         
 
         
 
         
 
        

 

出處:https://blog.csdn.net/miss_bread/article/details/78295463


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM