MVVM之旅(1)創建一個最簡單的MVVM程式

来源:https://www.cnblogs.com/SilveryBullet/archive/2018/02/06/8418401.html
-Advertisement-
Play Games

這是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:

QQ截圖20180206091702

然後我們將兩個按鈕一個文本框放到ChildWindow里:

QQ截圖20180206092119

那麼接下來問題來了:點擊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里來了

QQ截圖20180206105524

 綁定元素屬性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就如願以償的發生了改變:

QQ圖片20180206112528

用同樣的方式去處理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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在前面的文章中介紹了用戶的註冊及登錄功能,在註冊用戶時可以通過代碼的形式限制用戶名及密碼的格式,如果不符合要求那麼就無法完成操作,如下圖: 該功能的原理是Identity基於的Entity Framework組件在添加用戶之前對用戶提交數據進行校驗後給出的錯誤信息。 數據校驗功能在每一個軟體系統中都 ...
  • 最近閑來沒事研究了下12306網站的登錄,發現驗證碼其實不難破解,只要記錄正確圖片的具體坐標就好了。 具體登錄的實現只需要三步,而且全部是通過瀏覽器地址欄完成的噢!廢話不多說,現在開始三步走! 為使得更好操作,建議每一步打開一個新的標簽頁! 第一步:獲取圖片驗證碼 url:https://kyfw. ...
  • 在開發 XAML(WPF/UWP) 應用程式中,有時候,我們需要創建自定義控制項 (Custom Control) 來滿足實際需求。而在自定義控制項中,我們一般會用到一些原生的控制項(如 Button、TextBox 等)來輔助以完成自定義控制項的功能。 自定義控制項並不像用戶控制項 (User Control ...
  • PDF是一種在我們日常工作學習中最常用到的文檔格式之一,但常常也會因為文檔的不易編輯的特點,在遇到需要編輯PDF文檔內容或者轉換文件格式的情況時讓人苦惱。通常對於開發者而言,可選擇通過使用組件的方式來實現PDF文檔的編輯或者格式轉換,因此本文將介紹如何通過使用免費版的組件Free Spire.PDF ...
  • 轉載http://www.cnblogs.com/zzqvq/p/5816091.html Asp.Net MVC+EF+三層架構的完整搭建過程 架構圖: 使用的資料庫: 一張公司的員工信息表,測試數據 解決方案項目設計: 1.新建一個空白解決方案名稱為Company 2.在該解決方案下,新建解決方 ...
  • "回到目錄" Aspect面向方面編程 面向側面的程式設計(aspect oriented programming,AOP,又譯作面向方面的程式設計、觀點導向編程、剖面導向程式設計)是電腦科學中的一個術語,指一種程式設計範型。該範型以一種稱為側面(aspect,又譯作方面)的語言構造為基礎,側面是 ...
  • 一、自定義控制項的基本步驟: (本示例項目名稱為:W;添加的自定義控制項名稱為) 1、 在“解決方案資源管理器”視窗的項目名上: 右擊à添加à新建項(Ctrl+Shift+A) 2、則會彈出如下視窗,在該視窗中選擇“自定義控制項(WPF)”並修改類“名稱”,點擊“添加” 3、添加成功後則會在該項目中生成C ...
  • 學習.NET的正則表達式時,對零寬斷言比較迷惑,拿出時間學習了一下,做個筆記。 零寬斷言概述 (?<=pattern) (?<!pattern) STRING (?=pattern) (?!pattern) :各種斷言出現的相對位置 ?< lookbehind STRING <lookahead : ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...