四、 只讀依賴屬性
在以前在對於非WPF的功能來說,對於類的屬性的封裝中,經常會對那些希望暴露給外界只讀操作的字段封裝成只讀屬性,同樣在WPF中也提供了只讀屬性的概念,如一些 WPF控件的依賴屬性是只讀的,它們經常用於報告控件的狀態和信息,像IsMouseOver等屬性, 那么在這個時候對它賦值就沒有意義了。 或許你也會有這樣的疑問:為什么不使用一般的.Net屬性提供出來呢?一般的屬性也可以綁定到元素上呀?這個是由於有些地方必須要用到只讀依賴屬性,比如 Trigger等,同時也因為內部可能有多個提供者修改其值,所以用.Net屬性就不能完成天之大任了。
那么一個只讀依賴屬性怎么創建呢?其實創建一個只讀的依賴屬性和創建一個一般的依賴屬性大同小異。不同的地方就是DependencyProperty.Register變成了DependencyProperty.RegisterReadOnly。和前面的普通依賴屬性一樣,它將返回一個 DependencyPropertyKey。而且只提供一個GetValue給外部,這樣便可以像一般屬性一樣使用了,只是不能在外部設置它的值罷了。
下面我們就用一個簡單的例子來概括一下:
public partial class WindowReadOnly : Window
{
public WindowReadOnly ()
{
InitializeComponent();
//用SetValue的方法來設置值
DispatcherTimer timer =
new DispatcherTimer(TimeSpan.FromSeconds(1),
DispatcherPriority.Normal,
(object sender, EventArgs e)=>
{
int newValue = Counter == int.MaxValue ? 0 : Counter + 1;
SetValue(counterKey, newValue);
},
Dispatcher);
}
//屬性包裝器,只提供GetValue
public int Counter
{
get { return (int)GetValue(counterKey.DependencyProperty); }
}
//用RegisterReadOnly來代替Register來注冊一個只讀的依賴屬性
private static readonly DependencyPropertyKey counterKey =
DependencyProperty.RegisterReadOnly("Counter",
typeof(int),
typeof(WindowReadOnly),
new PropertyMetadata(0));
}
XAML代碼:
<Window x:Name="winReadOnly" x:Class="WpfApp1.WindowReadOnly"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WindowDepend" Height="300" Width="300">
<Grid>
<Viewbox>
<TextBlock Text="{Binding ElementName=winReadOnly, Path=Counter}" />
</Viewbox>
</Grid>
</Window>
效果如下圖所示:

五、 附加屬性
現在我們再繼續探討另外一種特殊的依賴屬性——附加屬性。附加屬性是一種特殊的依賴屬性。這是WPF的特性之一,通俗的理解起來就是,別人有的屬性,由於你跟他產生了關系所以你也有了這個屬於他的屬性。
附加屬性是說一個屬性本來不屬於某個對象,但由於某種需求而被后來附加上,也就是把對象放入一個特定環境后對象才具有的屬性就稱為附加屬性,附加屬性的作 用就是將屬性與數據類型解耦,讓數據類型的設計更加靈活,舉例,一個TextBox被放在不同的布局容器中時就會有不同的布局屬性,這些屬性就是由布局容 器為TextBox附加上的,附加屬性的本質就是依賴屬性,二者僅僅在注冊和包裝器上有一點區別。
附加屬性是依賴屬性的一種特殊形式,它可以讓用戶在一個元素中設置其他元素的屬性。一般來說,附加屬性是用於一個父元素定位其他元素布局 的。就像Grid和DockPanel元素就包含附加屬性。Grid使用附加屬性來指定包含子元素的特定行和列,而DockPanel使用附加屬性是來指 定子元素應該停靠在面板中的何處位置。
附加屬性就是自己沒有這個屬性,在某些上下文中需要就被附加上去。比如StackPanel的Grid.Row屬性,如果我們定義StackPanel類時定義一個Row屬性是沒有意義的,因為我們並不知道一定會放在Grid里,這樣就造成了浪費。
例如,下面轉場控件的定義使用了Grid的Row屬性來將自身定位到特定的行中。
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="101*"/>
<RowDefinition Height="80"/>
<RowDefinition Height="80"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" >
盡管對於一個普通的WPF開發人員來說,理解依賴和附加屬性並不一定是必須的,但是掌握好WPF系統的整個運行機制對於提升WPF應用技術是非常重要的。
使用附加屬性,可以避開可能會防止一個關系中的不同對象在運行時相互傳遞信息的編碼約定。一定可以針對常見的基類設置屬性,以便每個對象只需獲取和 設置該屬性即可。但是,你可能希望在很多情況下這樣做,這會使你的基類最終充斥着大量可共享的屬性。它甚至可能會引入以下情況:在數百個后代中,只有兩個 后代嘗試使用一個屬性。這樣的類設計很糟糕。為了解決此問題,我們使用附加屬性概念來允許對象為不是由它自己的類結構定義的屬性賦值。在創建對象樹中的各 個相關對象之后,在運行時從子對象讀取此值。
最好的例子就是布局面板。每一個布局面板都需要自己特有的方式來組織它的子元素。如Canvas需要Top和left來布 局,DockPanel需要Dock來布局。當然你也可以寫自己的布局面板(在上一篇文章中我們對布局進行了比較細致的探討,如果有不清楚的朋友也可以再 回顧一下)。
下面代碼中的Button 就是用了Canvas的Canvas.Top和Canvas.Left="20" 來進行布局定位,那么這兩個就是傳說中的附加屬性。
<Canvas>
<Button Canvas.Top="20" Canvas.Left="20" Content="Knights Warrior!"/>
</Canvas>
定義附加屬性的方法與定義依賴屬性的方法一致,前面我們是使用DependencyProperty.Register來注冊一個依賴屬性,只是在注冊屬性時使用的是RegisterAttach()方法。這個RegisterAttached的參數和 Register是完全一致的,那么Attached(附加)這個概念又從何而來呢?
其實我們使用依賴屬性,一直在Attached(附加)。我們注冊(構造)一個依賴屬性,然后在DependencyObject中通過 GetValue和SetValue來操作這個依賴屬性,也就是把這個依賴屬性通過這樣的方法關聯到了這個DependencyObject上,只不過是 通過封裝CLR屬性來達到的。那么RegisterAttached又是怎樣的呢?
下面我們來看一個最簡單的應用:首先我們注冊(構造)一個附加屬性
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
namespace WpfApp1.Services
{
public class TurnoverManager : DependencyObject
{
//通過靜態方法的形式暴露讀的操作
public static double GetAngle(DependencyObject obj)
{
return (double)obj.GetValue(AngleProperty);
}
//通過靜態方法的形式暴露寫的操作
public static void SetAngle(DependencyObject obj, double value)
{
obj.SetValue(AngleProperty, value);
}
//通過使用RegisterAttached來注冊一個附加屬性
public static readonly DependencyProperty AngleProperty =
DependencyProperty.RegisterAttached("Angle", typeof(double), typeof(TurnoverManager), new PropertyMetadata(0.0, OnAngleChanged));
//根據附加屬性中的值,當值改變的時候,旋轉相應的角度。
private static void OnAngleChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var element = obj as UIElement;
if (element != null)
{
element.RenderTransformOrigin = new Point(0.5, 0.5);
element.RenderTransform = new RotateTransform((double)e.NewValue);
}
}
}
}
然后,我們在程序中使用這個我們自己定義的附加屬性
<Window x:Class="WpfApp1.WindowTurnover"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp1.Services"
Title="WindowTurnover" Height="400" Width="500" Loaded="Window_Loaded">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="313*"/>
<RowDefinition Height="57*"/>
</Grid.RowDefinitions>
<Canvas Grid.Row="0">
<Ellipse Name="ellipseRed" Fill="Red" Width="100" Height="60" Canvas.Left="56"
Canvas.Top="98" local:TurnoverManager.Angle="{Binding ElementName=sliderAngle, Path=Value}"/>
<Rectangle Name="ellipseBlue" Fill="Blue" Width="80" Height="80" Canvas.Left="285"
Canvas.Top="171" local:TurnoverManager.Angle="45" />
<Button Name="btnWelcome" Content="歡迎光臨" Canvas.Left="265" Canvas.Top="48" FontSize="20" local:TurnoverManager.Angle="60"/>
</Canvas>
<WrapPanel Grid.Row="1">
<Label Content="角度大小" />
<Slider x:Name="sliderAngle" Minimum="0" Maximum="240" Width="300" />
</WrapPanel>
</Grid>
</Window>
在XAML中就可以使用剛才注冊(構造)的附加屬性了:如下圖。

通過調整角度值,顯示不同的效果如下兩圖。圖1,圖2。

圖1

圖2

