引言
在看PDC-09大會的視頻時,其中一篇講利用Blend來擴展Silverlight元素的行 為,當時感覺很酷:在Blend中,將MouseDragElementBehavior拖到任意一個元素上,這個元 素就可以被隨意拖動。
因為之前在Silverlight SDK中好像沒有看到相關的介紹,事實如此, Microsoft.Expression.Interactions和System.Windows.Interactivity程序集在Blend工具中才有。但是從來沒有去看過Blend的幫助文檔,因為我以為那不過是告訴你怎樣使用工具,事實上 我錯了,Microsoft Expression Blend 軟件開發工具包(SDK) 沒有包含怎樣使用Blend的信息, 而只有介紹以上提到的兩個程序集。
無知的人第一件會做的事就是上Google,我也是這樣的人。所以趕緊Google一下,原來 Behavior是一種Silverlight元素行為的重用方式,並且每一篇介紹Behavior的作者,無不流露出對Behavior的贊賞,甚至對有些人來講,那是Silverlight最令人激動的特性,因為那使他們可能很方便並且簡單的為自己的界面添加只有少數具有美術創意的人才能做出的元素效果。
於是我決定整理一下Behavior相關的知識。
然而Blend SDK一看卻感到很吃驚:這不是微軟史上最簡單的SDK么!如果排除類庫介紹 這個SDK只有3個頁面,而里面每個例子的代碼加起來還不到50行。更驚訝的是微軟認為這么簡單的東西我卻怎么看也不會做,因為我有一種先天缺陷:在不明白為什么的情況下再簡單的事情我都不會做
Behavior真這么簡單么?事實上不能這么說,從使用的角度,實現一個簡單的Behavior不需要多少代碼,需要做的事情也不多,然而在這背后的思路呢。比如ASP.NET,一般人看幾個小時說就能Hello,World,但是真正做起來卻會遇到很多問題。尤其現在開發工具越來越強大,許多東西不需要我過多理解就能實現,但是我相信,只有真正理解一些東西,我們的技術才會真正提高。
本文從自己的學習角度更深入點的分析這種特性。網上介紹的一般只是教怎么做,很多差不多就是SDK里面的簡單的例子。Behavior特性的背后其實是有比較好的思想的。這些思想有助於我們更好的理解這種特性。 本文不打算寫太多例子,因為例子實在很簡單,可以參考Blend SDK。
Behavior 的設計模式是一種行為模式
我們應該怎樣認識設計模式?
設計模式到底是一種什么樣的東西,每個開發人員有自己的理解。有的人認為那是具有很高技術經驗的人才應該掌握的東西;有的人也認為我們一般的開發中不會用到什么設計模式……. 這些觀點好像想說明這樣一件事情:設計模式是一種可有可無的東西,你有那個興致就可以去看 下,看了記不住也沒有關系。
當然每個人有自己的個人經驗,也許不懂設計模式不會影響你做某些事情,但是明白一種設計模式卻能幫助你更好的做某件事情。其實在我們的開發中,很多技術都從設計模式中抽象了出來,使我們不用去實現這些高度抽象的技術思維。從語言級別,比如C#中的迭代器模式,有一次我面試沒有答上來的委托對應的Observer模式;到框架級別,比如MVC,MVP以及MVVM模 式。Ruby On Rails從2004年出現到現在,一直受到不少開發者的追捧,其實ROR的開發者只不過是用Ruby來實現了MVC的模式,這個框架使其他開發者可以直接利用MVC的模式進行開發,而不必關心怎樣自己去實現這個模式。但是與對照教程做練習相比,理解MVC的思路絕對有助於真正的掌握Ruby On Rails技術。到我們的WPF也是一樣,XAML不僅僅是一個可以編寫界面的方式,它包含的是一種MVP的設計模式,當我們明白這種設計模式的思路時,我相信我們會覺得 XAML就是那么一回事,不然,跟着技術細節跑,你總會有許多的為什么並且不知道為什么。比如WPF有事件模式,為什么又要來個命令模式(Command),其實命令模式也是23種經典模式之一,在WPF中得到支持,看看命令模式再來看WPF中的Command,相信會有不同的感覺。 所以,在我們的學習中,可以將對技術的理解和設計模式一起思考,那樣將有助於我們更深刻的理解技術。我們本可以用更底層的技術來實現很多東西,正是因為一些高級語言和開發工具將一些復雜的設計模式融入到各種設計框架和語言中,才使得我們的開發更加簡單有效。既然 是基於這些抽象的模式之上,那么理解這些不同的模式將有助於我們真正掌握技術的內涵。
重用行為
語言發展到今天,很重要的一個進步就是重用。無論是從語言級別的類,還是COM里的dll,這些技術為技術的發展做出了很大的貢獻。
Behavior也是可重用的一種特性。Expression的網站上有許許多多的Behavior可供下載。Blend本身也提供了幾個,如下圖:
我們只要將一個Behavior拖到一個元素上,就能使這個元素具有某種行為,而實現這種行為的程序集和你的項目是獨立的,你創建的Behavior也可以被其他人使用。我們后面將會討論這是怎樣 實現的。 在行為設計模式中。往往有一下幾個參與者:
•Element :要被附加行為的對象元素(這里也可以是一種邏輯功能的抽象,比如排序)
•IBehavior:行為接口
•Behavior:不同的行為實現 通常,Element所在的程序集和Behavior所在的程序集是不同的,Behavior有多個實現版本,可以將不同的版本附加到Element,就會使Element具有不同的行為。比如這里Element是 一種排序的功能,那么你可以定義一個Behavior是歸並排序,而另一個Behavior是快速排序;所 不同的是行為模式中通常有一種選擇器來決定選取那個Behavior,而WPF采用一種附加的技術將 Behavior附加到一個Element,並且很獨特的是可以附加多個Behavior,后面我們將看到, Behavior中通常用於給元素附加事件,而事件的Add方法類似於多播委托,即使都添加相同的事 件仍然不會影響其他Behavior,事件和委托的一個重要區別就是事件沒有賦值方法(另一個是事 件確保只有事件所在的類才能觸發一個事件通知),這避免不小心取消其他訂閱。所以在 Silverlight中,一個元素可以添加多個Behavior。
不管怎樣,我們看到的是這樣一種思路:你可以為某個元素添加某種獨特的行為,而這種行 為不是直接通過為元素添加事件來實現,因為那樣在其他地方將達不到重用而不得不拷代碼或者 重寫。在漢語中很多相同字開頭的詞往往是近義詞,但我認為重寫和重用是反義詞。而是通過一種附加的技術,將其他程序集的Behavior附加到一個元素,這個意義非常重大,使得我們可無限 擴展並且重用行為。這樣你可以享受別人的成果。那么Silverlight怎樣實現這種附加呢?下一節將會討論。
怎樣將一種行為附加到一個元素呢?
附加,非常可怕的一種技術
當你在自定義一個Behavior的時候,你將決定千千萬萬以后會引用你的Behavior的元素的行為,你可以讓他放大,你可以讓縮小,旋轉,任何你想得出來的創意。他人的添加你的程序集 並將你自定義的附加到一個元素時,這個元素就具有了你定義的行為,想想要是世界上有一種附加技術將一種殺人的行為附加到你身上真是非常可怕。 背后的英雄 : IAttachedObject
Interface這種技術隱含的意義真是無法估量。.Net Framework中就有大量的接口,以下是 IAttachedObject 定義:
An interface for an object that can be attached to another object.
public interfaceIAttachedObject
{
DependencyObject AssociatedObject { get; }
void Attach(DependencyObject dependencyObject);
void Detach();
} 實現這個接口的類的實例可以被附加到另一個對象,如:TriggerBase,TriggerAction, Behavior等。在程序中我們可以調用void Attach(DependencyObject dependencyObject)方 法將繼承該接口的對象附加到另一對象,這里是dependencyObject對象。
通常我們在自定義行為的時候不是直接繼承自IAttachedObject接口,而是繼承自從 IAttachedObject繼承的一些類,這些類都有可重載的OnAttached(),OnDetached()方法,比如 Behavior類,TriggerBase類,TriggerAction類等。通常在Attach()方法中會觸發 OnAttached()方法,在Detach()方法中會觸發OnDetached()方法。
所以Blend SDK中自定義Action,Behavior和Trigger的例子都非常簡單,你只需要重寫 Ontached()方法和OnDetached()方法即可。你可以為AssociatedObject 添加各種事件從 而改變其行為。
以上分析的就是Silverlight中Behavior的機制。所以,本質上,Behavior仍然是利用事件機制來實現的。但是我們通過引入一種不同的設計模式而使得相同的功能更具有擴展性。這進一步說明設計模式的重要性。 后面我們進一步來分析實現方法。
回顧附加屬性:
這里有必要回顧以下附加屬性的概念,這將有助於理解Behavior的附加方式。附加屬性其 實是一種普通的依賴項屬性。並且它由WPF屬性系統管理,不同的是附加屬性被應用到一個非定 義到該屬性的類,附加屬性通過DependencyProperty.RegisterAttached()方法來定義。並 \且該屬性不需要.NET屬性包裝器,因為附加屬性可以被應用與任何附加的對象。看一下Grid的定義:
public class Grid: Panel
{
public static readonly DependencyProperty ColumnProperty;
public static readonly DependencyProperty ColumnSpanProperty;
public static readonly DependencyProperty RowProperty;
public static readonly DependencyProperty RowSpanProperty;
public static readonly DependencyProperty ShowGridLinesProperty;
public Grid();
public ColumnDefinitionCollection ColumnDefinitions { get; }
public RowDefinitionCollection RowDefinitions { get; }
public bool ShowGridLines { get; set; }
protected override sealed Size ArrangeOverride(Size arrangeSize);
public static int GetColumn(FrameworkElement element);
public static int GetColumnSpan(FrameworkElement element);
public static intGetRow(FrameworkElement element);
public static int GetRowSpan(FrameworkElement element);
protected override sealed Size MeasureOverride(Size constraint);
public static void SetColumn(FrameworkElement element, int value);
public static void SetColumnSpan(FrameworkElement element, int value);
public static void SetRow(FrameworkElement element, int value);
public static void SetRowSpan(FrameworkElement element, int value);
} 可以看到上面標記處的Grid的幾個附加屬性,它們沒有.NET屬性包裝器,而通過 GetPropertyName()和SetPropertyName()靜態方法來取值和賦值。
在被附加屬性的元素內部看不到屬性的值,所以也根本不知道它的存在---但是在自己定義 的代碼中可以根據相應的值進行操作。比如設置了Grid.Row屬性后,Grid在計算和排列內部元素 時知道每個元素的這個屬性的值,可以根據此值安排此元素在Grid中的位置,Canvac的 Top,Left,Bottom,Right附加屬性也是如此,只不過Canvas的附加屬性更精確,所以內部計算 更少,因此Canvas的執行效率通常更高。
你甚至可以在不包含Grid的地方使用Grid.Row等附加屬性,但是這沒有絲毫意義,除非你 想在程序中使用這個值。所以你也可以自定義這種附加屬性,比如你想在每個元素上保存一個狀 態值,你在程序中需要用到這個狀態值。我們接下來將看到Silverlight正是通過這種附加屬性的 特性實現了行為的附加
Silverlight 通過Interaction的附加屬性來實現這種附加
實現IAttachedObject的TriggerBase,TriggerAction以及Behavior通過Interaction的附 加屬性Triggers和Behaviors來實現這種附加。
namespace System.Windows.Interactivity
{
public static class Interaction
{
public static readonly DependencyProperty BehaviorsProperty;
public static readonly DependencyProperty TriggersProperty; public static BehaviorCollection GetBehaviors(DependencyObject obj);
public static TriggerCollection GetTriggers(DependencyObject obj);
}
}
通過Reflectot工具我們可以看到BehaviorsProperty和TriggersProperty的聲明:
public static readonly DependencyProperty BehaviorsProperty =
DependencyProperty.RegisterAttached("Behaviors",
typeof(BehaviorCollection),
typeof(Interaction),
new PropertyMetadata( new PropertyChangedCallback(Interaction.OnBehaviorsChanged) )) ;
TriggersProperty的聲明方式類似,不再貼代碼。我們看到其數據類型為BehaviorCollection,並且注冊為附加屬性,在屬性發生改變時會觸發 Interaction.OnBehaviorsChanged方法:
private static void OnBehaviorsChanged(DependencyObjectobj, DependencyPropertyChangedEventArgs args)
{
BehaviorCollection oldValue = (BehaviorCollection) args.OldValue;
BehaviorCollection newValue = (BehaviorCollection) args.NewValue;
if (oldValue != newValue)
{
if ((oldValue != null) && (oldValue.AssociatedObject != null))
{
oldValue.Detach();
}
if ((newValue != null) && (obj != null))
{
if (newValue.AssociatedObject != null)
{
throw new InvalidOperationException(ExceptionStringTable.CannotHostBehaviorCollectionMultiple TimesExceptionMessage);
}
newValue.Attach(obj);
}
}
}
分析此方法,此方法最主要的功勞就是調用BehaviorCollection.Attach()方法。而 BehaviorCollection集合會調用里面每一個子項的Attach()方法。前面我們講過在調用Behavior類的Attach()方法的時候,該方法會調用OnAttached()方法,所以在OnAttached方法里面,我們就可以為元素添加各種事件代碼。而因為事件的Add操作使得其中 一個Behavior添加的相同的事件處理程序不會覆蓋另一個Behavior添加的事件處理程序,從而我 們可以為一個元素添加很多的Behavior。
這里值得注意的是,上面標出的紅色背景的參數obj,在依賴項屬性中 PropertyChangedCallback函數中的第一個參數是定義這個屬性的類本身的實例對象。而對於以 來屬性,這個參數是指被附加了附加屬性的那個對象。這里不明白的話就看不懂上面的代碼。
Behaviors VS Triggers
下面來看下所謂簡單的實例代碼:

看得出這都是Blend提供的程序集才有的命名空間。在實例中我們為Rectangle和Canvas 元素添加了Interaction的兩個附加屬性,這兩個附加屬性是集合類型的,里面添加了一些子項, 比如MouseDragElementBehavior,EventTrigger。
Trigger和Behavior是類似的,而且用法也如此,Triggers的子項也都是繼承自 IAttachedObject接口的類的實例,所不同的是Triggers子項包含Actions屬性,這個屬性是一個 Action集合,每個Action也都是繼承自IAttachedObject接口。都有Attach()方法和 DeTach()方法,並且相應的會觸發OnAttached()和OnDetached()方法。而Action繼承 自Action的子類還包含InVoke()方法。
所以,對於Interaction.Triggers中的每個子項,會制定特定的事件,當該事件觸發會會自動調用Actions屬性中每個子項的InVoke()方法;而對於Interaction.Behaviors中的每個子 項,通常具體對什么事件做處理封裝在每個Behavior中,這種方式更靈活。
對照下面的代碼相信很容易理解兩種的區別: 
上面的類ListBoxItemSendToTop是Blend示例中的ColorSwatchSL中定義的一個Action。 該行為將ListBoxItem中的每個元素添加一種行為:當鼠標經過每個Item時將該元素放大,並且 浮到其他元素的上面。可以參考Blend的實例效果。我們可以參考下面的基礎層次結構,事實上 TriggerAction還繼承自IAttachedObject接口。

這幾行代碼將元素添加附加屬性Interaction.Triggers,並添加一個子項,指明當Loaded事 件發生時觸發ListBoxItemSendToTop的Invoke()方法。不過奇怪的是Blend的實例並沒有按 正確的思路使用,下面是截圖:

從上面的代碼可以看出,對元素行為的附加發生在OnAttached()方法中,而此方法不是 被EventTrigger制定的Loaded事件觸發,而是發生在程序初始化時TriggerCollection.Attach()觸發 EventTrigger.Attach(),一次觸發ListBoxItemSendToTop.Attach()而附加行為的。當這種觸發Loaded事 件的時候什么都沒有做。雖然這些寫程序沒有問題,但是和Triggers的思路卻是不一致的。以上例 子我們可以看出Triggers的執行過程。
為了比較Triggers和Behaviors,我將上面的Triggers實現改為Behaviors實現,代碼如下:

我將SendToTopBehavior聲明為一種Behavior,繼承自Behavior<T>,Behavior<T>的基 礎層次結構和Action類似,同樣它也繼承自IAttachedObject接口:

通過改寫為Behavior,這才是真正通過OnAttached()方法來添加行為,這和前面分析的過程一致。
通過以上比較應該能明白Triggers和Behaviors的區別。Behaviors提供了更靈活的方式,行為響應什么方式將完全有行為自己決定。而Action只提供行為,行為受什么事件觸發則由元素定義中 自己決定。
結論:
Behavior簡單嗎?現在看來它的思路不算復雜。然而從上面的分析過程來看也不算簡單。最主 要的問題,里面涉及到的東西比較多,比如要理解附加屬性。還得對XAML有一定理解,對於 XAML ,它里面的每一個標記都是對象或者對象的屬性。那么既然是對象,我們就可以利用對象初 始化的時候去干一些事情,比如為元素添加事件。這樣我們就能理解上面發生的一切,並且會明 白為什么我們感覺XAML又在做一些我們在后台代碼才能做的事情,認為XAML就是一堆死標記, 這就不能真正理解 XAML 背后的設計模式和意義。
另外,我也從設計模式的角度思考這個問題。這樣更有助於理解。
Blend SDK上兩分鍾就能看完的例子,我花兩天才勉強理解。我在想微軟是不是太高估開發者?還是我太笨了?
System.Windows.EventTrigger VS
System.Windows.Interactivity.EventTrigger 正當准備收筆的時候,才想起Silverlight本身也有觸發器的啊。那里的用法好像可不一樣。
這可吃驚不小,因為很多天沒看Silverlight了,加上一直沒做過項目,概念理解也不是很深。於是趕緊Go To Definition。才發現兩種是有區別的,這里做一下比較: 1 ,命名空間區別:
Silverlight本身的Triggers位於System.Windows命名空間,而Blend提供的Triggers位於 System.Windows.Interactivity命名空間,並且隨Blend一起附帶,與Silverlight SDK是獨立的。 2 ,定義區別:
namespace System.Windows.Interactivity
{
public static class Interaction
{
public static readonly DependencyProperty BehaviorsProperty;
public static readonly DependencyProperty TriggersProperty;
public static BehaviorCollection GetBehaviors(DependencyObject obj);
public static TriggerCollection GetTriggers(DependencyObject obj);
}
} namespace System.Windows
{
public abstract class FrameworkElement : UIElement
{
…...
public TriggerCollectionTriggers{ get; }
….. }
}
從定義上看,兩種是有區別的,這決定了使用方式會有區別。
Silverlight本身的Triggers是一個普通的.NET屬性,而Blend提供的Triggers是一個附加屬性。而我 們轉到TriggerCollection的定義,前者是一個普通的.NET集合類,繼承自 DependencyObject,IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable ,而后者 繼承自 DependencyObjectCollection<T> , IAttachedObject ,具有 Attach(),DeTatch(),OnAttached(),OnDeTached()方法。 3 ,使用方式區別
前者的子項是StoryBoard,通過響應事件來激發動畫,而后者正如前面分析,子項是繼承自 IAttachedObject接口的Action對象,通過響應事件來激發Action的Invoke()方法。 以上簡單的分析可以看出兩者雖然名稱相同,但要功能是不一樣的,Silverlight的Trigger是用 來觸發動畫,動畫是在一段時間內持續的改變元素屬性。而Blend的Trigger更多用來響應鼠標和鍵 盤這種用戶行為,當然你也可以在OnAttached里面來啟動一個動畫,這也是可以的。
總之,Blend提供的Trigger更靈活,功能更豐富,可以這么說,你能做出一切你想得到的用戶交互行為。正如WPF的外觀可以無限定制。雖然SDK是那么的簡單!
上次寫了關於Trigger和Action的文章(Silverlight中的Action和Trigger),這次寫一個Behavior的。Behavior的目的在於封裝部分UI功能,那樣就可以直接應用於元素而不用寫任何代碼。Behavior是一組相關操作的組合,它包含了Trigger和Action的工作。簡單的說就是Trigger和Action的合體。
聽說過Behavior的人一說到Behavior很容易想到拖動效果,這里我就做一個簡單的在Canvas里的拖動行為。創建自定義Behavior需要從Behavior<DependencyObject>繼承,並覆蓋OnAttached和OnDetaching方法(和Trigger差不多)。還是用代碼說話方便。
新建一個Behavior如下:
1: public class Behavior1 : Behavior<Shape>
2: {
3: private Canvas _canvas;
4: private bool _isDraging;
5: private Point _originalMousePosition;
6:
7: public Behavior1()
8: {
9: _isDraging = false;
10: }
11:
12: protected override void OnAttached()
13: {
14: base.OnAttached();
15: this.AssociatedObject.MouseLeftButtonDown += new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonDown);
16: this.AssociatedObject.MouseMove += new MouseEventHandler(AssociatedObject_MouseMove);
17: this.AssociatedObject.MouseLeftButtonUp += new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonUp);
18: }
19:
20: protected override void OnDetaching()
21: {
22: base.OnDetaching();
23: this.AssociatedObject.MouseLeftButtonDown -= new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonDown);
24: this.AssociatedObject.MouseMove -= new MouseEventHandler(AssociatedObject_MouseMove);
25: this.AssociatedObject.MouseLeftButtonUp -= new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonUp);
26: }
27:
28: private void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
29: {
30: _canvas = (Canvas)VisualTreeHelper.GetParent(this.AssociatedObject);
31: _originalMousePosition = e.GetPosition(_canvas);
32: _isDraging = true;
33: this.AssociatedObject.CaptureMouse();
34: }
35:
36: private void AssociatedObject_MouseMove(object sender, MouseEventArgs e)
37: {
38: if (_isDraging)
39: {
40: //get the current mouse position
41: Point currentMousePosition = e.GetPosition(_canvas);
42: //calculate the current position of the shape
43: this.AssociatedObject.SetValue(Canvas.LeftProperty, ((double)this.AssociatedObject.GetValue(Canvas.LeftProperty)) + (currentMousePosition.X - _originalMousePosition.X));
44: this.AssociatedObject.SetValue(Canvas.TopProperty, ((double)this.AssociatedObject.GetValue(Canvas.TopProperty)) + (currentMousePosition.Y - _originalMousePosition.Y));
45: //update original mouse position
46: _originalMousePosition = currentMousePosition;
47: }
48: }
49:
50: private void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
51: {
52: _isDraging = false;
53: this.AssociatedObject.ReleaseMouseCapture();
54: }
55: }
OnAttached里面注冊MouseLeftButtonDown,MouseMove,MouseLeftButtonUp三個事件,OnDetaching里面取消注冊。MouseLeftButtonDown事件獲取Canvas和原始坐標,並設置開始拖動的標志;MouseMove事件不停的計算當前坐標與原始坐標的差值,並更新到Shape的坐標,從而實現移動(坐標的計算方法這里不多說);MouseLeftButtonUp結束拖動。這三個事件處理方法實際上可以看成三個Action,而Behavior本身可以看成一個Trigger。
下面該使用這個Behavior了。
因為是在Canvas上的拖動,所以要有一個Canvas,然后在上面放一個矩形和一個橢圓:
1: <Canvas x:Name="LayoutRoot" Background="White">
2: <Rectangle Fill="#FFF4F4F5" Height="76" Canvas.Left="82" Stroke="Black" Canvas.Top="78" Width="81">
3: <i:Interaction.Behaviors>
4: <local:Behavior1/>
5: </i:Interaction.Behaviors>
6: </Rectangle>
7: <Ellipse Fill="#FFF4F4F5" Height="76" Canvas.Left="221" Stroke="Black" Canvas.Top="154" Width="79">
8: <i:Interaction.Behaviors>
9: <local:Behavior1/>
10: </i:Interaction.Behaviors>
11: </Ellipse>
12: </Canvas>
給它們分別應用上同一個Behavior,然后運行。結果很明顯,兩個Shape都能拖動了。
Behavior是個好東西,好處就不用說了,顯而易見。其實在Blend(做Silverlight界面的)中已經預定義了不少Behaviors,在Microsoft Expression Gallery也可以獲得他人的或共享自己的Behavior。
附上上面的源代碼
參考資料:
