這是MVVM之旅系列文章的第一篇,許多文章和書喜歡在開篇介紹某種技術的誕生背景和意義,但是我覺得對於程式員來說,一個能直接運行起來的程式或許能夠更直觀的讓他們瞭解這種技術。在這篇文章里,我將帶領大家一步一步創建一個最簡單的MVVM程式,程式雖然簡單,但是卻涵蓋了MVVM的基本要素,對於那些還不是很了 ...
這是MVVM之旅系列文章的第一篇,許多文章和書喜歡在開篇介紹某種技術的誕生背景和意義,但是我覺得對於程式員來說,一個能直接運行起來的程式或許能夠更直觀的讓他們瞭解這種技術。在這篇文章里,我將帶領大家一步一步創建一個最簡單的MVVM程式,程式雖然簡單,但是卻涵蓋了MVVM的基本要素,對於那些還不是很瞭解MVVM的讀者來說,相信這會是一個很好的入門。
程式的功能非常簡單:兩個按鈕一個文本框,點擊某個按鈕就把某個按鈕上的文字顯示到文本框里。
傳統做法的問題
對於如此簡單的問題,傳統的做法就是一句話的事,雙擊Button,在xaml.cs文件的事件響應函數里寫下下麵這樣一行代碼就行了:
this.textBox1.Text = button1.Content.ToString();
這種做法很簡單,但是卻暴露了一個很嚴重的問題:this.textBox1是對視圖元素的一個強引用,這樣的代碼把視圖和邏輯完全耦合在了一起(如果沒有textBox1這個具體的視圖對象實例,這行邏輯代碼根本編譯不過去,邏輯離不開視圖,這就耦合了)。這樣的代碼在小軟體里沒啥問題,但是當軟體變大變複雜的時候問題就來了:在大型軟體開發里,大家都是相互分工合作,各自負責自己的模塊,有人負責界面設計,有人負責後臺邏輯,如果代碼這樣寫,那美工的新版界面還沒有畫好的時候,我後臺的邏輯豈不是不能寫不能測試了?
視圖和邏輯分開早已是共識
把軟體的視圖界面和邏輯分開並不是MVVM的發明,上世紀80年代MVC就把視圖層和邏輯層分開(加上數據層,構成了經典的三層架構),後來的MVP在MVC的基礎上做了改進,使得程式之間的耦合性再次降低,微軟以MVP為基礎,考慮到WPF的特性,推出了純數據驅動的MVVM框架。
這裡要特別提一下數據驅動,MVVM讓我們的編程方式從原來的消息驅動、事件驅動轉成了更加高效的數據驅動,這是跟MVC、MVP完全不一樣的。也因此,MVVM里的ViewModel並不等同於在MVC和MVP里做邏輯處理的Controller和Presenter,它更像一個數據格式化器,它的任務就是把來源不同的各種數據進行處理,然後按照一定的格式提供給View。
MVVM的做法
既然MVVM是繼承了MVC、MVP這種經典的三層架構的風格,那麼它肯定將視圖層(V-View)和邏輯層(VM-ViewModel,這裡只是借鑒了邏輯層這樣經典的一個概念,把ViewModel翻譯成邏輯層並不合適,但是業務邏輯一般確實也是在這裡做的)做瞭解耦,因為我們這個例子非常小,所以暫時不涉及數據層(M-Model)。
我們先建一個WPF的項目,項目里添加Views和ViewModels兩個文件夾。顧名思義,Views文件夾里存放所有的View,ViewModels文件夾里存放對應的ViewModel:
然後我們將兩個按鈕一個文本框放到ChildWindow里:
那麼接下來問題來了:點擊Button並且改變TextBox里內容這個事情,如果不能在ChildWindow.xaml.cs里通過響應Button的click事件來完全,那要怎麼做呢?或者說的再簡單一點,不准你在xaml.cs文件後面寫代碼,你要怎麼實現這個事情?(Xaml文件代表的是我們的視圖,xaml.cs里寫代碼非常容易造成視圖和邏輯的耦合,如果我們想徹底解耦視圖層和邏輯層,那麼直接讓xaml純負責視圖,我的邏輯部分完全寫在另外的地方是非常簡單有效的辦法。Android就是這麼乾的,而且更徹底,Android開發里使用純XML文件代表視圖,它壓根就不提供xml.cs這種東西讓你寫代碼,你想要使用視圖裡的元素,你得在其他地方使用findViewById來找)。
MVVM給的答案就是:綁定(Binding)+命令(ICommand)。
添加綁定(Binding)
MVVM把View放在Xaml文件里,把邏輯放在ViewModel里,然後通過綁定讓指定的View和ViewModel關聯在一起。你要處理什麼業務邏輯都在ViewModel里寫,業務邏輯處理完了要更新View的時候也不是直接用“this.xxxView.某屬性=xxx”這樣的句式來更新(其實你想這樣更新也做不到,因為ViewModel為了和View解耦,裡面根本就不會持有View對象的引用)而是通過更改ViewModel里和View綁定的相關屬性來修改View。
把ChildWindow和ChildWindowViewModel綁定在一起很簡單,在ChileWindow.Xaml里設置DataContext就行了:
<Window x:Class="MVVMDemo.Views.ChildWindow" ... xmlns:vm="clr-namespace:MVVMDemo.ViewModels"> <Window.DataContext> <vm:ChildWindowViewModel/> </Window.DataContext>
這樣做了之後View和ViewModel就綁定在了一起。不過,因為我們在點擊Button之後要改變TextBox的顯示內容,所以我們還得把TextBox的Text屬性跟ViewModel做綁定,我們先在ChildWindowViewModel里建一個TextBox1Text屬性用來給TextBox的對象做綁定:
public class ChildWindowViewModel { public string TextBox1Text { get; set; } }
然後把textBox1的Text屬性和它綁定在一起:
<TextBox Name="textBox1" Text="{Binding TextBox1Text}" .../>
添加命令(ICommand)
綁定工作做好了接下來就要添加命令了。因為我們不能直接在xaml.cs文件里寫click事件的響應,所以響應點擊按鈕這個事情是通過命令(ICommand)來實現的。
我們先在ViewModel里添加一個ICommand屬性:
public ICommand Button1Cmd { get { return new DelegateCommand((obj) => { //button1點擊之後要做的事情寫在這裡 }); } }
然後同樣把這個ICommand屬性和button1的Command屬性綁定在一起:
<Button Content="Button1" Command="{Binding Button1Cmd}" .../>
這樣做了之後只要點擊button1,就會自動執行Button1Cmd里的代碼。在這個Button1Cmd屬性里,我們看到有個DelegateCommand類,這是在MVVM使用頻率超高的一個基礎類。因為ICommand只是一個介面,DelegateCommand幫助我們做了一些在MVVM里非常基礎公共的事情,使得我們可以直接在Button1Cmd里如此簡潔的寫命令代碼(說實話,微軟沒把這個類寫進類庫里我都感覺奇怪)。
DelegateCommand的代碼如下(文章末尾的源代碼里還提供了它的泛型版本):
public class DelegateCommand : ICommand { private Action<object> executeAction; private Func<object, bool> canExecuteFunc; public event EventHandler CanExecuteChanged; public DelegateCommand(Action<object> execute) : this(execute, null) { } public DelegateCommand(Action<object> execute, Func<object, bool> canExecute) { if (execute == null) { return; } executeAction = execute; canExecuteFunc = canExecute; } public bool CanExecute(object parameter) { if (canExecuteFunc == null) { return true; } return canExecuteFunc(parameter); } public void Execute(object parameter) { if (executeAction == null) { return; } executeAction(parameter); } }
測試命令
我們刪掉MainWindow,把App.xaml里的StartUri設為ChildWindow的路徑,讓程式運行的時候直接啟動ChildWindow:
<Application x:Class="MVVMDemo.App" ... StartupUri="Views/ChildWindow.xaml">
在DelegateCommand里添加一行彈出消息提示框的代碼:
return new DelegateCommand((obj) => { //button1點擊之後要做的事情寫在這裡 System.Windows.MessageBox.Show("button1 click!");//測試代碼 });
點擊button1,看到瞭如下彈出的消息框,證明Button綁定的命令確實傳到ViewModel里來了
綁定元素屬性TextBox1Text
那我們接下來在這裡去修改TextBox1Text的值,因為TextBox1Text這個屬性已經和ChildWindow里的textBox1的Text屬性做了綁定,所以按照我的想法,如果我在ViewModel里修改了TextBox1Text的值,textBox1顯示的數字就會跟著改變。
按照這個思路我們添加瞭如下的代碼:
return new DelegateCommand((obj) => { //button1點擊之後要做的事情寫在這裡 //System.Windows.MessageBox.Show("button1 click!");//測試代碼 this.TextBox1Text = "button1 click!"; });
再次運行,點擊button1,結果卻什麼事情都沒有發生,並沒有出現我們期待的textBox1里出現“button1 click!”的字樣,為什麼呢?明明確實執行了這行代碼,View和ViewModel之間的綁定也確實做好了,TextBox1Text的改變為什麼不能自動改變textBox1的值?
答案是這樣的:我們雖然把ViewModel的屬性跟View元素的屬性做了綁定,如果想讓ViewModel里的屬性發生變化之後View里對應的元素也跟著變,你得手動通知它。
為什麼需要我們手動去通知,微軟為什麼不把這種東西都做到框架裡面去?
你想啊,View的界面里有這麼多元素,每個元素都有這麼多屬性,而我需要改變的屬性只有那麼幾個,我不能因為我要改變這幾個屬性而把所有的屬性都附加上這種功能把,這樣太浪費資源了。另外,自己去手動通知代碼也非常簡單,都是可以重覆利用的。
添加通知INotifyPropertyChanged
因為每個屬性要通知界面都要實現這個通知介面,所以可想而知,這是一個要重覆做很多次的事情。為了讓我們以後更加省心,我們把這個通知介面的實現放到基類ViewModelBase里去,讓所有的ViewModel繼承這個基類就行了
public class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void RaisePropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } }
然後我們在ChildWindowViewModel繼承ViewModelBase,改寫一下TextBox1Text屬性:
private string textBox1Text; public string TextBox1Text { get { return this.textBox1Text; } set { this.textBox1Text = value; RaisePropertyChanged("TextBox1Text"); } }
我們在TextBox1Text的set里添加了RaisePropertyChanged("TextBox1Text");這樣一行,這就是告訴系統,如果我這個屬性發生了改變,就去通知界面里一個叫“TextBox1Text”的屬性(不過他只負責通知到位,通知到了之後你要做什麼它就不管了)。
再次運行程式,點擊button1,我們發現textBox1就如願以償的發生了改變:
用同樣的方式去處理button2,效果一樣的。
結語
至此,我們這個全世界最簡單的MVVM程式的功能就已經都實現了。通過綁定和命令實現了一個最簡單卻非常具有代表性的操作:界面點擊操作,後臺處理邏輯,處理好了以後把結果更新在界面里,也抽象出來了DelegateCommand和ViewModelBase兩個通用類。它很好的解耦了視圖和邏輯:大家可以看到,我們的ChildWindowViewModel裡面沒有任何和View相關的代碼,因此完全可以單獨拿來出測試;我們的ChildWindow.xaml.cs文件里沒有一行代碼,ChildWindow完完全全就是一個視圖界面,你也可以對他單獨操作。
而且更重要的是:我的ChildWindowViewModel只要第一次去設置好和ChildWindow綁定的屬性,以後就再也不用跟View打交道了,我以後所以對View的操作都變成了對ViewModel里屬性的操作,我只要知道我要寫的邏輯最後要賦值給那個屬性就行了,至於那個屬性最終會以什麼樣的形式綁定呈現在界面上,我完全不關心。這不是程式員夢寐以求的事情麽?
對於美工來說一樣解脫了,以前一個大的項目組裡雖然有程式員,也有專門的美工,但很多時候的工作是這樣的:程式員說這裡需要一個藍色的按鈕,美工就去切一個按鈕給程式員,程式員把這張圖片設置成按鈕的背景,接著要信息顯示的背景圖片又得找美工要,然後自己寫程式把圖片樣式顏色都調好。但是現在卻可以變成這樣:項目經理說這個View要顯示一個人的各種具體信息(年齡性別名字等等等之類的),然後美工可以拿起Blend這樣的工具,按照自己的想法把這整個View的界面畫好,然後直接就向貼紙一樣貼在程式員寫的ViewModel里,程式員什麼都不用改,指定一下DataContext和綁定屬性就可以直接用了,這樣的合作多麼暢快人心!
另外,MVVM雖然是微軟為了WPF量身定做提出來的,但是它的思想卻非常具有啟發性,它通過綁定讓視圖和邏輯層之間的解耦比MVP還徹底,所以現在不止WPF,Android、IOS、前端開發都在研究MVVM。但是畢竟MVVM是微軟為了WPF量身定做的,所以總的來看,還是WPF對MVVM的實現最為自然簡潔優雅。深入瞭解MVVM的思想和實現對提高WPF的編程水平有巨大的幫助,如果還是使用MFC、Winform時代的思想來寫WPF程式,那就真的是白白浪費了WPF這個如此先進的技術,有種用屠龍刀在切牛肉的既視感。
文章代碼下載地址:MVVMDemo.rar