MVVM、MVVMLight、MVVMLight Toolkit之我见

我想,现在已经有不少朋友在项目中使用了MVVMLight了吧,如果你正在做WPF,Silverlight,Windows Phone的开发,那么,你有十分必要的理由了解MVVM和MVVMLight。我写这篇文章的目的,是给大家做一个总结,以便更多的朋友了解并掌握MVVM。

 

首先,要说一下MVVM的概念。MVVM严格来说,并不是一种框架,而是一个设计的模式吧。与它有关的设计模式还有MVC (现在广泛用于Web应用中),以及MVP(之前有用过在Windows Forms和WPF中)

 

如果你希望对MVVM有更加感性地认识,我推荐你看下面这篇文章。

http://www.codeproject.com/KB/WPF/WpfMvvmQuickStart.aspx

这篇文章写得实在太好了,我很欣赏这样的技术人才,能把一个抽象问题有层次地讲清楚。(我强烈建议对MVVM的概念了解不深的朋友,认真读这篇文章,而不要急于用MVVMLight,因为MVVM是一种模式,而MVVMLight只是其中一种具体的实现)

 

然后,我要说一下MVVMLight吧,刚才说了,它是一种MVVM的实现。自然它不是唯一的一种实现,但现在大家公认的是,它是比较好的一个实现。就我个人的体会来说,我以前用过微软提供的Prism中的MVVM特性,但老实说,可能Prism的目标太大了,所以在MVVM这个具体的点上,实在不是那么好用。

 

值得一说的是,从使用Prism转换到使用MVVMLight过程相当简单,如果有类似情况的朋友,不要有什么顾虑。我这里不是说Prism不好,它与Mvvmlight严格来说,不是一个重量级的产品。MVVMLight专注与MVVM的实现,自然更加灵活

 

接下来,我认为要学习MVVMLight最好的Quick start,就是作者自己写的这个网页

http://galasoft.ch/mvvm/

通过这个文章,我们可以很清楚地了解MVVMLight的设计思路和包含的有关组件,无需太多补充,文章浅显易懂,确实是我们要学习的一个榜样

请注意,我这篇文章并非逐一讲解MVVMLight的细节功能使用,我主要提一些重点,并且分享一些我的看法和观点,当然这仅是我一家之言,不见得完全正确。

事实上针对如何使用的方面,已经有不少文章了,大家可以参考

http://zzk.cnblogs.com/s?w=mvvmlight

另外一方面,我觉得大家其实要自己多动手才会有实际的收获。MVVMLight使用并不难,在使用中大家可以领会到更多。

 

实际上,我们经常谈论MVVMLight的时候,其实谈的是MVVMLight Toolkit,它主要是为了更加方便开发人员使用MVVMLight,它会在本地的GAC(Global Assembly Cache)中分别安装针对WPF,Silverlight,Windows Phone的Assembly(分别各自有两个Assembly),并且在Visual Studio中添加相应的项目以及项模板,更加贴心的一点是,它还提供了几个代码段。

 

有的朋友可能会问,那么MVVMLight到底是什么呢?呃,MVVMLight嘛,就是MVVMLight Toolkit的名称啦 Be right back,有点绕对吧,放松点,不要那么较真嘛

 

imageimageimageimage

imageimageimage

 

这里要指出的是,我个人并不喜欢用这个Toolkit提供的项目模板和代码段。我觉得它所生成的代码有些冗余,修改起来反而麻烦。我真正的项目中就不用这些模板,而是倾向于自己编写ViewModel之类的。这可能也跟我之前并不是一开始就使用MVVMLight有关系,我更习惯自己写那些代码,我指的是一些ViewModel的属性,命令和绑定等等。

 

好吧,我承认,我为什么不喜欢使用Toolkit的模板呢?还有一个原因是,除非必要,我对于工具要给Visual Studio添加额外的东西(哪怕是有用的),总是很敏感的,我担心它让Visual Studio变慢。是的,你知道,这多少有点类似“洁癖”的嫌疑,但怎么说呢,让我保留自己这个权利吧

 

那么,如果你像我一样,不安装MVVMLight Toolkit,如何使用MVVMLight呢?实际上很简单,我更加习惯于使用nuget package 来获取最新的MVVMLight的Library,并将它们添加到项目中来。

image

你可以通过这个菜单打开nuget package explorer,如下图所示,然后,你可以在Online里面搜索MVVMLight,或者像我这样在Recent package中直接就可以Install。(nuget package是会被缓存在本地的,所以即便没有链接到网络,也可以正常使用)

image

既然可以缓存在本地,那么其实和安装到GAC是没有太大区别的,不是吗?

而且用这种方式还有一个好处,你总是可以得到最新的版本,因为nuget package是自动有更新提示的。而如果你是用Toolkit的话,则得不到更新的提示。(据可靠消息,MVVMLight将很快有4.0这个版本)

 

很好,你现在已经知道如何将MVVMLight添加到项目中,接下来就是该让它发挥威力的时候啦。大家一定要理解MVVM的两个核心目标

1.让UI界面与逻辑能够很好地分离又协同工作。

2.让逻辑代码更具有可测试性。

 

我们先来说说分离并协同这个目标,在MVVMLight中主要通过什么实现的呢?它提供了ViewModelBase这个基类,可以让我们很方便地编写ViewModel。从下面的截图可以看出,它提供了很多有用的特性,例如判断是否在设计状态(IsInDesignMode),以及触发属性更改通知(RaisePropertyChanged),尤其是后者,这可以说是MVVM的根基,为什么这么说呢?UI与逻辑的分离并且协同工作,关键就在于WPF和Silverlight有强大的数据绑定机制,而数据绑定机制之所以能够强大,就是因为WPF和Silverlight中引入的依赖属性(Dependency Property)的机制,而依赖属性,区别于普通属性的最重要一点就是既可以有单向绑定,也可以有双向绑定,而且属性更改之后,可以通知到所有绑定目标上面。

image

 

除了很好的支持绑定,UI与逻辑分离并协作的另外一个重要机制,就是命令(command)机制。在MVVMLight中,它提供了两个基本的命令:RelayCommand和RelayCommand<T>

image

image

这两个命令其实没有本质区别,只不过后者是支持泛型的一个参数的,就是可以从命令源接受参数数据。

需要注意的是,这两个命令只适合绑定在基于按钮的Click事件上面。例如Button,HyperlinkButton是最常见的。例如下面的例子

image

这个绑定的意思,其实就是说,当这个Button被点击了之后,调用ViewModel中的SaveCommand

如果需要传递参数过去呢,就是下面这样啦。我举了两个例子,第一个例子参数是一个常数,而第二个例子参数是一个绑定值,这都是允许的

image

但问题是,如果我要绑定其他事件呢?例如MouseMove事件,该怎么办呢?在MvvmLight.Extras这个程序集里面,单独又给出了一个Command绑定方式,叫EventToCommand,顾名思义,它可以将任何事件绑定到一个命令

image

要使用这个略微麻烦一些,请看下面的例子

image

所以,绑定(尤其是双向绑定)和命令是MVVM的精髓,但实际要认真讲起来,MVVMLight这方面实现得其实也没有什么特别突出的,其他一些框架也都是这么做的。以前没有这些框架之前,我们也是这么写的,无非是代码会多一些而已。

有童鞋可能会说了,属性绑定我们可以理解,但为嘛要这么麻烦去绑定命令呢?直接在xaml.cs里面写不就完了吗?请注意,MVVM的一个目标就是让xaml.cs代码中尽量少,极端的情况是没有任何用户代码。这样才能实现UI与逻辑的分离,所以尽可能地用Command来做。

这里我也分享我的个人经验,一定会有的时候,你没有办法全部用Command,而不在xaml.cs中写任何代码。那个时候,你也大可像我一样,将代码写一些在xaml.cs中也无妨。典型的情况,是希望在视图里面接受消息(下面就要讲到),并且更新界面的一些效果,例如启动动画。这里面是一个度的把握,并无绝对的好坏。我已经看到有人心领神会地点头了,所谓随机应变,大家要有一定的灵活性。

 

不过,Mvvmlight的一个创造性的设计,是它的Message(消息)机制它让View和ViewModel,以及ViewModel之间通讯变得相当方便,甚至神奇。我相当欣赏这个设计,这是Mvvmlight之所以称为Mvvmlight的原因。

具体来说,它提供了一个Messenger类型,可以用来发送和接收消息,它还提供了默认的几种消息类型。

A Messenger class (and diverse message types) to be used to communicate within the application. Recipients only receive the message types that they register for. Additionally, a target type can be specified, in which case the message will only be transmitted if the recipient's type matches the target parameter.
Messages can be anything from simple values to complex objects. You can also use specialized message types, or create your own types deriving from them.
More information about the Messenger class.

  • MessageBase: A simple message class, carrying optional information about the message's sender.
  • GenericMessage<T>: A simple message with a Content property of type T.
  • NotificationMessage: Used to send a notification (as a string) to a recipient. For example, save your notifications as constant in a Notifications class, and then send Notifications.Save to a recipient.
  • NotificationMessage<T>: Same as above, but with a generic Content property. Can be used to pass a parameter to the recipient together with the notification.
  • NotificationMessageAction: Sends a notification to a recipient and allows the recipient to call the sender back.
  • NotificationMessageAction<T>: Sends a notification to a recipient and allows the recipient to call the sender back with a generic parameter.
  • DialogMessage: Used to request that a recipient (typically a View) displays a dialog, and passes the result back to the caller (using a callback). The recipient can choose how to display the dialog, either with a standard MessageBox, with a custom popup, etc…
  • PropertyChangedMessage<T>: Used to broadcast that a property changed in the sender. Fulfills the same purpose than the PropertyChanged event, but in a less tight way.

一个稍微具体一点的例子,请参考

image

我非常喜欢这个Messenger的功能,但同时,我个人觉得它的设计有值得改进之处,首先它的语法有点繁琐了,不是吗?

我们显然更希望用下面这样的语法

image

这是如何实现的呢,其实我是自己对Messenger做了一个扩展

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using GalaSoft.MvvmLight.Messaging;

namespace WpfApplication1
{

    /// <summary>
    /// 对默认的Messenger做扩展,以便更加易于使用
    /// 作者:陈希章
    /// </summary>
    public static class MessengerExtension
    {
        public static void Send<T>(this IMessenger messenger, T body, object token)
        {
            Messenger.Default.Send<GenericMessage<T>>(new GenericMessage<T>(body), token);
        }

        public static void Register<T>(this Messenger messenger, object recipient, object token, Action<T> action)
        {
            Messenger.Default.Register<GenericMessage<T>>(recipient, token, msg => {
                action(msg.Content);
            });
        }
    }
}

 

关于Messenger,其次我还觉得,它定义那么多消息类型,并不是非常理想,容易把使用者搞晕(我其实也不是很理解为什么既要做一个GenericMessage,还有一个NotificationMessage等等)。这也是我用上面这样的方式扩展的原因。我后面会整理一个扩展代码,做成可以分享的package给大家使用。

 

讲了这么多,其实还有一个经常被大家忽视的目标:可测试性。这是很重要的。如何理解MVVM的可测试性,以及在MVVMLight中的具体实现呢?

我们来看一个例子,我们通常会说这是一个不可测试的代码例子

image

为什么说它是不可测试的呢?因为我们都知道,MessageBox是需要人去响应的,你要点击一下才会被关闭掉。而我们的测试(包括单元测试),大多都是要能批量,自动运行的,那么遇到这种MessageBox怎么办呢?

我们一般单元测试代码会这么样写

image

运行起来之后,它确实会按照预期的那样去执行代码,很显然它会弹出一个对话框,让我们去点击

image

点击了之后,当然测试会通过。但问题是,如果测试还需要人工干预才能运行,显然不利于自动化。

我们来看在MVVMLight中如何解决这个问题的。我们得捋一下思路:你的目的是要弹出一个对话框(或者类似的东西),但如果你必须用MessageBox的话,就肯定是会弹出那个对话框来。有什么办法可以解决这个问题呢?

答案就是:MvvmLight提供的Messenger机制。我们来看如下的例子

image

那么,这个消息会被谁来响应呢?一般是在View里面去响应,仔细想想:显示消息(以及如何显示)其实是View的责任,与ViewModel没有什么关系。

image

很好,这样就是MVVM的做法了,那么我们再来运行测试看看会怎么样呢?大家如果自己运行一下就知道了,测试直接通过了,没有任何消息提示。

等等,这难道就说明我们做对了吗?我们的测试中,怎么确认消息发出去了呢?也就是说,既然上面的代码并不会弹出消息,你怎么确认那个方法里面发送了消息呢?

所以,好戏一般都在后头,不要着急下结论。所以可测试性,是指MvvmLight为此类问题都准备了解决方案。我们如何确认SaveCommand里面肯定调用而且仅仅调用了一次Messenger.Send方法呢?

很显然,我们得有一个什么方式,模拟Messenger的功能:我们并不真的去发消息,我们是要验证发送消息的方法真的被调用,而且发的消息内容是不是“保存成功”,这就是我们测试的目的。

在这里,我们会用到一个模拟的框架,我最喜欢用的是Moq这个框架。这也是一个开源项目,它的官方网站是 http://code.google.com/p/moq/

同样,我们可以通过nuget package explorer中获取它,实在是很方便,不是吗?

image

 

然后,我们编写下面的测试代码

using WpfApplication1;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Windows.Input;

using Moq;
using GalaSoft.MvvmLight.Messaging;

namespace TestProject1
{
    
    
    /// <summary>
    ///This is a test class for MainWindowViewModelTest and is intended
    ///to contain all MainWindowViewModelTest Unit Tests
    ///</summary>
    [TestClass()]
    public class MainWindowViewModelTest
    {


        /// <summary>
        ///A test for SaveCommand
        ///</summary>
        [TestMethod()]
        public void SaveCommandTest()
        {
            MainWindowViewModel target = new MainWindowViewModel(); // TODO: Initialize to an appropriate value
            var messenger = new Mock<Messenger>();
            messenger.Setup(m => m.Send(It.Is<DialogMessage>(d => d.Content == "保存成功"))).Verifiable();
            Messenger.OverrideDefault(messenger.Object);

            var cmd = target.SaveCommand;
            cmd.Execute(null);

            messenger.Verify();

        }
    }
}

上面的代码很好理解,我们希望验证Messenger的Send方法是否被调用,而且发送的消息是不是一个DialogMessage,内容是不是“保存成功”。moq的特点就是语义很通俗易懂,让我们为它鼓掌。

再次运行测试的话,我们会发现这次也还是正常通过了测试。但如果,我们将ViewModel方法里面的那句发送消息的代码注释掉,则就会报告一个错误

image

我们甚至还可以验证Send方法调用了多少次,诸如此类,这是moq的功能,这里就不多展开了。

 

 

写在最后的话

感谢Laurent Bugnion 的杰出工作,他是微软MVP,我也看过他的视频,讲解MVVMLight及其原理和使用的,蛮平易近人的,典型的程序员和技术发烧友吧。有一个视频上面,他穿的一间黑色T恤,上面就写着几个字:geek, 极客,你懂的

Laurent还将源代码发布到了Codeplex,你可以通过下面这里下载到

http://mvvmlight.codeplex.com/

 

顺便做一个小的调查,MVVMLight是完全免费的,包括源代码。但作者也希望得到捐赠(我看过很多不错的开源项目都接受捐赠),我想请问在读这篇文章的各位,你有没有曾几何时捐赠过任何的开源项目呢?捐赠了多少?是什么让你做出捐赠的决定呢?

其实以我所看到的,我也知道在国内,捐赠的这种形式并不常见,所以这个调查纯属满足我的好奇心,谢谢啦Hot smile

我自己而言,以前也确实没有捐赠过任何的开源项目。但我现在的想法是,如果确实有相当好的开源项目,我有心是要适当地捐赠的。

posted @ 2011-10-01 21:23  陈希章  阅读(54583)  评论(70编辑  收藏  举报