淺談MVVM模式在控件編寫(xiě)中的一些概念和操作方法

淺談MVVM模式在控件編寫(xiě)中的一些概念和操作方法


在WPF中有UserControl和CostumeControl兩種控件,其中CostumeControl是屬于look-less Control,因此在使用上和普通的控件事實(shí)上并沒(méi)有任何區(qū)別,在我看來(lái)所謂的CostumeControl事實(shí)上就是Modify-and-reuse Control,因此如果WPF工程使用了MVVM模式的話,CostumeControl事實(shí)上面向Main View是沒(méi)有什么內(nèi)部黑盒的,所以在使用MVVM時(shí)不需要考慮太多問(wèn)題。

而我們的問(wèn)題主要集中在UserControl上,因?yàn)閁serControl就不是復(fù)用這么簡(jiǎn)單了,它事實(shí)上是一個(gè)黑盒,對(duì)于Main View而言,它自身也是一個(gè)View,我稱(chēng)之為二級(jí)View,同樣的,還有三級(jí)、四級(jí)……在很多情況下,一個(gè)UserControl內(nèi)部的渲染、邏輯等實(shí)現(xiàn)是不應(yīng)該暴露給外界的,也就是說(shuō)UserControl應(yīng)該和其他的普通控件一樣,是out-of-box的。那么這樣一來(lái),就需要考慮以下事實(shí):

  1. UserControl應(yīng)該對(duì)外暴露出依賴屬性;
  1. UserControl不應(yīng)該對(duì)外暴露出和自身邏輯、渲染有關(guān)的任何API,一切控件內(nèi)部的變化都應(yīng)該由暴露出去的依賴屬性的改變作為觸發(fā);
  2. UserControl的依賴屬性有時(shí)需要和其內(nèi)部的子控件進(jìn)行Binding。

而如果UserControl的實(shí)現(xiàn)很復(fù)雜的話,那么我們?cè)趯?shí)現(xiàn)UserControl的時(shí)候可能會(huì)考慮使用MVVM模式,但是在使用MVVM之前我們需要考慮以下幾個(gè)問(wèn)題:

  1. 誰(shuí)是View?
  1. 誰(shuí)是ViewModel?
  2. 誰(shuí)是Model?

對(duì)于第一個(gè)問(wèn)題,毋庸置疑的,Uercontrol的View就是它的xaml,里面定義的所有子控件組成了一個(gè)View,在考慮這個(gè)問(wèn)題的時(shí)候,就需要把這個(gè)View單獨(dú)出來(lái)考慮了。

既然View確定好了,那么考慮第二個(gè),誰(shuí)是ViewModel?這個(gè)問(wèn)題其實(shí)也很好回答,為UserControl內(nèi)建一個(gè)ViewModel就行,它負(fù)責(zé)UserControl的邏輯、渲染等控制。而UserControl所有內(nèi)部子控件需要Binding的依賴屬性都應(yīng)該去和ViewModel中的依賴屬性進(jìn)行綁定,而不應(yīng)該直接和UserControl的依賴屬性發(fā)生關(guān)系

最后一個(gè)問(wèn)題,Model。這其實(shí)是把MVVM應(yīng)用到UserControl中最為棘手的地方。因?yàn)槿绻覀円帉?xiě)一個(gè)可復(fù)用的UserControl的話,那么Model應(yīng)該是用戶提供的,那么用戶方事實(shí)上是把自己的Model通過(guò)外部的ViewModel綁定在了UserControl的依賴屬性上的。因此,考慮到這一點(diǎn),在編寫(xiě)UserControl的時(shí)候我們不應(yīng)該引入Model,因此,編寫(xiě)UserControl應(yīng)該是VVM模式了。

其實(shí)對(duì)于上述討論還有一個(gè)考量,那就是對(duì)于UserControl而言,它也不需要持久化的數(shù)據(jù)保存,因此事實(shí)上很多時(shí)候也不需要Model;更何況如果使用了Model那意味著在使用的時(shí)候還得把Model暴露給用戶,這顯然是破壞了UserControl封裝性的本意。

因此,經(jīng)過(guò)上面的討論,我們知道了要編寫(xiě)一個(gè)UserControl使用的事實(shí)上是VVM模式,那么具體要怎么操作呢?經(jīng)過(guò)一段時(shí)間的試驗(yàn),我摸索出了一個(gè)應(yīng)該算是比較好用的方法,總結(jié)起來(lái)如下:

  1. UserControl中的子控件的依賴屬性如果需要Binding,那么需要Binding到ViewModel上,而不是直接和UserControl的依賴屬性發(fā)生關(guān)系;
  1. UserControl暴露出來(lái)的控件直接用于調(diào)用方進(jìn)行Binding;
  2. 當(dāng)UserControl的依賴屬性發(fā)生變化的時(shí)候,應(yīng)該通過(guò)Messenger發(fā)送一個(gè)消息來(lái)通知ViewModel去修改相應(yīng)的依賴屬性;
  3. 當(dāng)ViewModel的依賴屬性發(fā)生變化的時(shí)候,應(yīng)該通過(guò)Messenger發(fā)送一個(gè)消息來(lái)通知UserControl去修改相應(yīng)的依賴屬性。

其中3、4點(diǎn)實(shí)現(xiàn)了ViewModel和View之間的解耦,而不需要使用丑陋的后臺(tái)Binding來(lái)把UserControl的依賴屬性和ViewModel綁定起來(lái)。

下面以一個(gè)簡(jiǎn)單的實(shí)例來(lái)說(shuō)明這個(gè)問(wèn)題:

這里我們定義一個(gè)簡(jiǎn)單UserControl,內(nèi)部只有一個(gè)TextBox控件:

TestControl.xaml:

<UserControl x:Class="MVVM_for_UserControl_Test.TestControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:MVVM_for_UserControl_Test"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>
        <local:TestViewModel x:Key="TestViewModel"/>
    </UserControl.Resources>
    
    <UserControl.DataContext>
        <Binding Source="{StaticResource TestViewModel}"/>
    </UserControl.DataContext>
    <StackPanel>
        <TextBox DataContext="{StaticResource TestViewModel}" Text="{Binding ThisText}"></TextBox>
    </StackPanel>
</UserControl>

其中我們?yōu)檫@個(gè)UserControl定義了這樣子的ViewModel并把ViewModel的依賴屬性綁到了TextBox的Text上:

TestViewModel.cs:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Messaging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVM_for_UserControl_Test
{
    enum MessageTokens
    {
        MyTextChangedFromView,
        MyTextChangedFromViewModel,
    }
    public class TestViewModel : ViewModelBase
    {
        private String _myText = "";
        public String ThisText
        {
            get { return _myText; }
            set
            {
                if(_myText == value)
                {
                    return;
                }
                _myText = value;
                Messenger.Default.Send<String>(value, MessageTokens.MyTextChangedFromViewModel);
                RaisePropertyChanged(() => ThisText);
            }
        }
        public TestViewModel()
        {
            Messenger.Default.Register<String>(this, MessageTokens.MyTextChangedFromView, (msg) =>
            {
                ThisText = msg;
            });
        }
    }
}

其中MessageTokens是用來(lái)區(qū)分消息類(lèi)型的Token,表示消息從哪里發(fā)往哪里,比如MyTextChangedFromView表示消息來(lái)自View這邊,而MyTextChangedFromViewModel表示消息來(lái)自ViewModel這邊。

可以看到,當(dāng)ThisText這個(gè)屬性被修改的時(shí)候我們發(fā)出了一個(gè)消息通知View需要更新MyText屬性。不過(guò)需要注意的是,這里有一個(gè)if(_myText == value) 這個(gè)判斷,這個(gè)判斷是非常必要的,至于為什么需要這個(gè)判斷放到后面來(lái)講。

其次就是在TestViewModel的構(gòu)造函數(shù)中注冊(cè)了一個(gè)監(jiān)聽(tīng)器,監(jiān)聽(tīng)從View那邊傳來(lái)的要求這邊修改ThisText屬性的消息,并對(duì)這個(gè)屬性進(jìn)行修改。這就是Usercontrol的ViewModel的主要內(nèi)容了。

然后我們來(lái)看一下UserControl的View的后臺(tái)代碼:

TestContro.xaml.cs:

using GalaSoft.MvvmLight.Messaging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace MVVM_for_UserControl_Test
{
    /// <summary>
    /// UserControl1.xaml 的交互邏輯
    /// </summary>
    public partial class TestControl : UserControl
    {
        public String MyText
        {
            get { return (String)GetValue(MyTextProperty); }
            set {
                if(value == (String)GetValue(MyTextProperty))
                {
                    return;
                }
                SetValue(MyTextProperty, value);
            }
        }
        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty MyTextProperty =
            DependencyProperty.Register("MyText", typeof(String), typeof(TestControl), new PropertyMetadata("", OnMyTextChanged));
        private static void OnMyTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control = d as TestControl;
            Messenger.Default.Send<String>(control.MyText, MessageTokens.MyTextChangedFromView);
        }
        public TestControl()
        {
            InitializeComponent();
            Messenger.Default.Register<String>(this, MessageTokens.MyTextChangedFromViewModel, (msg) =>
            {
                MyText = msg;
            });
        }
    }
}

在后臺(tái)代碼中我們?yōu)閁serControl定義了MyText這個(gè)依賴屬性,然后在這個(gè)屬性被改變時(shí)的回調(diào)方法中發(fā)送了一個(gè)消息,告訴ViewModel去更改它的相應(yīng)的屬性。同樣的,在構(gòu)造函數(shù)中我們也定義了一個(gè)監(jiān)聽(tīng)器用來(lái)監(jiān)聽(tīng)前面說(shuō)到的從ViewModel發(fā)過(guò)來(lái)的更新指令。

同樣的,需要注意的是,MyText這個(gè)依賴屬性的setter中我們也加了一個(gè)判斷。這里要解釋一下為什么要有這個(gè)判斷了,這就涉及到循環(huán)通知的問(wèn)題。

考慮一下,如果是ViewModel中的ThisText發(fā)生了改變,那么它就會(huì)去通知View中的MyText發(fā)生改變,而MyText發(fā)生改變之后又回去通知ViewModel中的ThisText去發(fā)生改變……如此就產(chǎn)生了死循環(huán),因此為了打破這個(gè)“通知怪圈”,我們需要添加一個(gè)if判斷來(lái)終止它:當(dāng)通知我要進(jìn)行修改的內(nèi)容并沒(méi)有改變,那么我們就忽略這個(gè)改變。注意,這個(gè)操作是非常重要的,如果沒(méi)有這個(gè)操作,那么整個(gè)程序?qū)o(wú)法正常運(yùn)作。

UserControl的大致內(nèi)容就這么一些了,剩下的是Main View的實(shí)現(xiàn),完全套用的MVVM模式,也就不細(xì)講了:

MainWindow.xaml:

<Window x:Class="MVVM_TEST.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:MVVM_TEST"
        xmlns:test="clr-namespace:MVVM_for_UserControl_Test;assembly=MVVM_for_UserControl_Test"
        xmlns:vm="clr-namespace:MVVM_TEST.ViewModel"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <vm:ViewModelLocator x:Key="Locator"/>
    </Window.Resources>
    <Window.DataContext>
        <Binding Source="{StaticResource Locator}" Path="Main"></Binding>
    </Window.DataContext>
    
    <StackPanel>
        <test:TestControl DataContext="{Binding MainModel}" MyText="{Binding MyText}"/>
        <Button Command="{Binding ClickHandle}">Change</Button>
    </StackPanel>
</Window>

MainViewModel.cs:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using MVVM_TEST.Model;
using System.Windows;
namespace MVVM_TEST.ViewModel
{
    /// <summary>
    /// This class contains properties that the main View can data bind to.
    /// <para>
    /// Use the <strong>mvvminpc</strong> snippet to add bindable properties to this ViewModel.
    /// </para>
    /// <para>
    /// You can also use Blend to data bind with the tool's support.
    /// </para>
    /// <para>
    /// See http://www.galasoft.ch/mvvm
    /// </para>
    /// </summary>
    public class MainViewModel : ViewModelBase
    {
        /// <summary>
        /// Initializes a new instance of the MainViewModel class.
        /// </summary>
        private TestModel _testModel;
        public TestModel MainModel
        {
            get { return _testModel; }
            set
            {
                _testModel = value;
                RaisePropertyChanged(() => MainModel);
            }
        }
        public RelayCommand ClickHandle { get; set; }
        public MainViewModel()
        {
            MainModel = new TestModel()
            {
                MyText = "Hello"
            };
            ClickHandle = new RelayCommand(() =>
            {
                MainModel.MyText = "Hello, World!";
            });
        }
    }
}

TestModel.cs:

using GalaSoft.MvvmLight;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVM_TEST.Model
{
    public class TestModel : ObservableObject
    {
        private String _myText = "";
        public String MyText
        {
            get { return _myText; }
            set
            {
                _myText = value;
                RaisePropertyChanged(() => MyText);
            }
        }
    }
}

運(yùn)行結(jié)果如下:

Model修改前.png
Model修改后.png

示例工程:點(diǎn)我下載


補(bǔ)充

關(guān)于用MVVM模式編寫(xiě)UserControl的問(wèn)題,我在Google上搜索了很久,發(fā)現(xiàn)有許多人都認(rèn)為MVVM不適合用于UserControl的編寫(xiě),原因在于UserControl只是一個(gè)View,很多時(shí)候我們都只能為它硬編碼。

但是如果當(dāng)UserControl變得相當(dāng)復(fù)雜、且我們不希望暴露太多具體實(shí)現(xiàn)給用戶的的時(shí)候,我們不得不采用MVVM模式來(lái)完成UserControl的編寫(xiě)。而要完成這樣一個(gè)任務(wù),就需要弄懂到底M、V、VM都是什么,尤其是如果把UserControl看成是第二級(jí)View的情況下,那么就更需要弄清楚這一點(diǎn)了。

事實(shí)上,在UserControl上使用MVVM最大的問(wèn)題我認(rèn)為就是UserControl的依賴屬性與其ViewModel的依賴屬性的綁定問(wèn)題,事實(shí)上也可以通過(guò)后臺(tái)代碼來(lái)進(jìn)行綁定,但是個(gè)人認(rèn)為這種實(shí)現(xiàn)并不是太漂亮,因此就采用了Messenger通信的方式來(lái)變相地完成這種綁定,雖然代碼可能會(huì)多寫(xiě)一些,但是讓View和ViewModel解耦了也算是一種補(bǔ)償。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,238評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,430評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,134評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,893評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,653評(píng)論 6 408
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,136評(píng)論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,212評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,372評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,888評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,738評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,939評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,482評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,179評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,588評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,829評(píng)論 1 283
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,610評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,916評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容

  • 1、概述 Databinding 是一種框架,MVVM是一種模式,兩者的概念是不一樣的。我的理解DataBindi...
    Kelin閱讀 76,852評(píng)論 68 521
  • 在 Android 開(kāi)發(fā)過(guò)程中,由于 Android 作為 View 描述的 xml 視圖功能較弱,開(kāi)發(fā)中很容易寫(xiě)...
    射覆閱讀 4,250評(píng)論 0 22
  • C++ 類(lèi) & 對(duì)象 類(lèi)的成員函數(shù)是指那些把定義和原型寫(xiě)在類(lèi)定義內(nèi)部的函數(shù),就像類(lèi)定義中的其他變量一樣。類(lèi)成員函數(shù)...
    資深小夏閱讀 241評(píng)論 0 0
  • 用力愛(ài)過(guò)的人不該計(jì)較,如果不計(jì)較還算用力愛(ài)過(guò)嗎? 2017年10月12日 星期四 晴天有風(fēng) 前幾天被渣渣前任回...
    JaryYang閱讀 732評(píng)論 13 12
  • 今天晚上下班回來(lái)后,兒子沒(méi)在家,兒子的好朋友來(lái)了,他們一起去玩了,過(guò)了一會(huì)兒,要吃飯了,打電話讓兒子回來(lái)吃...
    子瀚璐菡媽媽閱讀 110評(píng)論 0 1