WPF 學習筆記 - 5. DependencyProperty

 依賴屬性並不是一種語言層面的 "屬性",而是一種 WPF 提供的 "功能"。它在 CLR Property 的基礎上封裝了一些內在的行爲,使得基於聲明式的 XAML 具備更強大的動作操控能力,很顯然這比使用程序設計代碼編寫行爲事件要簡便和自然得多。


依賴屬性的特點:

(1) 使用高效的稀疏存儲系統,這意味着在不設置本地值的情況下,所有同類型對象的依賴屬性都將共享默認設置,大大節約內存開銷。
(2) 依賴屬性具備變更通知(Change Notification)能力,當屬性值發生變化時,可以通過預先註冊的元數據信息觸發聯動行爲。
(3) 依賴屬性可以從其在樹中的父級繼承屬性值。
(4) 依賴屬性可以依據優先級從多個提供程序中獲取最終值。

1. 依賴屬性實現

依賴屬性的實現很簡單:

(1) 所在類型繼承自 DependencyObject,幾乎所有的 WPF 控件都間接繼承自該類型。
(2) 使用 public static 聲明一個 DependencyProperty,該字段纔是真正的依賴屬性 (字段)。
(3) 在靜態構造中完成依賴屬性的元數據註冊,並獲取對象引用。
(4) 提供一個依賴屬性的實例化包裝屬性。注意使用 DependencyObject 相關方法作爲讀取/訪問器。

public class MyClass : DependencyObject
{
  public static readonly DependencyProperty TestProperty;

  static MyClass()
  {
    TestProperty = DependencyProperty.Register("Test", typeof(string), typeof(MyClass),
      new PropertyMetadata("Hello, World!", OnTestChanged));
  }

  private static void OnTestChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
  {
    
  }

  public string Test
  {
    get { return (string)GetValue(TestProperty); }
    set { SetValue(TestProperty, value); }
  }
}


提示: 在 VS2008 中可以使用 "propdp + TAB" 快速生成依賴屬性代碼。

2. 變更通知

當依賴屬性值發生變化時,WPF 會通過預先註冊的元數據 (Metadata) 信息完成某些 "關聯行爲" 調用。這樣我們就可以在 XAML 的聲明中完成行爲控制,比如開始或停止動畫。

我們試着將上面我們創建的自定義依賴屬性作爲源綁定給相關控件。

public partial class Window1 : Window
{
  MyClass o;

  public Window1()
  {
    InitializeComponent();

    o = new MyClass();

    var binding = new Binding("Test") { Source = o, Mode = BindingMode.TwoWay };
    this.textBox1.SetBinding(TextBox.TextProperty, binding);
  }

  private void btnTest_Click(object sender, RoutedEventArgs e)
  {
    o.Test = DateTime.Now.ToString();
  }
}


窗體初始化時,textBox1.Text 自動綁定爲 MyClass.TestProperty 的默認值 "Hello, World!" (參考 MyClass 靜態構造的元數據註冊代碼)。每當我們單擊按鈕修改依賴屬性(o.Test)時,textBox1.Text 都將自動同步更新,這就是依賴屬性的行爲方式。當然,我們還應該提供一個完全基於 XAML 的聲明方式演示,而不是上面的 C# 代碼。

<Window x:Class="Learn.WPF.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1">
  <Grid>
    <TextBox x:Name="textBox1" />
    <Label x:Name="label1" Content="{Binding ElementName=textBox1, Path=Text}" />
  </Grid>
</Window>


label1.Content 綁定到 textBox1.Text 這個依賴屬性上,每當我們修改 textBox1.Text 時,label1.Content 都會保持同步修改,這些動作無需我們編寫任何代碼。很顯然,這大大簡化了 XAML 的行爲控制能力,尤其是對所謂富功能 (Rich functionality) 的控制。

WPF 提供了一種稱之爲 "觸發器(Trigger)" 的機制來配合依賴屬性工作,我們用一個簡單的屬性觸發器看看效果。

<Window x:Class="Learn.WPF.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1">
  <Grid>
    <Button x:Name="btnTest">
      <Button.Style>
        <Style TargetType="{x:Type Button}">
          <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
              <Setter Property="Foreground" Value="Red" />
            </Trigger>
          </Style.Triggers>
        </Style>
      </Button.Style>
    </Button>
  </Grid>
</Window>


當依賴屬性 Button.IsMouseOver 發生變化時 (== True),將導致觸發器執行,設置 Foreground=Red。很顯然這比我們處理 MouseEnter 事件要簡單得多,關鍵是 UI 設計人員無需編寫代碼即可得到所需的效果。除了屬性觸發器外,WPF 還提供了數據觸發器和事件觸發器。

3. 屬性值繼承

此繼承非 OOP 上的繼承,它的本意是父元素的相關設置會自動傳遞給所有層次的子元素 (元素可以從其在樹中的父級繼承依賴項屬性的值)。其實很簡單也很熟悉,當我們修改窗體父容器控件的字體設置時,所有級別的子控件都將自動使用該字體設置(未做自定義設置),相信你在 WinForm 中已經使用過了。

<Window x:Class="Learn.WPF.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1" FontSize="20">
  <Grid>
    <TextBox x:Name="textBox1" />
    <Label x:Name="label1" Content="Hello, World!" />
    <Button x:Name="btnTest" Content="Test" />
  </Grid>
</Window>


Window.FontSize 設置會影響所有的內部元素字體大小,這就是所謂的屬性值繼承。當然,一旦子元素提供了顯式設置(比如下例中的 label1.FontSize),這種繼承就會被打斷。

<Window x:Class="Learn.WPF.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1" FontSize="20">
  <Grid>
    <TextBox x:Name="textBox1" />
    <Label x:Name="label1" Content="Hello, World!" FontSize="10" />
    <Button x:Name="btnTest" Content="Test" />
  </Grid>
</Window>


注意並不是所有的依賴屬性都會繼承父元素的設置。

4. 多提供程序優先級 

WPF 允許我們可以在多個地方設置依賴屬性的值,這的確很方便,但也問題也不少。比如下面的例子中,我們在三個地方設置了按鈕的背景顏色,只是最後哪個會起作用呢?

<Button x:Name="button1" Background="Red">
  <Button.Style>
    <Style TargetType="{x:Type Button}">
      <Setter Property="Background" Value="Green"/>
      <Style.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
          <Setter Property="Background" Value="Blue" />
        </Trigger>
      </Style.Triggers>
    </Style>
  </Button.Style>
  Click
</Button>


確切的答案是 "<Button Background='Red'>" 起作用了,爲什麼呢?因爲 WPF 內在的優先級規則決定了 "本地值" 優先級別最高。

本地值 > 樣式觸發器 > 模板觸發器 > 樣式設置程序 > 主題樣式觸發器 > 出題樣式設置程序 > 屬性值繼承 > 默認值


這樣的一個過程被稱之爲 "基礎值判斷"。我們需要特別說名一下,所謂本地值是指我們直接或間接調用了 DependencyObject.SetValue,也就是顯示設置了依賴屬性的值。我們可以用下面這樣的代碼清除本地值設置。

this.button1.ClearValue(Button.BackgroundProperty);


雖然我們獲取了基礎值,但事情並沒有結束,接下來有幾個更厲害的選手出場,他們的優先級別更高,依賴屬性必須一一過關纔算最後確定下來。

基礎值判斷 -> 表達式計算 -> 應用動畫 -> 限制(Coerce) -> 驗證 -> 最終結果


(1) 驗證是指我們註冊依賴屬性所提供的 ValidateValueCallback 委託方法,它最終決定了屬性值設置是否有效;
(2) 限制則是註冊時提供的 CoerceValueCallback 委託,它負責驗證屬性值是否在允許的限制範圍之內,比如大於等於 9 小於等等 100;
(3) 動畫是一種特殊行爲,它的優先級高於基礎設置也能理解;
(4) 如果依賴屬性值是計算表達式 (System.Windows.Expression,比如前面示例中的綁定語法),那麼 WPF 會嘗試 "計算" 表達式的結果。
(5) 基礎值就是上面提供的那些顯示設置,它的優先級比較好確定。

5. 附加屬性

附加屬性是一種特殊的依賴屬性,它看上去頗爲古怪,尤其是對我們這些熟悉了面向對象規則的程序員而言。看下面的例子。

<DockPanel>
  <CheckBox DockPanel.Dock="Top">Hello</CheckBox>
</DockPanel>


DockPanel.Dock 是 DockPanel 中定義的依賴屬性,但卻出現在子元素的聲明上,看上去很詭異。

<StackPanel TextElement.FontSize="30" TextElement.FontStyle="Blod">
  <Button>Help</Button>
  <Button>OK</Button>
</StackPanel>


TextElement.FontSize, TextElement.FontStyle 既不屬於 StackPanel,也不屬於 Button,但卻能完成元素樹的字體定義。

附加屬性嚴格來說是一個 XAML 概念,依賴屬性是 WPF 概念。附加屬性通常用於界面元素的佈局設置。這樣一種特殊的擴展機制,使得父元素可以將一些自定義設置傳遞給所有子元素,而無需要求子元素必須具備相同的依賴屬性。這種應用方式有點擴展方法的意思,初次接觸時的確不太好理解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章