WPF中MVVM模式原理分析與實(shí)踐
1, 前提
可以說MVVM是專為WPF打造的模式, 也可以說MVVM僅僅是MVC的一個(gè)變種, 但無論如何, 就實(shí)踐而言, 如果你或你的團(tuán)隊(duì)沒有使用"Binding"的習(xí)慣, 那么研究MVVM就沒有多大意義.
另外,個(gè)人覺得, 使用Command以及打造一種合理的簡化的方式去使用Command也與使用Binding一樣重要.
2, 誕生
為了解決現(xiàn)實(shí)世界中的問題,我們需要將現(xiàn)實(shí)世界中的事物加以抽象, 然后得到了Domain Object, 無論貧血的還是富血的, 我們都可以簡單地把他們歸結(jié)為"由現(xiàn)實(shí)世界抽象出來的模型", 也就是我們的model, 也就M-V-VM中的"M"。
但其無法與我們的用戶進(jìn)行交互, 所以, 我們需要為其創(chuàng)建一個(gè)界面(視圖, View), 該視圖可以與用戶輸入設(shè)備進(jìn)行交互, 這很棒, 但問題是如何將View與我們的model關(guān)聯(lián)起來? Binding便可以發(fā)揮作用了, 比如視圖上的某一個(gè)文本框中的文本和Model中的"用戶名"關(guān)聯(lián)起來, 用戶便可以通過操作該文本框來訪問和修改Model的"用戶名"了。
這是極其簡單的情況, 但實(shí)際編程時(shí)我們發(fā)現(xiàn), Model中的屬性(與方法)往往不那么容易與View中的界面控件關(guān)聯(lián)起來, 比如, "類型不匹配": 界面控件所需要的類型與模型中屬性提高的類型不匹配. "需要額外操作": 模型中的數(shù)據(jù)需要經(jīng)過一些額外的處理才能傳給視圖,反之亦然. 此時(shí), 我們意識到View似乎需要一個(gè)"Helper"類來處理一些額外工作.
這個(gè)helper所包含的代碼可以放在除了Model外的很多地方(我們現(xiàn)在不考慮貧血富血之類的爭論), 比如View中, 記得自己剛學(xué)習(xí)窗體程序開發(fā)時(shí)就是這么干的, 將絕大多數(shù)處理邏輯放在那個(gè)所謂的CodeBehind中. 后來,正如大家在各種設(shè)計(jì)模式書籍中所看到的一樣,為了將View和Model剝離開來,實(shí)現(xiàn)view可替換(比如你可以講自己精心設(shè)計(jì)的軟件同時(shí)運(yùn)行于窗體程序,Web甚至Mobile上), 便有了MVC. 有了MVC以后似乎就開始滋生M-V-XXX之類的爭論與變種模型, 比如MVP以及這里的MVVM,甚至MVP也有著Supervising Controller與Presentation Model兩種方式. 但主要圍繞兩個(gè)問題,一是model與view之間的關(guān)系, 完全隔離的?單向的還是雙向的? 二是這個(gè)"XXX"需要完成哪些功能,簡單流程調(diào)度?復(fù)雜規(guī)則處理? OK,這些爭論都沒有關(guān)系, 是否采用某種模式取決于你的開發(fā)所處的環(huán)境(比如語言特性,框架特性)以及你的業(yè)務(wù)特性以及所面臨的主要變化點(diǎn)等等。
但與MVC,MVP所不同的是,MVVM的引入不僅僅是技術(shù)上的原因(解除耦合應(yīng)對變化等老生常談),另外一個(gè)很大原因是:軟件團(tuán)隊(duì)開發(fā)方式的改變.如果你做過一段時(shí)間的WPF項(xiàng)目開發(fā)的話,你可能會有比較明顯的感覺:在View層打造上,如何分配程序員和美工的工作.在繼續(xù)閱讀之前,大家可以看看我以前的一篇文章"在UI Designer與Developer之間". 以前我們團(tuán)隊(duì)采用的便是"集成模式", 我便兼職了其中的"Integrator"角色.這還不錯(cuò).但說實(shí)在的,這僅僅是一個(gè)在特殊情況下不得已而為之的暫時(shí)方案,所以我們付出了很大的努力開始轉(zhuǎn)向"收割模式"了,要轉(zhuǎn)向這個(gè)模式,至少需要兩個(gè)基本條件:
(1)你擁有能夠熟練運(yùn)用Blend等工具能為程序員輸出XAML的美工, 他專注于純粹的UI/UE, 另外他還必須具有一定的"程序員"思維.以便輸出的東西能很好地作為程序的一部分而運(yùn)轉(zhuǎn)起來,而不是僅僅"看上去"是那樣的。
(2)你需要能夠脫離View層但仍能編寫出高質(zhì)量代碼的程序員。
幸運(yùn)的是, 我們在努力創(chuàng)造條件1,并取得了很好的效果.(你可以招一個(gè)具有Flash腳本編寫經(jīng)驗(yàn)的并且有極大的學(xué)習(xí)熱情的美工人員, 并對他進(jìn)行Blend的相關(guān)培訓(xùn)). 而MVVM模式為我們實(shí)現(xiàn)第二個(gè)條件提供了極大的便利. 為什么MVC/MVP模式不行而MVVM可以呢? 很簡單, 在MVC和MVP模式中, View層都具有很多代碼邏輯, 開發(fā)View層的是程序員, 雖然UI/UE團(tuán)隊(duì)會做很多工作, 但這個(gè)層的"實(shí)現(xiàn)者"仍然是程序員. 在以前的開發(fā)中,其工作得很好, 而在WPF開發(fā)中程序員對View層的展現(xiàn)顯得力不從心了,美工(指符合上面條件1的美工)雖然很擅長, 但他會說"可惜我不會程序".于是, 我們需要一種方式將View層的代碼邏輯抽取出來,并View層很純粹以便完全讓美工去打造它.相應(yīng)地, 需要將View層的相應(yīng)邏輯抽取到一個(gè)代碼層上,以便讓程序員專注在這里。
回想一下, 我們只所以要在View(Xaml)背后寫一些代碼(C#), 無非是想傳遞一些數(shù)據(jù)以及傳遞數(shù)據(jù)時(shí)的數(shù)據(jù)的處理或在用戶與界面控件進(jìn)行交互時(shí)執(zhí)行一些操作, 最簡單的例子是在MVC中當(dāng)界面發(fā)生交互時(shí)View去調(diào)用Controler中的某個(gè)方法, 以便將該操作的相應(yīng)"指示"傳遞到"后臺"去. 在以前的技術(shù)中, 這樣的"銜接性"的代碼是必須的. 而在WPF中, 則可以通過另外的技術(shù)來進(jìn)行層與層之間的"銜接", 這就是"Binding" 和"Command", 以及稍后我們會提到的"AttachBehavior". 通過Binding, 我們可以實(shí)現(xiàn)數(shù)據(jù)的傳遞; 通過Command, 我們可以實(shí)現(xiàn)操作的調(diào)用.(AttachBehavior的作用稍后再談). Binding和Command是可以寫在XAML中的, 這樣看來XAML后面對于的CS文件可以被完全拋棄或不予理會了. 這樣的XAML文件正是美工所需要的. 而這些對于Binding以及Command的定義描述以及其他相關(guān)信息的代碼應(yīng)該放在那里呢, 當(dāng)然不是View, 更不是Model, 是"ViewModel". ViewModel是為這個(gè)View所量身定制的, 它包含了Binding是所需的相關(guān)信息,比如Converter以及為View的Binding提供DataContext, 它包含了Command的定義以便View層可以直接使用, 另外,它還是一個(gè)變種的Controler, 它得負(fù)責(zé)業(yè)務(wù)流程的調(diào)度。
于是, 便有了這副圖, 然后, 正如"時(shí)勢造英雄"所言, MVVM就誕生了.
3, ViewModel 與單元測試
如果你是一名正在使用MVVM模式打造軟件的程序員, 那么我勸你盡快忘掉View. 你所面對的是這樣一個(gè)模式"UnitTest-ViewModel-Model"(這并非一個(gè)模式, 僅僅是我為闡述觀點(diǎn)而暫時(shí)如此表述的)。
記得曾經(jīng)有一個(gè)Model-View-AbstractView模式, 而MVVM中的VM實(shí)際也是一個(gè)AbstractView: the abstraction of view. 它是一個(gè)抽象的View, 具有一個(gè)View的靈魂,而不具備相應(yīng)的可視化控件而已. 所以對于程序員而已, 打造這樣一個(gè)抽象的VM就可以認(rèn)為是完成View層的打造了.而當(dāng)美工完成無數(shù)控件組成的實(shí)際的View后, 我們就可以用Binding和Command這樣的黏合劑將這個(gè)抽象的View和實(shí)際的View黏合在一起了。
那么在黏合之前, 我們怎么知道自己的VM是否正常工作呢? 單元測試!
在說明對于ViewModel進(jìn)行單元測試的重要性之前, 送給大家一句話: "View and Unit Test are just two different types of ViewModel consumers" (Josh Smith). 如果我們將ViewModel看作生產(chǎn)者, 那么View和Unit Test都是具有同等地位的消費(fèi)者而已. 并且UnitTest相比于View而言具備更大的消費(fèi)能力. 或者你可以簡單的認(rèn)為View也僅僅是一種不太推薦的測試方式而已. 所以要實(shí)施好這個(gè)模式, 那么對ViewModel的單元測試就是必須的了,并且這個(gè)測試要不依賴于任何UI控件. (那么不是不對應(yīng)ViewModel的開發(fā)是不是就應(yīng)該通過測試來驅(qū)動(dòng)了?TDD?)
4, AttachBehavior
一般情況下利用Command, Binding, AttachProperty等WPF特性, View和ViewModel之間能配合工作得很好. 假設(shè)我們有一個(gè)Button, 當(dāng)該Button被點(diǎn)擊的時(shí)候我們要完成一些操作, 很簡單, 將該操作封裝成一個(gè)Command并綁定到該Button上就可以了, 但如果我們要在Button被Load的時(shí)候執(zhí)行另外一些操作呢? 由于Button沒有直接被Load事件所觸發(fā)的Command, 所以不能使用Command了. 不能直接將Load事件處理器寫在Button所在的Xaml所對應(yīng)的CS文件里, 這和我們剛才對MVVM的設(shè)計(jì)是相矛盾的. 一個(gè)不太好的方案是繼承一下Button, 并撰寫一個(gè)由Load所觸發(fā)的Command, 這可行, 但明顯不好. 正如一個(gè)控件沒有某個(gè)屬性并且在不繼承的情況下而采用AttachProperty一樣, 我們可以采用AttachBehavior. AttachBehavior不是WPF特性, 它僅僅是一個(gè)***實(shí)踐, 一個(gè)Pattern. 關(guān)于AttachBehavior語法如何書寫, 請參考 : http://www.codeproject.com/KB/WPF/AttachedBehaviors.aspx
【編輯推薦】