論道WP(五):通過附加屬性和行爲擴展控件

近和朋友合作一個應用,開發的時候遇到兩個問題,第一個是ListBox控件的多選問題,第二個是PhotoChooserTask選擇器和Image控件的配合問題。巧合的是,近日有讀者在我的博客裏提到他也遇到第二個問題,因此,我想在這篇文章裏分享一下如何使用附加屬性和Expression Blend行爲解決這兩個問題。

當我們把SelectionMode屬性的值設爲Multiple時,ListBox控件就能支持多選了,如圖1所示,此時,我們可以通過ListBox控件的SelectedItems屬性獲取選中的項。接下來,我很自然就會想到把ListBox控件的ItemsSource和SelectedItems兩個屬性分別綁到視圖模型的對應屬性,但是,SelectedItems屬性不是依賴屬性,無法進行數據綁定,怎麼辦?

圖 1

既然自帶的SelectedItems屬性不支持數據綁定,我們就自己創建一個支持數據綁定的吧。在Silverlight裏,我們可以通過附加屬性擴展依賴對象,比如說,我們可以在ListBox控件上設置Grid.Row附加屬性,如代碼1所示。

代碼 1

Grid.Row附加屬性不是ListBox控件的屬性,卻被用來附加額外的數據。在ListBox控件上設置Grid.Row附加屬性本身不會產生什麼效果,但是,當我們把這個ListBox控件放在一個Grid裏時,這個Grid將會根據Grid.Row附加屬性的值安排ListBox控件的位置,這是附加屬性的常見用途。

回到我們的問題,我希望創建一個SelectedItems附加屬性,作爲自帶的SelectedItems屬性和視圖模型對應的屬性之間的橋樑,如代碼2所示。當我們把SelectedItems附加屬性綁到視圖模型的對應屬性時,前者會把後者的數據添加到自帶的SelectedItems屬性。當用戶在用戶界面上更改選中的項時,SelectedItems附加屬性會把數據更新回視圖模型的對應屬性。

代碼 2

接下來,我們一起看看這個SelectedItems附加屬性是如何實現的。創建一個類,然後在裏面創建一個SelectedItems附加屬性,如代碼3所示。在Visual Studio裏,你可以通過propa這個代碼段快速創建附加屬性。值得提醒的是,你必須同時提供GetSelectedItems和SetSelectedItems兩個方法,否則無法在XAML裏讀或寫這個附加屬性的值。

代碼 3

雖然現在已經可以在ListBox控件上把SelectedItems附加屬性綁到視圖模型的對應屬性,但是僅僅這樣無法實現我們想要的效果,因爲ListBox控件並不認識這個外來的異物。於是,把綁定的數據添加到自帶的SelectedItems屬性的責任就落到SelectedItems附加屬性的身上了。我希望SelectedItems附加屬性能在其值發生改變時自動刷新自帶的SelectedItems屬性的值,因此,在註冊SelectedItems附加屬性的時候通過PropertyMetadata指定負責刷新的方法。

在刷新之前,我們必須確保目標控件是ListBox控件,並且處於多選模式,如代碼4所示,否則就多此一舉了。刷新的代碼非常簡單,只需把自帶的SelectedItems屬性清空,然後把綁定的數據添加進來就行了。值得提醒的是,listBox.SelectedItems.Clear();必須放在if (collection != null)的外面,否則,當綁定的數據變成null時,ListBox控件仍會顯示之前選中的項,這顯然是不對的。

代碼 4

當用戶在用戶界面上更改選中的項時,會觸發ListBox控件的SelectionChanged事件,我們可以趁此機會把數據更新回數據源,如代碼5所示。值得提醒的是,因爲SelectedItems附加屬性的值有可能從一個有效值變成null,所以我們必須在改變之後的值不爲null時才訂閱事件,否則,試圖更新的時候將會引發NullReferenceException異常。此外,因爲向自帶的SelectedItems屬性添加數據會觸發ListBox控件的SelectionChanged事件,所以每次刷新之前需要取消訂閱事件(首次除外),否則,相同的數據最終會在數據源出現兩次。

代碼 5

在SelectionChanged事件的事件處理程序裏,我們可以通過SelectionChangedEventArgs對象的AddedItems屬性獲取用戶選中的項,並添加到數據源;通過SelectionChangedEventArgs對象的RemovedItems屬性獲取用戶取消選中的項,並從數據源移除,如代碼6所示。

代碼 6

這裏的實現只在SelectedItems附加屬性的值發生改變時才刷新自帶的SelectedItems屬性的值,如果你的數據源是ObservableCollection對象,而你又希望數據源的更改可以實時反映到自帶的SelectedItems屬性上,你可以訂閱ObservableCollection對象的CollectionChanged事件。

如何選取Image控件的圖片?

在Windows Phone裏,如果你想爲一個聯繫人選取一個頭像,可以在新建/編輯聯繫人頁面上單擊Image控件打開PhotoChooserTask選擇器,然後選取一張圖片。我想在我的應用裏使用這種模式,如圖2所示,但我不想每次做一個新的應用都要重複實現一次。起初我想通過繼承擴展Image控件,無奈它是密封的,不能繼承,只好把目光放在Expression Blend行爲上。

圖 2

我所期望的效果是這樣的,假設我們有一個ChoosePhotoBehavior行爲,把它從Assets面板拖到一個Image控件上,然後在Properties面板上把PhotoUri屬性綁到視圖模型的對應屬性,並且把它設爲雙向綁定,如圖3所示,這樣,當用戶單擊Image控件時,就會打開PhotoChooserTask選擇器,在用戶選好圖片之後,ChoosePhotoBehavior行爲就會更新Image控件,並把PhotoUri屬性的值設爲圖片的路徑,由於數據綁定是雙向的,視圖模型的對應屬性也會隨之更新。

圖 3

接下來,我們一起看看這個ChoosePhotoBehavior行爲是如何實現的。創建一個ChoosePhotoBehavior類,並使之繼承Behavior<Image>類,如代碼7所示。Behavior<T>的泛型參數用來指定這個行爲適用於什麼對象,這個對象的類型必須是DependencyObject類或其子類。如果你希望一個行爲可以用在一個繼承體系上,你可以泛型參數設爲這個繼承體系的基類。

代碼 7

接着,創建一個PhotoUri依賴屬性,如代碼8所示。在Visual Studio裏,你可以通過propdp這個代碼段快速創建依賴屬性。我希望PhotoUri依賴屬性能在其值發生改變時自動刷新Image控件,因此,在註冊PhotoUri依賴屬性的時候通過PropertyMetadata指定負責刷新的方法。

代碼 8

HandlePhotoUriPropertyChanged方法會把改變之後的PhotoUri依賴屬性的值傳給SetPhotoSource方法,由SetPhotoSource方法負責具體的刷新工作,如代碼9所示。在ChoosePhotoBehavior行爲裏,我們可以通過從Behavior<Image>類繼承過來的AssociatedObject屬性訪問Image控件。

代碼 9

LoadPhoto方法會讀取指定位置的圖片,然後返回BitmapImage對象,如代碼10所示。由於圖片的存放位置可能是應用的安裝文件夾或者獨立存儲區,爲了區分這兩種路徑,這裏規定指向獨立存儲區的URI必須帶有“isostore:”前綴,如“isostore:/photo1.png”,而指向安裝文件夾的URI則和平時的表示方式保持一致,如“/photo1.png”。

代碼 10

ChoosePhotoBehavior行爲的主要用途是在用戶單擊Image控件時打開PhotoChooserTask選擇器,並把用戶選取的圖片顯示在Image控件上。爲此,我們需要訂閱Image控件的Tap事件,而訂閱該事件的最佳時機是在OnAttached方法裏,如代碼11所示。OnAttached方法會在Expression Blend SDK把ChoosePhotoBehavior行爲附加到Image控件的時候調用,因此,我們可以趁此機會初始化Image控件。此外,我們可以在OnDetaching方法裏取消訂閱Image控件的Tap事件,OnDetaching方法會在ChoosePhotoBehavior行爲和Image控件分離的時候調用。

代碼 11

值得提醒的是,如果PhotoUri依賴屬性的值是通過數據綁定獲得的,那麼HandlePhotoUriPropertyChanged方法會在OnAttached方法之後調用,因爲PhotoUri依賴屬性的值在創建ChoosePhotoBehavior對象的時候無法確定下來了,必須等到ChoosePhotoBehavior行爲附加到Image控件之後才能確定。如果PhotoUri依賴屬性的值是一個常量,那麼HandlePhotoUriPropertyChanged方法會在OnAttached方法之前調用,因爲PhotoUri依賴屬性的值在創建ChoosePhotoBehavior對象的時候就能確定下來了。但是,由於此時ChoosePhotoBehavior行爲還沒附加到Image控件,AssociatedObject屬性的值是null,這正是爲什麼SetPhotoSource方法(參見代碼9)要在刷新Image控件之前確保AssociatedObject屬性的值不爲null。

當用戶單擊Image控件時,會設置並顯示PhotoChooserTask選擇器,如代碼12所示。在用戶選好圖片之後,會把圖片複製到獨立存儲區,刷新Image控件,修改PhotoUri依賴屬性的值,如代碼13所示。

代碼 12

代碼 13

由於修改PhotoUri依賴屬性會導致HandlePhotoUriPropertyChanged和SetPhotoSource兩個方法依次被調用,爲了避免重複加載圖片刷新Image控件,這裏通過一個_isChoosingPhoto字段來表示是否處於選取圖片的過程,如代碼14所示。這樣,當PhotoUri依賴屬性的值是因爲用戶選取圖片而改變時,SetPhotoSource方法將會跳過刷新Image控件的代碼。

代碼 14

最後,爲了配合ChoosePhotoBehavior行爲產生的帶有“isostore:”前綴的URI,我特意創建了一個UriToPhotoConverter轉換器,如代碼15所示,以便圖1的Image控件可以正確顯示對應的圖片。

代碼 15

值得提醒的是,當我們把Image控件的Source屬性綁到視圖模型的某個字符串屬性時,Silverlight會幫我們完成從String到ImageSource的類型轉換,但是,這僅限於指向安裝文件夾的URI,對於指向獨立存儲區的URI,你要麼自己讀取圖片並創建BitmapImage對象,要麼通過轉換器處理類型轉換。

何時,何者?

附加屬性和Expression Blend行爲看似兩種不同的擴展方式,實質上它們都是基於Silverlight的依賴屬性系統,如果你查看圖3生成的XAML代碼,你會發現ChoosePhotoBehavior行爲和Image控件之間隔着一個Interaction.Behaviors附加屬性,如代碼16所示。附加屬性並不僅僅適用於SelectedItems附加屬性這種簡單情景,如果你細心觀察Silverlight for Windows Phone Toolkit的ContextMenu組件,你會發現它也是通過ContextMenuService.ContextMenu附加屬性實現的。

代碼 16

自定義的附加屬性只能通過手動編輯XAML來使用,因爲Expression Blend並不認識它們,因此不會在屬性面板上顯示。Expression Blend行爲則不同,它是專爲Expression Blend而設的,因此可以通過拖放的方式使用,此外,它的屬性也能在屬性面板上進行設置,尤其適合使用Expression Blend的前端設計師。

創建附加屬性或者Expression Blend行爲的一條重要原則是讓它們用起來儘可能簡單,但這不意味着它們本身也是簡單的,它們承擔着本該由用戶自行處理的複雜性,換句話說,這些複雜性從它們的用戶轉移到它們的創建者。事物之所以看起來簡單是因爲與之相關的複雜性已經在內部被處理掉了,無論你開發一個組件還是一個產品,如果你沒有做好心理準備迎接那些複雜性,那麼這個組件或者產品的發展將會受到限制,因爲它們無法承擔用戶希望擺脫的複雜性。

最後,不得不提的一點是,本文介紹的兩個擴展組件可在http://wputils.codeplex.com/下載,代碼採用MIT開源協議。

 

原文:http://www.infoq.com/cn/articles/WP-extends-controller

 

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