依賴屬性並不是一種語言層面的 "屬性",而是一種 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 概念。附加屬性通常用於界面元素的布局設置。這樣一種特殊的擴展機制,使得父元素可以將一些自定義設置傳遞給所有子元素,而無需要求子元素必須具備相同的依賴屬性。這種應用方式有點擴展方法的意思,初次接觸時的確不太好理解。