5. 完整的自定義依賴屬性 5.1 定義 以上代碼為一個相對完成的依賴屬性例子(還有一些功能比較少用就不寫出了),從這段代碼可以看出,自定義依賴屬性的步驟如下: 1. 註冊依賴屬性並生成依賴屬性標識符。依賴屬性標識符為一個public static readonly DependencyProper ...
5. 完整的自定義依賴屬性
5.1 定義
/// <summary>
/// 標識 Title 依賴屬性。
/// </summary>
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string), typeof(MyPage), new PropertyMetadata(string.Empty));
/// <summary>
/// 獲取或設置Content的值
/// </summary>
public object Content
{
get { return (object)GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
/// <summary>
/// 標識 Content 依賴屬性。
/// </summary>
public static readonly DependencyProperty ContentProperty =
DependencyProperty.Register("Content", typeof(object), typeof(MyPage), new PropertyMetadata(null, OnContentChanged));
private static void OnContentChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
MyPage target = obj as MyPage;
object oldValue = (object)args.OldValue;
object newValue = (object)args.NewValue;
if (oldValue != newValue)
target.OnContentChanged(oldValue, newValue);
}
protected virtual void OnContentChanged(object oldValue, object newValue)
{
}
以上代碼為一個相對完成的依賴屬性例子(還有一些功能比較少用就不寫出了),從這段代碼可以看出,自定義依賴屬性的步驟如下:
註冊依賴屬性並生成依賴屬性標識符。依賴屬性標識符為一個public static readonly DependencyProperty欄位,在上面這個例子中,依賴屬性標識符為ContentProperty。依賴屬性標識符的名稱必須為“屬性名+Property”。在PropertyMetadata中指定屬性預設值。
實現屬性包裝器。為屬性提供 CLR get 和 set 訪問器,在Getter和Setter中分別調用GetValue和SetValue。Getter和Setter中不應該有其它任何自定義代碼。
註意: Setter中不要寫其它任何自定義代碼這點很重要,如果使用Binding或其它XAML中賦值的方式,程式並不會使用Setter,而是直接調用SetValue函數賦值。這也是為什麼需要使用一個PropertyChangedCallback統一處理所有值變更事件,而不是直接寫在Setter裡面。
如果需要監視屬性值變更。可以在PropertyMetadata中定義一個PropertyChangedCallback方法。因為這個方法是靜態的,可以再實現一個同名的實例方法(可以參考ContentControl的OnContentChanged方法)。
5.2 代碼段
註冊依賴屬性的語法比較難記,可以使用VisualStudio自帶的代碼段propdp(輸入propdp後按兩次tab)自動生成,這個代碼段生成的代碼只有基本功能,如下所示:
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(MyPage), new PropertyMetadata(0));
要生成完整的依賴屬性代碼,可以使用自定義的代碼段,以下代碼段生成的就是完整的依賴屬性定義,快捷鍵是dp:
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Keywords>
<Keyword>dp</Keyword>
</Keywords>
<SnippetTypes>
<SnippetType>SurroundsWith</SnippetType>
</SnippetTypes>
<Title>Dependency Property</Title>
<Author>dino.c</Author>
<Description>For Dependency Property</Description>
<HelpUrl>
</HelpUrl>
<Shortcut>dp</Shortcut>
</Header>
<Snippet>
<References>
<Reference>
<Assembly>
</Assembly>
</Reference>
</References>
<Declarations>
<Literal Editable="true">
<ID>int</ID>
<ToolTip>int</ToolTip>
<Default>int</Default>
<Function>
</Function>
</Literal>
<Literal Editable="true">
<ID>MyProperty</ID>
<ToolTip>屬性名</ToolTip>
<Default>MyProperty</Default>
<Function>
</Function>
</Literal>
<Literal>
<ID>defaultvalue</ID>
<ToolTip>預設值</ToolTip>
<Default>0</Default>
</Literal>
<Literal Editable="false">
<ID>classname</ID>
<ToolTip>類名</ToolTip>
<Function>ClassName()</Function>
<Default>ClassNamePlaceholder</Default>
</Literal>
</Declarations>
<Code Language="csharp" Kind="method body">
<![CDATA[
/// <summary>
/// 獲取或設置$MyProperty$的值
/// </summary>
public $int$ $MyProperty$
{
get { return ($int$)GetValue($MyProperty$Property); }
set { SetValue($MyProperty$Property, value); }
}
/// <summary>
/// 標識 $MyProperty$ 依賴屬性。
/// </summary>
public static readonly DependencyProperty $MyProperty$Property =
DependencyProperty.Register("$MyProperty$", typeof($int$), typeof($classname$), new PropertyMetadata($defaultvalue$,On$MyProperty$Changed));
private static void On$MyProperty$Changed(DependencyObject obj,DependencyPropertyChangedEventArgs args)
{
$classname$ target= obj as $classname$;
$int$ oldValue = ($int$)args.OldValue;
$int$ newValue = ($int$)args.NewValue;
if (oldValue != newValue)
target.On$MyProperty$Changed(oldValue, newValue);
}
protected virtual void On$MyProperty$Changed($int$ oldValue,$int$ newValue)
{
}]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
6. Slider與OneWayBinding的"Bug"
UWP的依賴屬性比起WPF有了大幅簡化,需要學習的地方少了很多,但是功簡化了也不一定是一件好事。譬如下麵這個代碼:
<StackPanel>
<Slider x:Name="SliderSource"
Maximum="200" />
<Slider x:Name="SliderTarget"
Maximum="100"
Value="{Binding ElementName=SliderSource,Path=Value,Mode=OneWay}" />
</StackPanel>
理想的情況下,拖動SliderSource到100以後,因為SliderTarget的Value已經超過Maximum設置的100,所以沒有反應;再次把SliderSource拖動到100以下,SliderTarget才會重新跟隨SliderSource改變。但實際上,之後無論再怎麼拖動SliderSource,SliderTarget都不會有反應。
估計所有繼承自RangeBase的控制項都會有這個BUG,如果要寫一個RangeBase控制項(包含Value,Minimum,Maximum三個double值的控制項,Value必須在後兩個值的範圍之間),這是個很讓人煩惱的問題。既然現在知道Value會被Maximum及Minimum約束,那麼就可以猜想到問題出在ValueProperty的PropertyChangedCallback函數中。產生這個BUG的原因可以參考下麵的代碼。
private static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
RangeBase range = d as RangeBase;
double minimum = range.Minimum;
double maximum = range.Maximum;
double value = (double)e.NewValue;
double initialVal = (double)e.OldValue;
if (value < minimum)
{
SetValue(ValueProperty, minimum);
}
if (value > maximum)
{
SetValue(ValueProperty, maximum);
}
range.OnValueChanged(initialVal, value);
}
實際的代碼要複雜些,但基本的邏輯就是這樣。如果新的Value值超過了Maximum或Minimum,就將Value重新設置為Maximum或Minimum,保證Value不會超過設定的範圍。在這個例子里,如果在這個函數開頭的位置調用 range.ReadLocalValue(range.ValueProperty),返回的是一個Binding,在結尾的位置調用,返回的則是double類型的100,因為這段代碼將Value由OneWay Binding覆蓋為maximum的double值了。
在WPF中,這個問題並不存在,因為WPF的依賴屬性可以使用CoerceValueCallback約束屬性值,而UWP的依賴屬性被簡化了,缺少這個功能。可以在網上用“Silverlight CoerceValue Helper”或“Silverlight CoerceValue Utils”等關鍵字試試搜索一些解決方案。為什麼使用Silverlight的關鍵字來搜索?因為Silverlight同樣存在這個問題。雖然網上能找到不少解決方案,但以我的經驗來說沒有方案能很好地解決這個問題。最後我的解決方案如下:
/// <summary>
/// 不要使用OneWayBinding,只能使用TwoWayBinding
/// </summary>
public object MyProperty
{
get { return (object)GetValue(MyPropertyProperty); }
set { SetValue(MyPropertyProperty, value); }
}
反正不是寫控制項庫給別人用,寫個註釋並且和同事打聲招呼就算了。
有興趣的話可以參考Silverlight RangeBase的源代碼,由於Silverlight和UWP比較接近,參考Silverlight的源碼基本就可以理解RangeBase的實現細節。
RangeBase.cs
這個是Silverlight的開源實現Moonlight的源碼,Moonlight的源碼對理解UWP、Silverlight都很有參考價值。順便一提,Silverlight的依賴屬性參考文檔也比UWP的依賴屬性參考文檔好用一些。
提示: 為什麼使用TwoWay Binding可以解決這個問題?OneWay Binding和TwoWay Binding的內部行為不同。使用OneWay Binding的情況下,給SliderTarget.Value設置一個值,意思就只是SliderTarget的Value需要設置成一個新的值,捨棄了之前的Binding。在TwoWay Binding的情況下,設置一個值的意思不止是Value會成為那個新的值,同時綁定的對象也會更新成這個值,TwoWay Binding 理所當然地不能被捨棄。
7.參考
依賴屬性概述
自定義依賴屬性
Silverlight 依賴項屬性概述