各位朋友大家好,我是Payne,歡迎大家關注我的博客,我的博客地址是http://qinyuanpei.com。最近因為項目上的原因開始接觸WPF,或許這樣一個在現在來講顯得過時的東西,我猜大家不會有興趣去了解,可是你不會明白對某些保守的項目來講,安全性比先進性更為重要,所以當你發現銀行這類機構還在使用各種“復古”的軟件系統的時候,你應該相信這類東西的確有它們存在的意義。與此同時,你會更加深刻地明白一個道理:技術是否先進性和其流行程度本身并無直接聯系。由此我們可以推論出:一項不流行的技術不一定是因為它本身技術不先進,或許僅僅是因為它無法滿足商業化的需求而已。我這里的確是在說WPF,MVVM思想最早由WPF提出,然而其發揚光大卻是因為前端領域近年來比較熱的AngularJS和Vue.js,我們這里表達的一個觀點是:很多你以為非常新潮的概念,或許僅僅是被人們重新賦予了新的名字,當你理清這一切的來龍去脈以后,你會發現這一切并沒有什么不同。這符合我一貫的主張:去發現問題的實質、不要被框架束縛、通過共性來消除差異,所以在今天這篇文章里,我想說說WPF中MVVM模式下命令與委托的關系。
什么是MVVM?
既然提及MVVM,那么我們就無可避免的需要知道什么是MVVM。我們在本文開篇已經提到,MVVM這個概念最早由微軟提出,具體來講是由微軟架構師John Gossman提出的。我個人更喜歡通過將MVC、MVP和MVVM這三者橫向對比的方式來加強理解,因為這從某種意義上來講,這是一個逐步改進和演化的過程。我們常常談及軟件的三層架構,我們常常對MVC耳濡目染以致將其神化,可事實上它們是某種在思想上無限接近的理念而已。
??首先,我們從最簡單的MVC開始說起,作為最常用的軟件架構之一,我們可以從上面的圖示中看到,MVC其實是非常簡單的一個概念,它由模型(Model)、視圖(View)和控制器(Controller)三部分組成,建立在一個單向流動的通信基礎上,即View通知Controller響應用戶請求,Controller在接到View的通知后會更新Model內的數據,然后Model會將新的數據反饋給View。我們發現這個設計可以使軟件工程中的關注點分離,我們注意到通過MVC模式,我們實現了視圖和模型的分離,通過控制器這個膠水層讓兩者間接聯系起來,所以MVC的優點是讓各個模塊更好的協作。那么,它的缺點是什么呢?顯然,視圖和控制器是高度耦合的,因為控制器中無可避免地要訪問視圖內的元素,所以控制器注定無法在這塵世間獨善其身。要知道最早的MVC架構是基于觀察者模式實現的,即當Model發生變化時會同時通知View和Controller,所以我們很快就可以認識到:我們從古至今的所有努力,都是為了讓視圖和模型彼此分離,我們在這條路上越走越遠,幸運的是一直都不忘初心。
??接下來,我們為了徹底地讓視圖和模型分離,我們發明了新的軟件架構:MVP。雖然從感性的認識上來講,它是將Controller改名為Presenter,然而從理性的認識上來講,它在讓視圖和模型分離這件事情上做得更為決絕果斷。通過圖示我們可以發現,視圖和模型不再發生直接聯系,它們都通過Presenter相互聯系,而且各個部分間的通信都變成了雙向流動。我們可以很快意識到,現在全新的控制器即Presenter會變得越來越“重”,因為所有的邏輯都在這里,而視圖會變得越來越“輕”,它不再需要主動去獲取模型提供的數據,它將被動地接擁抱變化,因為現在在視圖里基本上沒有任何業務邏輯。現在我們可以預見,人類會在隔絕視圖和模型這件事情上乘勝追擊,人們會嘗試讓Controller/Presenter/ViewModel變得越來越臃腫,我想說的是,求它們在得知這一切真相時的心理陰影面積,我們試圖讓每一個模塊各司其職、通力協作,結果臟活累活兒都交給了Controller/Presenter/ViewModel,我想說這件事情做的真是漂亮。
??歷史總是如此的相似,人類在作死的道路上匍匐前進,繼續發揚改名的優良傳統,這一次是Presenter被改名為ViewModel,在命名這件事情上,我認為程序員都是有某種強迫癥因素在里面的,所以當你發現一個事物以一個新的名字出現在你的視野中的時候,通常它會有兩種不同的結局,第一,陳酒換新瓶,我們販賣的不是酒是情懷;第二,看今天的你我怎樣重復昨天的故事,我這張舊船票還能否登上你的客船。幸運的是,MVVM相對MVP的確發生了些許改變,一個重要的特性是雙向綁定,View的變化將自動反映在ViewModel中,而顯然ViewModel是一個為View打造的Model,它可以容納更多的普通的Model,因此從某種意義上來說,ViewModel依然作為連接View和Model的橋梁而出現,它是對View的一種抽象,而抽象有兩層含義,即數據(Property)和行為(Command),一旦你明白了這一點,ViewModel無非是一個特殊而普通的類而已,特殊是因為它需要實現INotifyPropertyChanged接口,普通是因為它繼承了面向對象編程(OOP)的基本思想。
更像MVC的MVVM
??到現在為止,我們基本上理解了MVC、MVP和MVVM這三者間的聯系和區別,可是這樣真的就是最好的結果嗎?我們首先來思考一個問題,即什么樣的代碼應該寫在控制器里。比如我們在對項目進行分層的時候,到底應該讓控制器負責哪些任務?我們可以讓Controller處理單獨的路由,同樣可以讓Controller參與視圖邏輯,甚至我們在編寫Model的時候,我們可以有兩種不同的選擇,第一,編寫一個簡單的數據聚合實體,具體邏輯都交給控制器來處理,我們將這種方式稱為貧血模型;第二,編寫一個持有行為的數據聚合實體,控制器在業務邏輯中調用這些方法,我們將這種方式稱為充血模型。所以,在這里我們糾結的地方,其實是選擇讓控制器更“重”還是讓模型更“重”,我曾經接觸過1年左右的Android開發,我認為Android工程是一個相對符合MVC架構的設計,可是我們難免會發現,作為控制器的Activity中的代碼非常臃腫,因為我們在這里需要和視圖、模型關聯起來,所以綜合現有的這些軟件架構思想,我們發現模型和視圖相對來講都是可以復用的,可是作為連接這兩者的Controller/Presenter/ViewModel是非常臃腫而且難以復用的,所以我懷疑我們是否是在真正的使用MVVM。
??我不知道MVVM架構正確的使用方法是什么樣的,因為這是我第一次接觸到這樣一個新的概念,就如同很多年前,我在學校圖書館里看到的一本講Web開發的書中描寫的那樣:當我們不了解MVC的時候,我們理所當然地認為通過文件夾將項目劃分為Model、View、Controller,這樣好像就是MVC啦。可是事實真的是這樣嗎?以我目前公司項目的情況而已,我認為它更像是使用了雙向綁定的MVC,因為你經常可以在ViewModel中看到,某個屬性的Get訪問器中各種被if-else折磨的“臟”代碼,而在ViewModel中我基本上看不到Model的身影,并且因為使用了Binding的概念嚴重弱化了ViewModel作為類的基本屬性,因此它沒有構造函數、沒有初始化,我們可以在Get訪問器中看到各種硬編碼,因為視圖上的需求經常變動,所以當整個項目結束的時候,我本人是非常不愿意去看ViewModel這部分的代碼的,因為項目上要求避免寫Code-Behind代碼,所以大量的事件被Command和UIEventToCommand代替,這樣讓ViewModel變得更“重”了。原本我們希望的是讓這三者各司其職,結果現在臟活累活兒全部變成了ViewModel一個人的。雖然雙向綁定可以避免去寫大量賦值語句,可是我知道ViewModel內心深處會表示:寶寶心里苦。
??如果說WPF對技術圈最大的貢獻,我認為這個貢獻不在雙向綁定,而是它真正意義上實現了設計和編程分離,我們必須承認設計和編程都是一項創造性活動,前者趨向感性,而后者趨向理想,在沒有實現這兩者分離的時候,程序員需要花費大量時間去還原設計師的設計,可是對程序員來講,一段程序有沒有界面設計在某些場合下是完全不重要的,在沒有界面設計的情況下,我們可以通過單元測試來測試代碼的可靠程度,相反地在有了界面設計以后我們反而不容易做到這一點,所以你問我WPF對技術圈最大的貢獻是什么,我會回答它解放了程序員,可以讓理性思維去做理性思維更適合的事情。我不太喜歡聲明式編程,這里是指WPF中XAML這種繼承自XML的標記語言,因為Visual Studio對XAML沒有提供調試的支持,所以當你發現視圖顯示出現問題的時候,你很難分清楚是前臺視圖綁定出現錯誤還是后臺ViewModel出現錯誤,只要你輸入符合XML規范的內容程序都會編譯通過而非引發異常,因為它是用反射所以性能問題廣為人所詬病,其次ViewModel中通知前臺屬性發生變化時需要使用OnPropertyChanged,該方法需要傳入一個字符串類型的值,通常是指屬性的名稱,可是如果你定義了一個字符串類型的屬性,當你在這里傳入這個屬性的時候,因為它是字符串類型所以不會引發編譯錯誤,可是我覺得這個東西還是比較坑。
委托與命令
??好了,現在我想說說WPF中的命令和委托,事實上在我計劃寫這篇文章前,我對這里無比好奇,可當我發現這東西的實質以后,我忽然覺得花費如此大的篇幅來講解這樣一個概念,這是不是會顯得特別無聊。我們的項目上使用的是一個叫做MVVM light的框架,當然我們沒有使用它的全部功能,公司的前輩們非常猥瑣地從這個開源項目中挑了些源代碼出來,這里我不想提及關于這個框架本身地相關細節,因為我認為理解問題的實質比學會一個框架更加重要。首先,WPF為每一個控件都提供了一個Command的依賴屬性,因為任何實現了ICommand接口的類都可以通過綁定的方式和前臺關聯起來,我們這里對比下命令和路由事件的區別可以發現,路由事件必須寫在Code-Behind代碼中,而命令可以寫在ViewModel里,所以直觀上來講命令更加自由靈活。下面我們以一個簡單的例子來剖析這兩者間的關系。
??我們知道使用Command需要實現ICommand接口,所以實現起來是相對容易的,我們這里繼續沿用MVVM light中的RelayCommand這個名字:
public class RelayCommand : ICommand{ private readonly Action
我們可以看到這里有兩個重要的方法,Execute和CanExecute,前者是一個void類型的方法,后者是一個bool類型的方法。當我們需要判斷控件是否應該執行某一個過程的時候,CanExecute這個方法就可以幫助我們完成判斷,而Execute方法顯然是執行某一個過程的方法,可以注意到通過委托我們讓調用者更加自由和靈活地傳入一個方法,這是我喜歡這種設計的一個地方,因為我的一位同事就對普通的路由事件表示無法理解。
??這里需要說明的是CanExecuteChanged這個事件,這個和INotifyPropertyChanged接口中的PropertyChanged成員類似,是在當CanExecute發生變化的時候通知視圖的,我對這里的理解是CanExecute本身就具備對某一個過程是否應該被執行的支持,可是遺憾的是在,在我參與的項目中,人們更喜歡聲明大量的布爾類型變量來處理這里的相關邏輯,因此無論是對Property還是Command而言,在ViewModel里都是看起來非常丑陋的代碼實現。
??好了,現在對我們而言,這是一個非常愉快的旅程,因為在完成對RelayCommand的定義以后,我們綁定命令和定義命令的過程是非常簡單的。除此以外,WPF提供了一個RoutedCommand類,該類實現了ICommand接口,我懷疑MVVM light中的EventToCommand正是通過這種思路實現了路由事件到命令的轉換,因為只有RoutedCommand具備訪問UI事件的能力,這里我們僅僅提出問題,進一步的思考和驗證我們可以留到以后去做。下面我們來看看如何聲明和綁定命令:
public RelayCommand ClickCommand{ get { return new RelayCommand((arg)=> { MessageBox.Show("Click"); }); }}
顯然這個ClickCommand將作為一個屬性出現在ViewModel中,我選擇了一個我最喜歡用的方法,或許這樣看起來非常低端。可是在調試界面的過程中,它要比斷點調試更為直接和直觀。當我們的ViewModel中出現這樣的只讀屬性的時候,直接在Get訪問器中定義它的返回值似乎是最直接有效的方案,可問題是Get訪問器應該是非常“輕”的,因為大量業務邏輯的滲透,現在連這里都不能保留其純粹性了嗎?這讓我表示非常郁悶啊。
現在你可以發現,委托和命令結合得非常好,當你發現這一切如此美妙的時候,回歸本質或許是我們最喜歡的事情,就像純粹的你我一樣,在這個世界上,我們彼此裝點著各自生命里美好的風景,執著而勇敢、溫暖而明媚,那些周而復始的日子里,總能聽到夢想開花的聲音。
小結
在這篇文章里我們討論了MVC、MVP、MVVM各自架構變遷的前因后果,由此我們知道了軟件設計中,一個典型的設計目標是讓視圖和模型分離,可我們同樣發現,帶著這個目標去設計軟件的時候,我們基本鮮有更換視圖的時候,雖然從理論上來講,所有的業務邏輯都是在ViewModel中,視圖和模型應該是可以進行更換的,可是你告訴我,有誰會為同一個軟件制作不同的界面呢?難道我們還能期望通過一個靜態工廠,來為不同的平臺返回不同的視圖,然后理論上只要適配正確的控制器就可以實現軟件對不同平臺的“自適應”,可是軟件開發領域發展至今,最有可能提供完整跨平臺方案的Web技術目前都無法滿足這個需求,所以我們是否應該去懷疑這個設計的正確性呢?同樣的,以Java的SSH三大框架為代表的“配置文件”流派,認為應該將數據庫的相關信息寫在配置文件里,這樣可以滿足我們隨時切換到不同數據庫產品上的需要,可是你告訴我,這樣的應用場景多嗎?所以,技術本身的設計并沒有問題,我們需要思考的是,是否應該被框架和架構束縛,說到底我們是為了設計出更棒的軟件產品,以此為目標,其實框架和架構更應該衍生為一種哲學意義上的思想,我們想讓每一行代碼都充滿智慧的光芒,它驕傲卻不孤獨,因為總有人理解它、懂它。
就愛閱讀www.92to.com網友整理上傳,為您提供最全的知識大全,期待您的分享,轉載請注明出處。
歡迎轉載:http://www.kanwencang.com/bangong/20161102/32994.html
文章列表