浅谈MVVM模式在控件编写中的一些概念和操作方法

浅谈MVVM模式在控件编写中的一些概念和操作方法


在WPF中有UserControl和CostumeControl两种控件,其中CostumeControl是属于look-less Control,因此在使用上和普通的控件事实上并没有任何区别,在我看来所谓的CostumeControl事实上就是Modify-and-reuse Control,因此如果WPF工程使用了MVVM模式的话,CostumeControl事实上面向Main View是没有什么内部黑盒的,所以在使用MVVM时不需要考虑太多问题。

而我们的问题主要集中在UserControl上,因为UserControl就不是复用这么简单了,它事实上是一个黑盒,对于Main View而言,它自身也是一个View,我称之为二级View,同样的,还有三级、四级……在很多情况下,一个UserControl内部的渲染、逻辑等实现是不应该暴露给外界的,也就是说UserControl应该和其他的普通控件一样,是out-of-box的。那么这样一来,就需要考虑以下事实:

  1. UserControl应该对外暴露出依赖属性;
  1. UserControl不应该对外暴露出和自身逻辑、渲染有关的任何API,一切控件内部的变化都应该由暴露出去的依赖属性的改变作为触发;
  2. UserControl的依赖属性有时需要和其内部的子控件进行Binding。

而如果UserControl的实现很复杂的话,那么我们在实现UserControl的时候可能会考虑使用MVVM模式,但是在使用MVVM之前我们需要考虑以下几个问题:

  1. 谁是View?
  1. 谁是ViewModel?
  2. 谁是Model?

对于第一个问题,毋庸置疑的,Uercontrol的View就是它的xaml,里面定义的所有子控件组成了一个View,在考虑这个问题的时候,就需要把这个View单独出来考虑了。

既然View确定好了,那么考虑第二个,谁是ViewModel?这个问题其实也很好回答,为UserControl内建一个ViewModel就行,它负责UserControl的逻辑、渲染等控制。而UserControl所有内部子控件需要Binding的依赖属性都应该去和ViewModel中的依赖属性进行绑定,而不应该直接和UserControl的依赖属性发生关系

最后一个问题,Model。这其实是把MVVM应用到UserControl中最为棘手的地方。因为如果我们要编写一个可复用的UserControl的话,那么Model应该是用户提供的,那么用户方事实上是把自己的Model通过外部的ViewModel绑定在了UserControl的依赖属性上的。因此,考虑到这一点,在编写UserControl的时候我们不应该引入Model,因此,编写UserControl应该是VVM模式了。

其实对于上述讨论还有一个考量,那就是对于UserControl而言,它也不需要持久化的数据保存,因此事实上很多时候也不需要Model;更何况如果使用了Model那意味着在使用的时候还得把Model暴露给用户,这显然是破坏了UserControl封装性的本意。

因此,经过上面的讨论,我们知道了要编写一个UserControl使用的事实上是VVM模式,那么具体要怎么操作呢?经过一段时间的试验,我摸索出了一个应该算是比较好用的方法,总结起来如下:

  1. UserControl中的子控件的依赖属性如果需要Binding,那么需要Binding到ViewModel上,而不是直接和UserControl的依赖属性发生关系;
  1. UserControl暴露出来的控件直接用于调用方进行Binding;
  2. 当UserControl的依赖属性发生变化的时候,应该通过Messenger发送一个消息来通知ViewModel去修改相应的依赖属性;
  3. 当ViewModel的依赖属性发生变化的时候,应该通过Messenger发送一个消息来通知UserControl去修改相应的依赖属性。

其中3、4点实现了ViewModel和View之间的解耦,而不需要使用丑陋的后台Binding来把UserControl的依赖属性和ViewModel绑定起来。

下面以一个简单的实例来说明这个问题:

这里我们定义一个简单UserControl,内部只有一个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>

其中我们为这个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是用来区分消息类型的Token,表示消息从哪里发往哪里,比如MyTextChangedFromView表示消息来自View这边,而MyTextChangedFromViewModel表示消息来自ViewModel这边。

可以看到,当ThisText这个属性被修改的时候我们发出了一个消息通知View需要更新MyText属性。不过需要注意的是,这里有一个if(_myText == value) 这个判断,这个判断是非常必要的,至于为什么需要这个判断放到后面来讲。

其次就是在TestViewModel的构造函数中注册了一个监听器,监听从View那边传来的要求这边修改ThisText属性的消息,并对这个属性进行修改。这就是Usercontrol的ViewModel的主要内容了。

然后我们来看一下UserControl的View的后台代码:

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;
            });
        }
    }
}

在后台代码中我们为UserControl定义了MyText这个依赖属性,然后在这个属性被改变时的回调方法中发送了一个消息,告诉ViewModel去更改它的相应的属性。同样的,在构造函数中我们也定义了一个监听器用来监听前面说到的从ViewModel发过来的更新指令。

同样的,需要注意的是,MyText这个依赖属性的setter中我们也加了一个判断。这里要解释一下为什么要有这个判断了,这就涉及到循环通知的问题。

考虑一下,如果是ViewModel中的ThisText发生了改变,那么它就会去通知View中的MyText发生改变,而MyText发生改变之后又回去通知ViewModel中的ThisText去发生改变……如此就产生了死循环,因此为了打破这个“通知怪圈”,我们需要添加一个if判断来终止它:当通知我要进行修改的内容并没有改变,那么我们就忽略这个改变。注意,这个操作是非常重要的,如果没有这个操作,那么整个程序将无法正常运作。

UserControl的大致内容就这么一些了,剩下的是Main View的实现,完全套用的MVVM模式,也就不细讲了:

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);
            }
        }
    }
}

运行结果如下:

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

示例工程:点我下载


补充

关于用MVVM模式编写UserControl的问题,我在Google上搜索了很久,发现有许多人都认为MVVM不适合用于UserControl的编写,原因在于UserControl只是一个View,很多时候我们都只能为它硬编码。

但是如果当UserControl变得相当复杂、且我们不希望暴露太多具体实现给用户的的时候,我们不得不采用MVVM模式来完成UserControl的编写。而要完成这样一个任务,就需要弄懂到底M、V、VM都是什么,尤其是如果把UserControl看成是第二级View的情况下,那么就更需要弄清楚这一点了。

事实上,在UserControl上使用MVVM最大的问题我认为就是UserControl的依赖属性与其ViewModel的依赖属性的绑定问题,事实上也可以通过后台代码来进行绑定,但是个人认为这种实现并不是太漂亮,因此就采用了Messenger通信的方式来变相地完成这种绑定,虽然代码可能会多写一些,但是让View和ViewModel解耦了也算是一种补偿。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容

  • 1、概述 Databinding 是一种框架,MVVM是一种模式,两者的概念是不一样的。我的理解DataBindi...
    Kelin阅读 76,732评论 68 521
  • 在 Android 开发过程中,由于 Android 作为 View 描述的 xml 视图功能较弱,开发中很容易写...
    射覆阅读 4,222评论 0 22
  • C++ 类 & 对象 类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。类成员函数...
    资深小夏阅读 235评论 0 0
  • 用力爱过的人不该计较,如果不计较还算用力爱过吗? 2017年10月12日 星期四 晴天有风 前几天被渣渣前任回...
    JaryYang阅读 711评论 13 12
  • 今天晚上下班回来后,儿子没在家,儿子的好朋友来了,他们一起去玩了,过了一会儿,要吃饭了,打电话让儿子回来吃...
    子瀚璐菡妈妈阅读 106评论 0 1