WPF 之 依賴屬性與附加屬性(五)

Dwaynerbing發表於2021-02-06

一、CLR 屬性

​ 程式的本質是“資料+演算法”,或者說用演算法來處理資料以期得到輸出結果。在程式中,資料表現為各種各樣的變數,演算法則表現為各種各樣的函式(操作符是函式的簡記法)。

類的作用是把散落在程式中的變數和函式進行歸檔封裝並控制它們的訪問。被封裝在類裡的變數稱為欄位(Field),它表示的是類或例項的狀態;被封裝在類裡的函式稱為方法(Method),它表示類或例項的功能。

​ 欄位(Field)被封裝在例項裡,要麼能被外界訪問(非 Private修飾),要麼不能(使用 Private 修飾),這種直接把資料暴露給外界的做法很不安全,很容易把錯誤的數值寫入欄位。為了解決此問題,.NET Framework 推出了屬性(Property),這種 .NET Framework 屬性又稱為 CLR 屬性。

​ 屬性是一種成員,它提供靈活的機制來讀取、寫入或計算私有欄位的值。 屬性可用作公共資料成員,但它們實際上是稱為訪問器的特殊方法。 這使得我們不僅可以輕鬆訪問資料,還有助於提高方法的安全性和靈活性。具體使用如下:

private double _seconds;

   public double Hours
   {
       get { return _seconds / 3600; }
       set {
          if (value < 0 || value > 24)
             throw new ArgumentOutOfRangeException(
                   $"{nameof(value)} must be between 0 and 24.");

          _seconds = value * 3600;
       }
   }

二、依賴屬性(Dependency Property)

​ 例項中每個 CLR 都包裝著一個非靜態的欄位(或者說由一個非靜態的欄位在後臺支援)。如果一個 TextBox 有 100 個屬性,每個屬性都包裝著一個 4 byte 的欄位,那如果程式執行建立 10000 個 TexBox 時,屬性將佔用 100*4**10000≈3.8M 的記憶體。在這 100 個屬性中,最常用的是 Text 屬性,這意味著大多數的記憶體都會被浪費掉。為了解決此問題,WPF 推出了依賴屬性。

依賴屬性(Dependency Property),就是一種可以自己沒有值,但能通過 Binding 從資料來源獲得值(依賴在別人身上)的屬性。擁有依賴屬性的物件被稱為“依賴物件”。

​ WPF 中允許物件在被建立的時候並不包含用於儲存資料的空間(即欄位所佔用的空間)、只保留在需要用到資料時能夠獲得預設值、借用其他物件資料或實時分配空間的能力——這種物件被稱為“依賴物件(Dependency Object)”,這種實時獲取資料的能力依靠依賴屬性(Dependency Property)來實現。

​ WPF 中,必須使用依賴物件作為依賴屬性的宿主,使二者結合起來,才能形成完整的 Binding 目標被資料所驅動。依賴物件的概念由 DependencyObject 類實現,依賴屬性的概念由 DependencyProperty 類實現。DependencyObject 類具有 GetValue 和 SetValue 兩個方法。具體實現一個依賴屬性如下圖所示(在 Visual Studio 中可以使用 “propdp” 按 Tab 鍵快捷生成):

 public class StudentObject : DependencyObject
    {
        // CLR包裝
        public int MyProperty
        {
            get { return (int)GetValue(MyPropertyProperty); }
            set { SetValue(MyPropertyProperty, value); }
        }

        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MyPropertyProperty =
            DependencyProperty.Register("MyProperty", typeof(int), typeof(StudentObject), new PropertyMetadata(0));

    }

​ WPF 中控制元件的屬性大多數都為依賴屬性,例如,Window 窗體的Title 屬性,我們檢視程式碼後如下:

 /// <summary>獲取或設定視窗的標題。</summary>
    /// <returns>
    ///   一個 <see cref="T:System.String" /> ,其中包含視窗的標題。
    /// </returns>
    [Localizability(LocalizationCategory.Title)]
    public string Title
    {
      get
      {
        this.VerifyContextAndObjectState();
        return (string) this.GetValue(Window.TitleProperty);
      }
      set
      {
        this.VerifyContextAndObjectState();
        this.SetValue(Window.TitleProperty, (object) value);
      }
    }

​ WPF 中控制元件的繼承關係: Control -----> FrameworkElement -----> UIElment -----> Visual -----> DependencyObject 。即 WPF 中所有 UI 控制元件都是依賴物件,UI 控制元件的大多數屬性都已經依賴化了。

​ 當我們為依賴屬性新增 CLR 包裝時,就相當於為依賴物件準備了暴露資料的 Binding Path,即該依賴物件具備扮演資料來源(Source)和資料目標(Target)的能力。該依賴物件雖然沒有實現 INotifyPropertyChanged 介面,但當屬性的值發生改變的時候與之關聯的 Binding 物件依然可以得到通知,依賴屬性預設帶有這樣的功能,具體如下:

​ 我們宣告一個自定義控制元件,控制元件的依賴屬性為 DisplayText:

 public class MorTextBox : TextBox
    {
        public string DipalyText
        {
            get { return (string)GetValue(DipalyTextProperty); }
            set { SetValue(DipalyTextProperty, value); }
        }

        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DipalyTextProperty =
            DependencyProperty.Register("DipalyText", typeof(string), typeof(MorTextBox), new PropertyMetadata(""));

        public new BindingExpressionBase SetBinding(DependencyProperty dp, BindingBase binding)
        {
            return BindingOperations.SetBinding(this, dp, binding);
        }
    }

​ 我們先把該依賴屬性作為資料目標獲取第一個 TextBox 的 Text 屬性,然後把自己的 DisplayText 依賴屬性的值作為第二個 TextBox 的 Text 屬性的資料來源:

  <StackPanel>
        <TextBox Margin="5" Height="50" x:Name="t1"></TextBox>
        <!--自定義依賴屬性作為 Target-->
        <local:MorTextBox x:Name="t2" Visibility="Collapsed" DipalyText="{Binding ElementName=t1,Path=Text,UpdateSourceTrigger=PropertyChanged}"></local:MorTextBox>
        <!--自定義依賴屬性作為 Source-->
        <local:MorTextBox Margin="5" Height="50"  Text="{Binding ElementName=t2,Path=DipalyText,UpdateSourceTrigger=PropertyChanged}"></local:MorTextBox>
    </StackPanel>

​ 當我們執行程式後,第二個 TextBox 的數值隨著第一個 TextBox 數值的改變而改變。

三、附加屬性(Attached Properties)

​ 實際開發中,我們會經常遇到這樣的情況,一個人在學校的時候需要記錄班級等資訊,在公司需要記錄職業等資訊,那麼如果我們在設計 Human 類的時候,在類裡面直接定義 Grade、Position 屬性合適嗎?

​ 顯然不合適!首先,當我們在學校上學的時候完全用不到公司等資訊,那麼Position 所佔的記憶體就被浪費了。為了解決此問題,我們首先想到依賴屬性,但解決了記憶體浪費問題,還存在一個問題,即一旦流程改變,那麼 Human 類就需要做出改動,例如:當我們乘車的時候,有車次資訊;去醫院看病的時候,有排號資訊等。這意味著應用場景的不斷變化,導致我們所屬的資訊不斷髮生變化。為了解決此問題,.NET 推出了附加屬性(Attached Properties)。

附加屬性(Attached Properties)是說一個屬性本來不屬於某個物件,但由於某種需求而被後來附加上。也就是說把物件放入一個特定環境後物件才具有的屬性(表現出來就是被環境賦予的某種屬性)。上述例子,我們可以使用附加屬性去解決這個問題(新增附加屬性時,可以在 Visual studio 中輸入 "propa" 然後按 Tab 鍵快捷生成):

 class Human : DependencyObject
    {
        public string Name { get; set; }

        public int Age { get; set; }
    }

    class School : DependencyObject
    {
        public static string GetGrade(DependencyObject obj)
        {
            return (string) obj.GetValue(GradeProperty);
        }

        public static void SetGrade(DependencyObject obj, string value)
        {
            obj.SetValue(GradeProperty, value);
        }

        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty GradeProperty =
            DependencyProperty.RegisterAttached("Grade", typeof(string), typeof(School), new PropertyMetadata(""));
    }

    class Company : DependencyObject
    {
        public static string GetPosition(DependencyObject obj)
        {
            return (string) obj.GetValue(PositionProperty);
        }

        public static void SetPosition(DependencyObject obj, string value)
        {
            obj.SetValue(PositionProperty, value);
        }

        // Using a DependencyProperty as the backing store for Position.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PositionProperty =
            DependencyProperty.RegisterAttached("PPosition", typeof(string), typeof(Company), new PropertyMetadata(""));
    }

​ 使用依賴屬性的方式如下:

            // Attached Properties
            {
                Human human0 = new Human() { Name = "John", Age = 10, };
                School.SetGrade(human0, "四年級二班");
                Human human1 = new Human() { Name = "Andy", Age = 26, };
                Company.SetPosition(human1, "軟體工程師");
                Human human2 = new Human() { Name = "Kolity", Age = 25, };
                Company.SetPosition(human2, "產品經理");
                TextBoxAttached.Text += $"{human0.Name},{human0.Age},{School.GetGrade(human0)}\r\n";
                TextBoxAttached.Text += $"{human1.Name},{human1.Age},{Company.GetPosition(human1)}\r\n";
                TextBoxAttached.Text += $"{human2.Name},{human2.Age},{Company.GetPosition(human2)}\r\n";
            }

​ 輸出結果,如下所示:

John,10,四年級二班
Andy,26,軟體工程師
Kolity,25,產品經理

​ 從附加屬性的實現中,我們可以看出附加屬性(Attached Properties)的本質就是依賴屬性(Dependency Property)。附加屬性通過宣告與依賴屬性相關的 Get 與 Set 方法實現寄宿在宿主類(例如:Human)上,這意味宿主類也必須實現 DependencyObject 類。

​ 其實,WPF 控制元件的佈局控制元件的許多屬性就為附加屬性,例如:當把一個 TextBox 放入 Grid中時,對於 TextBox 而言我們可以使用 Grid 的 Row 和 Column 屬性,如下:

        <Grid >
            <TextBox Grid.Row="0" Grid.Column="0"></TextBox>
        </Grid>

​ 放入 Canvas 中,可以使用 Canvas 的 Left 等附加屬性:

        <Canvas>
            <TextBox Canvas.Left="0" Canvas.Right="100" Canvas.Bottom="20" Canvas.Top="8"></TextBox>
        </Canvas>

​ 附加屬性(Attached Properties)的本質是依賴屬性(Dependency Property),因此,附加屬性也可以使用 Binding 依賴在其他物件的資料上,例如:我們通過兩個 Slider 來控制矩形在 Canvas 中的橫縱座標:

  <Canvas x:Name="c1">
            <Slider x:Name="s1" Width="200" Height="50" Canvas.Top="10" Canvas.Left="50" Maximum="300"></Slider>
            <Slider x:Name="s2" Width="200" Height="50" Canvas.Top="40" Canvas.Left="50" Maximum="400"></Slider>
            <Rectangle Fill="CadetBlue" Width="30" Height="30" Canvas.Left="{Binding ElementName=s1,Path=Value}" Canvas.Top="{Binding ElementName=s2,Path=Value}"></Rectangle>
        </Canvas>

相關文章