移動開發 細究MVVM

細究MVVM

熟悉WPF或Silverlight的同學應該不會對MVVM模式感到陌生了,它把應用程序劃分成視圖、視圖模型和模型三層,如圖1所示:

圖 1

表面上,這個層次結構還蠻清楚的,但如果你細究每層應該包含什麼,事情就沒那麼簡單了。

視圖應該是最容易理解的一個部分了,它通常是指用戶可以看到的界面,一般都是通過XAML代碼來實現的。但是,XAML代碼並不能實現一切想要的效果,這個時候很多人會把目光投向代碼隱藏文件,在裏面通過事件處理程序實現某些效果。那麼,究竟什麼樣的代碼可以放在代碼隱藏文件裏?這些代碼是否包含原本應該放在視圖模型裏的代碼?這些效果是否還有其他途徑可以實現?

視圖模型從字眼上看應該是視圖的抽象,這意味着每個視圖都應該有一個對應的視圖模型,這也是我們經常看到的做法。不過,有人對此表示反對,他們認爲視圖和視圖模型不是一一對應的關係,整個應用程序應該有一個主要的視圖模型,各個視圖將會綁到這個主要的視圖模型上,同時有一些次要的視圖模型,用來表示諸如配置等輔助方面。那種做法纔是正確的?視圖模型裏面又該包含什麼樣的代碼?

最後是模型,大多數人對它的一個共識就是,它會包含具體的數據,這些數據最終會顯示到用戶界面上。問題是,它到底是簡單的POCO還是業務邏輯的複雜類型?我們是否應該在這裏放置驗證邏輯?是否允許它和視圖直接綁定,還是需要另外創建對應的包裝類?如果使用LINQ to SQL存儲數據的話,模型裏的類是否就是LINQ to SQL的實體類?如果需要訪問Web Service獲取數據,模型裏的類和添加Web Service時自動生成的類又是什麼關係?

哇,問題還真不少啊!你是否曾經遇到這些問題?你對它們有什麼想法?誠然,這些問題沒有唯一的標準答案,它們都是開發者在具體實踐中提煉和總結出來的智慧,但它們通常只針對於特定的應用場景,或者說,它們是爲了滿足某些需要而產生的。舉個例子吧,有些人可能會覺得添加Web Service時自動生成的類和他們要創建的模型類基本上是一致的,爲了避免重複勞動,他們選擇直接使用那些自動生成的類,這種做法一般不會出現問題,直到由於需求的變更,模型不再和Web Service對應起來,但此時應用程序的其他部分已經通過這些自動生成的類和Web Service緊密耦合起來了,修改應用程序就可能變得非常困難。相反,如果應用程序的功能比較單一、專注,Web Service的接口也比較穩定,那麼特意爲那些自動生成的類創建一組一模一樣的模型類顯然增加勞動成本,埋下潛在的維護問題。

最簡單的實現

假設我經常去圖書館借書,我需要一個應用查看所有圖書的歸還日期,如圖2所示:

圖 2

這是一個非常簡單的應用,從MVVM模式的角度來看,圖2所展示的用戶界面就是視圖了,而視圖模型和模型也都非常簡單,分別爲圖3的MainViewModel類和Book類:

圖 3

頁面的ListBox控件將會綁到MainViewModel的Books屬性,書名將會綁到Book的Title屬性,而歸還日期則綁到Book的DueDate。

到目前爲止,一切都非常自然順暢,直到我對它提出兩個新的需求:

  • 圖書列表根據歸還日期從小到大排序,即最先要還的書拍在最上面。
  • 今天和明天要還的書字體使用強調色。

這兩個新的需求都非常合理。第一個需求屬於頁面的抽象邏輯,不與頁面的任何控件掛鉤,這種需求一般會在視圖模型裏面實現,具體地就是在MainViewModel的構造函數裏初始化Books屬性時進行升序排序。

至於第二個需求,它涉及到具體的TextBlock控件以及對Book類的DueDate屬性的二次處理,原則上不應該在Book類裏面實現,根據個人偏好,這個需求有兩種不同的實現方式:

  • 創建一個ItemViewModel類,包裝Book類並暴露相關屬性,同時提供一個Foreground屬性用於和TextBlock控件的對應屬性綁定,Foreground屬性可以在初始化ItemViewModel時根據Book的DueDate屬性計算。
  • 通過轉換器實現相同的效果。

有人說,使用MVVM模式可以消除轉換器的需要,是的,任何時候當你需要一個轉換器,你都可以通過創建包裝類並提供額外的屬性獲得相同的效果,但我們不應該把這個問題絕對化,轉換器的存在價值體現在可以在不同的綁定關係上重用相同的邏輯,而且更符合Expression Blend用戶的使用習慣。

看到這裏,有些同學可能會問,如果用戶要求同時提供根據圖書標題和歸還日期兩種排序方式呢?每當我們遇到一個新的需求時,請不要馬上動手實現或者考慮如何實現,應該先想想用戶爲什麼有這樣的需求。根據歸還日期進行排序這個需求對應着幫助用戶避免逾期歸還所受的懲罰,但根據圖書標題排序呢?很多時候,我們會想當然地認爲用戶需要某些功能,而忽略用戶真正的需求,這樣不但會導致功能冗餘,還會分散用戶對於最重要功能的注意。事實上,根據圖書標題排序這個需求很可能是想幫助用戶瞭解某本書是否已經存在於列表中,或者某本書的具體信息,如還可以讀多久,本質上,這個需求很可能是幫助用戶從列表中快速查找某本書。如果是這樣,爲什麼不考慮給出一個即時搜索的功能,比如說,當用戶單擊搜索按鈕時,會顯示一個搜索框,用戶在裏面輸入關鍵字,圖書列表馬上顯示包含該關鍵字的圖書?

命令與操作

到目前爲止,這個應用幾乎可以說一無是處,因爲它不支持添加、編輯和刪除等操作,那麼,實現這些操作又有哪些東西需要考慮呢?假設我們在用戶界面上添加相應的按鈕和菜單,如圖4所示:

圖 4

以往的做法是在代碼隱藏文件裏通過事件處理程序來實現,但在MVVM模式裏,我們提倡通過命令對象來實現,問題是,這些命令對象在哪實現?添加操作是頁面範圍的,與之對應的命令對象可以在MainViewModel類裏實現,但編輯和刪除兩個操作對應於Book類,那麼,我們是否應該在Book類裏添加相應的行爲?

有一部分人對此的觀點是,模型並非單純的POCO,而是完整的領域模型,可以包含區別於頁面邏輯的業務邏輯,並且不會和ORM的實體類等同起來,這樣做的好處是我們有一個統一的地方來維護整個應用的狀態,也和具體的數據層解耦,無論數據最終來自本地還是遠程服務,都不會影響在此之上的東西,與此同時,我們也不必在爲不同的視圖模型之間如何傳遞數據感到煩惱。當然,這樣做的壞處也是明顯的,它引入了大量可能不必要的複雜性,對於小型項目具有不少殺傷力。

如果我們不在Book類裏添加行爲,又不想在代碼隱藏文件裏通過事件處理程序來實現這些操作,那麼我們就需要考慮一下Expression Blend的行爲(Behavior)了。具體的想法是這樣的,假設用戶單擊編輯菜單項的時候將會打開EditItemPage.xaml頁面,而這個頁面需要知道用戶選中哪本圖書,那麼整個操作就可以看作通過NavitagionService.Navigate方法打開“EditItemPage.xaml?title=XXX”這樣的鏈接了。要實現這樣的效果,你可以使用AppBarUtils for Windows Phone SDK 7.1的NavigateWithQueryStringAction,如代碼1所示:

<AppBarUtils:NavigateWithQueryStringAction TargetPage="/EditItemPage.xaml"> 
<AppBarUtils:Parameter Field="title" Value="{Binding Title}"/> 
</AppBarUtils:NavigateWithQueryStringAction> 

代碼 1

配合EventTrigger在MenuItem上使用就可以實現預期的效果了。

除此之外,你也可以考慮創建一個ItemViewModel類,然後在上面實現編輯操作的命令對象,然後和MenuItem的Command屬性進行綁定。如果你選擇這種做法,就會無可避免地遇到在視圖模型裏打開頁面的問題。我們通常用來打開頁面的NavitagionService.Navigate方法必須在頁面的範圍內纔可訪問,但視圖模型對於視圖一無所知,怎麼調用這個方法?

常見的做法是封裝PhoneApplicationFrame的Navigate方法。當你用Visual Studio創建一個Windows Phone項目時,App類裏面會有一個RootFrame屬性,你可以通過這個屬性調用PhoneApplicationFrame的Navigate方法。事實上,PhoneApplicationFrame和頁面是共用同一個NavitagionService對象的。

刪除操作是一種很特別的操作,它同時涉及到集合以及裏面的元素,但在XAML裏,MenuItem只能從父元素繼承對應的Book對象,卻無從知曉包含該對象的集合,這爲實現刪除操作造成極大困擾。常見的解決辦法是把MainViewModel作爲一個靜態屬性放在App類裏,這樣你就可以輕易訪問到包含Book對象的Books集合。從這個角度來看,如果我們一開始就把模型設計成領域模型,負責管理和維護領域對象的狀態,那麼現在就不必把某個視圖模型硬塞到App類裏了。

應用程序欄以及其他

在Windows Phone上使用MVVM模式必定會遇到的一個障礙就是應用程序欄,它的問題在於它不是Silverlight控件,而是系統組件,這意味着它無法像通常的Silverlight控件那樣進行數據綁定。市面上有不少解決方案,其中之一就是前面提到的AppBarUtils for Windows Phone SDK 7.1,有興趣的可以看看Allen Lee寫的《AppBarUtils使用指南》

如果你把模型設計成領域模型,那麼你一定要注意Windows Phone的“深度鏈接”(deep link),這種情況會在你使用Toast通知和次要磁貼(secondary tile),並在用戶單擊打開應用的某個頁面時出現。由於用戶僅對某個頁面感興趣,而且當用戶按返回鍵時會直接退出應用而不是按照應用的常規邏輯返回上一頁,因此構建整個領域模型會顯得勞師動衆、耗費資源。

有人認爲,使用MVVM模式的一大好處是爲Expression Blend用戶帶來便利,確實是這樣,數據綁定和命令對象的應用使得Expression Blend用戶更易通過可視化操作使用開發人員的後臺代碼。如果你的視圖模型也會給Expression Blend用戶使用,那麼你必須考慮的一點就是在視圖模型裏提供設計時數據,尤其是你的邏輯包含訪問本地數據庫或者Web Service,因爲在Expression Blend的設計器裏無法執行這些代碼。

最後不得不提的是,MVVM模式使得我們可以繞過用戶界面對應用的功能進行測試,包括單元測試,如果你有興趣,可以看看Chenkai的《Windows phone 應用開發[9]-單元測試》

發佈了74 篇原創文章 · 獲贊 4 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章