65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF:如果 Carlsberg 做了 MVVM 框架:第 5 部分(共 n 部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (46投票s)

2009年8月8日

CPOL

15分钟阅读

viewsIcon

138157

大概会是 Cinch,一个用于 WPF 的 MVVM 框架。

目录

Cinch 文章系列链接

介绍

上次,我们开始研究使用 Cinch 时典型的 Model 和 ViewModel 可能包含的内容。

在本文中,我将讨论以下内容

先决条件

演示应用程序使用了

  • VS2008 SP1
  • .NET 3.5 SP1
  • SQL Server(请参阅 MVVM.DataAccess 项目中的 README.txt 以了解演示应用程序数据库需要设置什么)

特别感谢

我想唯一的方法就是开始,那么我们开始吧,好吗?但在我们开始之前,我只需要重复一下特别感谢部分,并增加一位:Paul Stovell,我上次忘了把他包括进来。

在我开始之前,我特别要感谢以下人员,没有他们,本文以及后续的系列文章将不可能实现。基本上,我通过研究了这些人的大部分工作,看到了什么有效,什么无效,然后推出了 Cinch,我希望它能填补其他框架未覆盖的新领域。

  • Mark Smith(Julmar Technology)的优秀 MVVM 辅助库,它极大地帮助了我。Mark,我知道我曾征求你同意使用你的一些代码,你非常慷慨地同意了,但我只想感谢你那些很棒的想法,其中一些我确实没想过。我对此非常佩服。
  • Josh Smith / Marlon Grech(作为一个整体)他们出色的中介者实现。你们俩太棒了,一直都很愉快。
  • Karl Shifflett / Jaime Rodriguez(微软的员工)的优秀 MVVM Lob 之旅,我曾参加过。小伙子们干得好!
  • Bill Kempf,他就是 Bill,一个疯狂的编程奇才,他也拥有一个很棒的 MVVM 框架,名为 Onyx,我曾在他身上写过 一篇文章。Bill 总是能回答棘手的问题,谢谢 Bill。
  • Paul Stovell 他出色的 委派验证想法,Cinch 使用它来验证业务对象。
  • 所有 WPF Disciples 的成员,在我看来,是最好的在线社区。

谢谢你们,伙计/女孩,你们懂的。

单元测试

本文的重点将是如何编写单元测试,尽可能多地测试你的 ViewModel/Model 代码。一位智者曾说过:给我看你的单元测试,如果它们足够好,我将能够理解应用程序是如何工作的。

我不是要告诉你应该做多少单元测试,那是你的选择。但我可以告诉你的是,Cinch 背后的主要思想是使 ViewModel/Model 尽可能易于测试。我不得不说,我对实际可测试的内容感到非常满意。

本文的其余部分将讨论你可以用来测试自己的 ViewModel/Model 类的测试技术,当然,你也可以检查附加的演示代码,其中包含一套完整的 NUnit 测试,这些测试会测试演示应用程序的所有功能。

测试命令

ViewModel 中最小但完整的可测试单元之一将是暴露给 UI 的 ICommand。我喜欢设置单元测试的方式是主要测试 ViewModel 中暴露的 ICommand 属性,因为这些属性是实际触发 ViewModel 中暴露这些 ICommand 属性的操作。

使用 Cinch 中的 ICommand 接口实现 SimpleCommand,这再简单不过了,因为它甚至还公开了一个 CommandSucceeded 属性,单元测试可以在公开的 ICommand 运行后对其进行检查。

所以,假设你有一个 ViewModel 设置如下,并且有一个需要测试的公开 ICommand

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;

using Cinch;
using MVVM.Models;

namespace MVVM.ViewModels
{
    public class MainWindowViewModel : Cinch.ViewModelBase
    {
        private SimpleCommand doSomethingCommand;

        public MainWindowViewModel()
        {
            //Create DoSomething command
            doSomethingCommand = new SimpleCommand
            {
                CanExecuteDelegate = x => CanExecuteDoSomethingCommand,
                ExecuteDelegate = x => ExecuteAddCustomerCommand()
            };
        }

        public SimpleCommand DoSomethingCommand 
        {
            get { return doSomethingCommand; }
        }

        private Boolean CanExecuteDoSomethingCommand
        {
            get
            {
                return true;
            }
        }

        private void ExecuteDoSomethingCommand()
        {
            DoSomethingCommand.CommandSucceeded = false;
            
        //DO SOMETHING
        //DO SOMETHING
            DoSomethingCommand.CommandSucceeded = true;
        }
    }
}

在单元测试中,测试此公开 ICommand 的最简单方法是执行以下操作:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;

using MVVM.ViewModels;
using Cinch;

namespace MVVM.Test
{
    [TestFixture]
    public class Tests
    {
        [Test]
        public void MainWindowViewModel_DoSomethingCommand_Test()
        {
            MainWindowViewModel mainWindowVM = new MainWindowViewModel();
           
            mainWindowVM.DoSomethingCommand.Execute(null);
            Assert.AreEqual(mainWindowVM.DoSomethingCommand.CommandSucceeded, true);
        }
    }
}

从上面的例子可以看出,这仅仅是获取正确的 ICommand,然后执行它,并查看 CommandSucceeded 属性是否设置为 true。如果你不喜欢依赖简单的布尔标志,ICommand 通常会做一些事情,例如删除项目或添加项目,因此你可以检查该结果状态而不是,这将验证 ICommand 是否成功运行;这取决于你。

请注意,单元测试非常细粒度,它只测试一个公开的 ICommand,这正是我推荐的做法。

测试 Mediator

如果你还记得 第二部分,Cinch 中的 ViewModelBase 类提供了一个消息发送/接收机制,这被称为 Mediator。为了让你回忆起来,这里有一张图展示了它的工作原理:

我们可以设想我们有一段 ViewModel 代码,它通过 Mediator 发送一条消息,例如:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Windows.Threading;
using System.Windows.Data;

using Cinch;
using MVVM.Models;
using MVVM.DataAccess;

namespace MVVM.ViewModels
{
    /// <summary>
    /// Summy ViewModel that send messages using Mediator
    /// </summary>
    public class MediatorSendViewModel : Cinch.ViewModelBase
    {
        #region Ctor
        public MediatorSendViewModel()
        {
            //send a Message using Mediator
            Mediator.NotifyColleagues<MediatorSendViewModel>(
                "MediatorSendViewModelCreated",
                this);
        }
        #endregion
    }
}

并且我们有一些 ViewModel 代码对接收此消息感兴趣,例如:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Windows.Threading;
using System.Windows.Data;

using Cinch;
using MVVM.Models;
using MVVM.DataAccess;

namespace MVVM.ViewModels
{
    /// <summary>
    /// Summy ViewModel that receives messages using Mediator
    /// </summary>
    public class MediatorReceiveViewModel : Cinch.ViewModelBase
    {
        #region Data
        private List<MediatorSendViewModel> itemsCreated = 
                new List<MediatorSendViewModel>();
        #endregion

        #region Ctor
        public MediatorReceiveViewModel()
        {

        }
        #endregion

        #region Mediator Message Sinks
        [MediatorMessageSink("MediatorSendViewModelCreated")]
        private void MediatorSendViewModelCreatedMessageSink(
            MediatorSendViewModel theNewObject)
        {
            itemsCreated.Add(theNewObject);
        }
        #endregion

        #region Public Properties
        static PropertyChangedEventArgs itemsCreatedChangeArgs =
             ObservableHelper.CreateArgs<MediatorReceiveViewModel>(x => x.ItemsCreated);

        public List<MediatorSendViewModel> ItemsCreated
        {
            get { return itemsCreated; }
            set
            {
                if (itemsCreated == null)
                {
                    itemsCreated = value;
                    NotifyPropertyChanged(itemsCreatedChangeArgs);
                }
            }
        }
        #endregion
    }
}

那么我们如何测试这种交互呢?

嗯,这相当容易。归根结底,这些交互只是类,所以我们只需要检查消息接收的结果,看看消息载荷是否达到了预期的目的。以下是一个测试上述发送/接收的单元测试示例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;

using MVVM.DataAccess;
using MVVM.ViewModels;
using MVVM.Models;
using Cinch;
using System.Threading;

namespace MVVM.Test
{
    /// <summary>
    /// Tests the Mediator functionality
    /// </summary>
    [TestFixture]
    public class Mediator_Tests
    {
        [Test]
        public void TestMediator()
        {
            MediatorReceiveViewModel receiver = new MediatorReceiveViewModel();
            Assert.AreEqual(receiver.ItemsCreated.Count, 0);

            MediatorSendViewModel sender = new MediatorSendViewModel();
            Assert.AreEqual(receiver.ItemsCreated.Count, 1);
        }
    }
}

测试后台工作线程任务

虽然多线程很困难,但我已尽力确保 Cinch 中至少有一种可测试的后台工作线程选项。这 comes in the form of the BackgroundTaskManager<T> class,该类在之前的文章中已详细讨论。

我将不深入介绍 BackgroundTaskManager<T> 类的内部工作原理,因为这在之前的文章中已经讲过了。现在,假设我们有一个 Cinch ViewModel,它设置了一个后台任务(ICommand 调用 LazyFetchOrdersForCustomer() 方法,为简洁起见省略了),该任务延迟加载选定客户的订单,看起来像这样:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Windows.Threading;
using System.Windows.Data;

using Cinch;
using MVVM.Models;
using MVVM.DataAccess;

namespace MVVM.ViewModels
{

    public class AddEditCustomerViewModel : Cinch.WorkspaceViewModel
    {
        private BackgroundTaskManager<DispatcherNotifiedObservableCollection<OrderModel>> 
            bgWorker = null;

        private SimpleCommand doSomethingCommand;

        public AddEditCustomerViewModel()
        {
            //Create DoSomething command
            doSomethingCommand = new SimpleCommand
            {
                CanExecuteDelegate = x => CanExecuteDoSomethingCommand,
                ExecuteDelegate = x => ExecuteAddCustomerCommand()
            };
            //setup background worker
            SetUpBackgroundWorker();
        }

        public SimpleCommand DoSomethingCommand 
        {
            get { return doSomethingCommand; }
        }
     
        static PropertyChangedEventArgs bgWorkerChangeArgs =
            ObservableHelper.CreateArgs<AddEditCustomerViewModel>(x => x.BgWorker);

        public BackgroundTaskManager<DispatcherNotifiedObservableCollection<OrderModel>> BgWorker
        {
            get { return bgWorker; }
            set
            {
                bgWorker = value;
                NotifyPropertyChanged(bgWorkerChangeArgs);
            }
        }
        
        private void SetUpBackgroundWorker()
        {
            bgWorker = new BackgroundTaskManager<DispatcherNotifiedObservableCollection<OrderModel>>(
                () =>
                {
                    //Do Background Work
                },
                (result) =>
                {

                    //Use background worker results
                    
                    //Store Count
                    this.Count = result.Count;
                });
        }

        /// <summary>
        /// Fetches all Orders for customer
        /// </summary>
        private void LazyFetchOrdersForCustomer()
        {
            if (CurrentCustomer != null &&
                CurrentCustomer.CustomerId.DataValue > 0)
            {
                bgWorker.RunBackgroundTask();
            }
        }
        
        private Boolean CanExecuteDoSomethingCommand
        {
            get
            {
                return true;
            }
        }

        private void ExecuteDoSomethingCommand()
        {
            DoSomethingCommand.CommandSucceeded = false;
            LazyFetchOrdersForCustomer();
            DoSomethingCommand.CommandSucceeded = true;
        }
    }
}

你认为我们该如何进行单元测试?我们实际上是在后台执行一些操作,期望它能完成,但我们不知道它需要多长时间。然而,我们需要我们的单元测试能够正常进行,并等待合理的时间限制,或者直到被告知后台任务已完成。

虽然这听起来很难,但 System.Threading 命名空间中有几个类可以帮助我们,而实际的 BackgroundTaskManager<T> 类也有一个可以帮助我们的机制。

我不会深入介绍 System.Threading 命名空间中的类,但你绝对应该查找

  • ManualResetEvent

这是一个用于线程同步的 WaitHandle 对象。 

所以,继续前进,让我们看看 Cinch 在单元测试和后台多线程方面允许你做什么。

将 Completed 事件与 WaitHandle 结合使用

使用 ManualResetEvent WaitHandle,它由 BackgroundTaskManager<T> 完成事件设置,以向 WaitHandle 发出信号,表明后台任务已完成。

这是一个不错的选择,因为测试的读者可以看到 BackgroundTaskManager<T> 类的 Completed 事件处理程序,因此单元测试可能更具可读性,而在处理多线程时,这总是有帮助的。还可以为 WaitHandle 指定一个可选的超时和退出条件,这意味着我们不必无限期等待。

这是一个例子。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;

using MVVM.ViewModels;
using Cinch;

namespace MVVM.Test
{
    [TestFixture]
    public class Tests
    {
        [Test]
        public void AddEditCustomerViewModel_BgWorker_Test()
        {
            Int32 EXPECTED_VALUE =100;

            AddEditCustomerViewModel addEditCustomerVM = 
                                     new AddEditCustomerViewModel();
            
            //Set up WaitHandle
            ManualResetEvent manualResetEvent = new ManualResetEvent(false);
            
            //Wait for the completed event, and then signal the WaitHandle that 
            //the test can continue
            addEditCustomerVM .BgWorker.BackgroundTaskCompleted += 
                              delegate(object sender, EventArgs args)
            {
               // Signal the waiting NUnit thread that we're ready to move on.
               manualEvent.Set();
            };
                        
            //Do the work (via the ICommand)
            addEditCustomerVM.DoSomethingCommand.Execute(null);
            
            //Wait for reasonable time (5 seconds, with exit
            //state set to "not signalled" on WaitHandle)
            manualResetEvent.WaitOne(5000, false);
            
            //Assert : Check if background task results
            //and expected results are the same
            Assert.AreEqual(EXPECTED_VALUE, addEditCustomerVM.Count)
        }
    }
}

服务

还记得 第三部分,我们讨论了服务以及我们如何不仅拥有 WPF 服务实现,还拥有单元测试版本。以下子节将展示如何使用单元测试服务实现来正确测试 Cinch Model/ViewModel。

注意:不要忘记,单元测试服务实现是通过 Dependency Injection 注入到 Unity IOC 容器中的,使用的设置是当前测试应用程序的 App.Config。你可以在附加的演示应用程序中看到一个例子。

测试使用测试 IMessageBox 服务

正如 Cinch 系列中其他几篇文章所述,Cinch 与使用 Mock 对象不同之处在于,Cinch 能够通过使用一组回调委托(Func<T,TResult>)来完全覆盖任何所有 ViewModel 代码,这些回调委托在单元测试中设置。例如,假设我们有一段代码;在 ViewModel 中链接此代码以进行测试:

var messager = this.Resolve<IMessageBoxService>();

if (messager.ShowYesNo("Clear all items?", CustomDialogIcons.Question)
    == CustomDialogResults.Yes)
{
    if (messager.ShowYesNo("This procedure can not be undone, Proceed", 
        CustomDialogIcons.Question)  == CustomDialogResults.Yes)

        stuff.Clear();
}

使用 Cinch 提供的单元测试 IMessageBoxService 实现和单元依赖注入(在之前的 Cinch 文章中讨论过)的组合,我们可以用 Mock 对象无法实现的方式来测试这段代码。

例如,使用 Mock 对象,我们肯定能够满足第一个条件,因为实际的 IMessageBoxService 实现将被 Mock 以提供单个 CustomDialogIcons。但第二个条件呢?怎么处理?答案在于你在单元测试中设置的回调委托。通过使用这种方法,完全有可能将 ViewModel 代码驱动到任何你想要的路径,并实现完整的代码覆盖。

为了实现完整的代码覆盖,我会进行两个测试:一个提供 CustomDialogResults.Yes,然后是 CustomDialogResults.No,所以我预期在该测试中 stuff.Count 不会为零。

然后我会进行另一个测试,其中单元测试提供 CustomDialogResults.Yes,然后是另一个 CustomDialogResults.Yes,所以我预期 stuff.Count 为零。

以下是这两个测试的示例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;

using MVVM.ViewModels;
using Cinch;

namespace MVVM.Test
{
    [TestFixture]
    public class Tests
    {
        [Test]
        public void ShouldNotClearStuff_Tests()
        {
            //Setup some hyperthetical ViewModel
            SomeViewModel x = new SomeViewModel();

            //Resolve service to get Test service implementation
            TestMessageBoxService testMessageBoxService =
                (TestMessageBoxService)
                    ViewModelBase.ServiceProvider.Resolve<IMessageBoxService>();

            //Queue up the responses we expect for our given TestMessageBoxService
            //for a given ICommand/Method call within the test ViewModel

            testMessageBoxService.ShowYesNoResponders.Enqueue
                (() =>
                    {
    
                        return CustomDialogResults.Yes;
                    }
                );

           testMessageBoxService.ShowYesNoResponders.Enqueue
                (() =>
                    {
    
                        return CustomDialogResults.No;
                    }
                );

            Int32 oldStuffCount = x.Stuff.Count;

            //Execute some hyperthetical command
            x.SomeCommand.Execute(null);

            //Make sure clear did not work, as the ViewModel should not have cleared the
            //Stuff list, unless it saw Yes -> Yes
            Assert.AreEqual(x.Stuff.Count, oldStuffCount );
        }

        [Test]
        public void ShouldClearStuff_Tests()
        {
            //Setup some hyperthetical ViewModel
            SomeViewModel x = new SomeViewModel();

            //Resolve service to get Test service implementation
            TestMessageBoxService testMessageBoxService =
                (TestMessageBoxService)
                    ViewModelBase.ServiceProvider.Resolve<IMessageBoxService>();
    
            //Queue up the responses we expect for our given TestMessageBoxService
            //for a given ICommand/Method call within the test ViewModel

            testMessageBoxService.ShowYesNoResponders.Enqueue
                (() =>
                    {
    
                        return CustomDialogResults.Yes;
                    }
                );

           testMessageBoxService.ShowYesNoResponders.Enqueue
                (() =>
                    {
    
                        return CustomDialogResults.Yes;
                    }
                );

            Int32 oldStuffCount = x.Stuff.Count;

               //Execute some hyperthetical command
               x.SomeCommand.Execute(null);

            //Make sure clear worked, as the ViewModel should have cleared the
            //Stuff list, as it saw Yes -> Yes
           Assert.AreNotEqual(x.Stuff.Count, oldStuffCount );
           Assert.AreEqual(x.Stuff.Count, 0);
        }
    }
}

如果你理解了这一点,那么接下来的几个服务也很容易理解,因为它们都遵循相同的模式。

测试使用测试 IOpenFileService 服务

IOpenFileService 的工作方式与 IMessageBoxService 非常相似,不同之处在于它需要处理文件名和一个来自对话框的 bool? 结果(当然,在单元测试实现中没有对话框,它的工作方式与上面演示的几乎相同)。

所以,对于 ViewModel 中的这段代码:

var ofd = this.Resolve<IOpenFileService>();

bool? result = ofd.ShowDialog(null);

if (result.HasValue && result)
{
    File.Open(ofd.FileName);
    ....
    ....
}

我们可以在测试中处理这个问题,并向 ViewModel 提供一个有效的文件名(就像用户选择了实际文件一样),如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;

using MVVM.ViewModels;
using Cinch;

namespace MVVM.Test
{
    [TestFixture]
    public class Tests
    {
        [Test]
        public void OpenSomeFile_Tests()
        {
            //Setup some hyperthetical ViewModel
            SomeViewModel x = new SomeViewModel();

            //Resolve service to get Test service implementation
            TestOpenFileService testOpenFileService =
                (TestOpenFileService)
                    ViewModelBase.ServiceProvider.Resolve<IOpenFileService>();
    
            //Queue up the responses we expect for our given TestOpenFileService 
            //for a given ICommand/Method call within the test ViewModel
            testMessageBoxService.ShowDialogResponders.Enqueue
                (() =>
                    {
                      testOpenFileService.FileName = @"c:\test.txt";
                      return true
                    }
                );

            //Do some testing based on the File requested
            .....
            .....
            .....
            .....
        }
    }
}

测试使用测试 ISaveFileService 服务

ISaveFileService 的工作方式与 IOpenFileService 非常相似。

所以,对于 ViewModel 中的这段代码:

var sfd = this.Resolve<ISaveFileService>();

bool? result = ofd.ShowDialog(null);

if (result.HasValue && result)
{
    File.Create(sfd.FileName);
    ....
    ....
}

我们可以在测试中处理这个问题,并向 ViewModel 提供一个有效的文件名(就像用户选择了实际的文件保存位置一样),如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;

using MVVM.ViewModels;
using Cinch;

namespace MVVM.Test
{
    [TestFixture]
    public class Tests
    {
        [Test]
        public void OpenSomeFile_Tests()
        {
            //Setup some hyperthetical ViewModel
            SomeViewModel x = new SomeViewModel();

            //Resolve service to get Test service implementation
            TestSaveFileService testSaveFileService =
                (TestSaveFileService)
                    ViewModelBase.ServiceProvider.Resolve<ISaveFileService>();
    
            //Queue up the responses we expect for our given TestSaveFileService 
            //for a given ICommand/Method call within the test ViewModel

            testMessageBoxService.ShowDialogResponders.Enqueue
                (() =>
                    {
                        string path = @"c:\test.txt";
                        testSaveFileService.FileName = path ;
                        return true;
                    }
                );

            //Do some testing based on the File save location the Unit test set up
            .....
            .....
            .....
            .....
        }
    }
}

测试使用测试 IUIVisualizerService 服务

还记得 第三部分第四部分,我们有一个可以用来显示弹出窗口的服务。当你想到弹出窗口通常用于什么时,你可能会开始理解 IUIVisualizerService 的实际作用。

你看,通常会打开一个弹出窗口,并给它一些对象(通常是一个 Model/ViewModel,它刚刚存储了当前状态,例如使用我们 第二部分第四部分 中看到的 IEditableObject 支持),这个对象代表当前状态,然后弹出窗口会修改当前状态对象,并返回 true(DialogResult.Ok)或 false(用户取消了弹出操作),此时启动弹出窗口的对象必须决定如何处理其自身的状态/它传递给弹出窗口的那个对象的状态。

演示应用程序处理此问题的方式如下:

  • ViewModel 有一个支持 IEditableObject 的 Model(s),所以在弹出窗口显示之前,Model(s) 被调用来 BeginEdit(),这会将当前状态存储在内部存储中。请记住,我更喜欢使用 Model,但上次在 第四部分 中,我也说过 Cinch 允许 ViewModel 支持 IEditableObject,所以如果你更喜欢这种方法,此时你应该调用当前 ViewModel 的 BeginEdit()
  • 接下来,使用 IUIVisualizerServices 显示弹出窗口,传入一些状态(通常是一个 ViewModel),我们已经备份了,这要归功于我们在显示弹出窗口之前通过 IEditableObject.BeginEdit() 方法捕获的状态快照。
  • 用户与弹出窗口交互,更改值等,这些都会影响当前的弹出窗口状态,这个状态是你传入的一个 Model,或者是一个 ViewModel,如果你喜欢的话,然后用户按下:
    • OK:然后关闭弹出窗口,弹出窗口的状态已由弹出窗口上的操作修改,用户满意并希望接受。
    • Cancel:然后关闭弹出窗口,并使用 IEditableObject.CancelEdit() 方法回滚传递给弹出窗口的对象的状态。你就回到了显示弹出窗口之前的状态。

想象一下,我的 ViewModel 代码看起来像这样来显示弹出窗口:

private void ExecuteEditOrderCommand()
{
    EditOrderCommand.CommandSucceeded = false;
    addEditOrderVM.CurrentViewMode = ViewMode.EditMode;            
    CurrentCustomerOrder.BeginEdit();
    addEditOrderVM.CurrentCustomer = CurrentCustomer;
    bool? result = uiVisualizerService.ShowDialog("AddEditOrderPopup", 
                                                  addEditOrderVM);

    if (result.HasValue && result.Value)
    {
        CloseActivePopUpCommand.Execute(true);
    }
    EditOrderCommand.CommandSucceeded = true;
}

从上面的解释来看,现在很明显,这里发生的一切就是我们拍摄了 Customer.Order 状态的快照,然后显示了弹出窗口。为了完整起见,这里是弹出窗口的“保存”按钮(DialogResult.Ok 为 true)命令的代码(当然,这实际上是在 ViewModel 中):

private void ExecuteSaveOrderCommand()
{
    try
    {
        SaveOrderCommand.CommandSucceeded = false;

        if (!CurrentCustomerOrder.IsValid)
        {
            messageBoxService.ShowError("There order is invalid");
            SaveOrderCommand.CommandSucceeded = false;
            return;
        }

        //Order is valid, so end the edit and try and save/update it
        this.CurrentCustomerOrder.EndEdit();

        switch (currentViewMode)
        {
            #region AddMode
            ......
            ......
            ......
            ......
            #endregion
            #region EditMode
            //EditMode
            case ViewMode.EditMode:
                Boolean orderUpdated =
                    DataService.UpdateOrder(
                        TranslateUIOrderToDataLayerOrder(CurrentCustomerOrder));

                if (orderUpdated)
                {
                    messageBoxService.ShowInformation(
                        "Sucessfully updated order");
                    this.CurrentViewMode = ViewMode.ViewOnlyMode;
                }
                else
                {
                    messageBoxService.ShowError(
                        "There was a problem updating the order");
                }
                SaveOrderCommand.CommandSucceeded = true;
                //Use the Mediator to send a Message to AddEditCustomerViewModel to tell it a new
                //or editable Order needs actioning
                Mediator.NotifyColleagues<Boolean>("AddedOrderSuccessfullyMessage", true);
                break;
            #endregion
        }
    }
    catch (Exception ex)
    {
        Logger.Log(LogType.Error, ex);
        messageBoxService.ShowError(
            "There was a problem saving the order");
    }
}

可以看到,这实际上是保存(为清晰起见已删除)或编辑一个 Order 对象。那么 Cancel 按钮(从弹出窗口返回 false)呢?我们来看一下:

private void ExecuteCancelOrderCommand()
{
    CancelOrderCommand.CommandSucceeded = false;
    switch (CurrentViewMode)
    {
        case ViewMode.EditMode:
            this.CurrentCustomerOrder.CancelEdit();
            CloseActivePopUpCommand.Execute(false);
            CancelOrderCommand.CommandSucceeded = true;
            break;
        default:
            this.CurrentCustomerOrder.CancelEdit();
            CancelOrderCommand.CommandSucceeded = true;
            break;
    }
}

很抱歉稍微离题了,但我认为这是必要的,这样你才能理解我们将要介绍的 ViewModel 代码和单元测试代码。

那么,我们如何在单元测试中执行与实际弹出窗口相同的事情呢?嗯,实际上相当容易。让我们在单元测试中尝试复制我们在实际弹出窗口中所做的操作。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;

using MVVM.ViewModels;
using Cinch;

namespace MVVM.Test
{

    [TestFixture]
    public class Tests
    {
        [Test]
        public void OrderModel_SaveState_Tests()
        {
            SomeViewModel vm = new SomeViewModel();

            //get old value
            Int32 oldQuantity = vm.CurrentCustomerOrder.Quantity.DataValue;
            //uses Datarapper

            //get test service
            TestUIVisualizerService testUIVisualizerService =
              (TestUIVisualizerService)
                ViewModelBase.ServiceProvider.Resolve<IUIVisualizerService>();

            //Queue up the response we expect for our given TestUIVisualizerService
            //for a given ICommand/Method call within the test ViewModel
            testUIVisualizerService.ShowDialogResultResponders.Enqueue
            (() =>
               {
                //simulate here any state changes you would have altered in the popup
                  //basically change the state of anything you want
                  vm.CurrentCustomerOrder.Quantity.DataValue = 56;  //uses Datarapper
                  vm.CurrentCustomerOrder.ProductId.DataValue = 2;  //uses Datarapper
            
                  return true;
               }
            );

            //Execute the command to show the popup
            vm.ExecuteEditOrderCommand.Execute(null);

            //see if state has changed
            Assert.AreNotEqual(oldQuantity, vm.CurrentCustomerOrder.Quantity.DataValue);
        }

        [Test]
        public void OrderModel_CancelEdit_Tests()
        {
            SomeViewModel vm = new SomeViewModel();

            //get old value
            Int32 oldQuantity = vm.CurrentCustomerOrder.Quantity.DataValue;
            //uses Datarapper

            //get test service
            TestUIVisualizerService testUIVisualizerService =
              (TestUIVisualizerService)
                ViewModelBase.ServiceProvider.Resolve<IUIVisualizerService>();
            
            //Queue up the response we expect for our given TestUIVisualizerService
            //for a given ICommand/Method call within the test ViewModel
            testUIVisualizerService.ShowDialogResultResponders.Enqueue
            (() =>
                {
                   //Simulate user prressing cancel, so not alter some state, 
                   //but return like user just pressed cancel
                   vm.CurrentCustomerOrder.Quantity.DataValue = 56;  //uses Datarapper
                          return false;
                }
            );

             //Execute the command to show the popup
            vm.ExecuteEditOrderCommand.Execute(null);

             //see if state has changed
            Assert.AreEqual(oldQuantity, vm.CurrentCustomerOrder.Quantity.DataValue);
         }
    }
}

测试支持 IDataErrorInfo 的对象的有效性

还记得 第二部分第四部分,我们有使用 Cinch 类 ValidatingObjectValidatingViewModelBase 来验证 Model/ViewModel 的概念。这两个类都是可验证的,这得益于 IDataErrorInfo 接口的实现。Cinch 通过在被验证的对象内部提供验证规则来处理 IDataErrorInfo 接口。下面是一个例子:

Cinch 支持以下类型的规则:

  • SimpleRule:基于委托的规则,所有验证都在回调委托中完成
  • RegxRulee:用于验证基于文本数据的正则表达式规则

那么,我们如何测试给定 IDataErrorInfo 支持对象的有效性呢?

嗯,这其实相当容易。假设我们有一个模型类(同样,如果你无法控制自己的 Model,或者更喜欢拥有一个抽象 Model 的 ViewModel,那么它也可以是 ValidatingViewModelBase),它看起来像这样:

using System;

using Cinch;
using MVVM.DataAccess;
using System.ComponentModel;

namespace MVVM.Models
{
    public class OrderModel : Cinch.EditableValidatingObject
    {
        private Cinch.DataWrapper<Int32> orderId = new DataWrapper<Int32>();
        private Cinch.DataWrapper<Int32> customerId = new DataWrapper<Int32>();
        private Cinch.DataWrapper<Int32> quantity = new DataWrapper<Int32>();
        private Cinch.DataWrapper<Int32> productId = new DataWrapper<Int32>();
        private Cinch.DataWrapper<DateTime> deliveryDate = new DataWrapper<DateTime>();
        private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;

        //rules
        private static SimpleRule quantityRule;

        public OrderModel()
        {
            #region Create DataWrappers

            OrderId = new DataWrapper<Int32>(this, orderIdChangeArgs);
            CustomerId = new DataWrapper<Int32>(this, customerIdChangeArgs);
            ProductId = new DataWrapper<Int32>(this, productIdChangeArgs);
            Quantity = new DataWrapper<Int32>(this, quantityChangeArgs);
            DeliveryDate = new DataWrapper<DateTime>(this, deliveryDateChangeArgs);

            //fetch list of all DataWrappers, so they can be used again later without the
            //need for reflection
            cachedListOfDataWrappers =
                DataWrapperHelper.GetWrapperProperties<OrderModel>(this);

            #endregion

            #region Create Validation Rules

            quantity.AddRule(quantityRule);

            #endregion    
        }
        
        static OrderModel()
        {
            quantityRule = new SimpleRule("DataValue", "Quantity can not be < 0",
                      (Object domainObject)=>
                      {
                          DataWrapper<Int32> obj = (DataWrapper<Int32>)domainObject;
                          return obj.DataValue <= 0;
                      });
        }
        
        /// <summary>
        /// Override hook which allows us to also put any child 
        /// EditableValidatingObject objects IsValid state into
        /// a combined IsValid state for the whole Model
        /// </summary>
        public override bool IsValid
        {
            get
            {
                //return base.IsValid and use DataWrapperHelper, if you are
                //using DataWrappers
                return base.IsValid &&
                    DataWrapperHelper.AllValid(cachedListOfDataWrappers);
            }
        }
        .....
        .....
        .....
        .....
        .....
    }
}

然后,我们只需要从单元测试中通过以下方式来测试此类对象的有效性:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;

using MVVM.ViewModels;
using Cinch;

namespace MVVM.Test
{
    [TestFixture]
    public class Tests
    {
        [Test]
        public void OrderModel_InValid_State_Tests()
        {
        
            OrderModel model = new OrderModel();
            model.Quantity = new DataWrapper 
            {
              DataValue = 0,
              IsEditable = true
            };
        
            //check to see if object is invalid 
            //thanks to rules we supplied in the object
            Assert.AreEqual(model.IsValid, false);
        }

        [Test]
        public void OrderModel_Valid_State_Tests()
        {
            OrderModel model = new OrderModel();
            model.Quantity = new DataWrapper 
                {
                  DataValue = 10,
                  IsEditable = true
                };

            //check to see if object is valid 
            //thanks to rules we supplied in the object
            Assert.AreEqual(model.IsValid, true);

        }
    }
}

测试支持 IEditableObject 的对象的状

还记得 第二部分第四部分,我们有使用 Cinch 类 EditableValidatingObjectEditableValidatingViewModelBase 来编辑 Model/ViewModel 的概念。这两个类都是可编辑的,这得益于 IEditableObject 接口的实现,它提供了以下三个方法:

  • BeginEdit:将当前状态存储到备份存储中
  • EndEdit:应用新值
  • CancelEdit:检索旧值并覆盖当前值,从而取消编辑

那么,我们如何测试给定 IEditableObject 支持对象的状呢?

嗯,这其实相当容易。假设我们有一个模型类(同样,如果你无法控制自己的 Model,或者更喜欢拥有一个抽象 Model 的 ViewModel,那么它也可以是 EditableValidatingViewModelBase),它看起来像这样:

using System;

using Cinch;
using MVVM.DataAccess;
using System.ComponentModel;

namespace MVVM.Models
{
    public class OrderModel : Cinch.EditableValidatingObject
    {
        private Cinch.DataWrapper<Int32> orderId = new DataWrapper<Int32>();
        private Cinch.DataWrapper<Int32> customerId = new DataWrapper<Int32>();
        private Cinch.DataWrapper<Int32> quantity = new DataWrapper<Int32>();
        private Cinch.DataWrapper<Int32> productId = new DataWrapper<Int32>();
        private Cinch.DataWrapper<DateTime> deliveryDate = new DataWrapper<DateTime>();
        private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;

        public OrderModel()
        {
            ....
            ....
            ....
            ....
        }
        ....
        ....
        ....
        ....

        /// <summary>
        /// Override hook which allows us to also put any child 
        /// EditableValidatingObject objects into the BeginEdit state
        /// </summary>
        protected override void OnBeginEdit()
        {
            base.OnBeginEdit();
            //Now walk the list of properties in the CustomerModel
            //and call BeginEdit() on all Cinch.DataWrapper<T>s.
            //we can use the Cinch.DataWrapperHelper class for this
            DataWrapperHelper.SetBeginEdit(cachedListOfDataWrappers);
        }

        /// <summary>
        /// Override hook which allows us to also put any child 
        /// EditableValidatingObject objects into the EndEdit state
        /// </summary>
        protected override void OnEndEdit()
        {
            base.OnEndEdit();
            //Now walk the list of properties in the CustomerModel
            //and call CancelEdit() on all Cinch.DataWrapper<T>s.
            //we can use the Cinch.DataWrapperHelper class for this
            DataWrapperHelper.SetEndEdit(cachedListOfDataWrappers);
        }

        /// <summary>
        /// Override hook which allows us to also put any child 
        /// EditableValidatingObject objects into the CancelEdit state
        /// </summary>
        protected override void OnCancelEdit()
        {
            base.OnCancelEdit();
            //Now walk the list of properties in the CustomerModel
            //and call CancelEdit() on all Cinch.DataWrapper<T>s.
            //we can use the Cinch.DataWrapperHelper class for this
            DataWrapperHelper.SetCancelEdit(cachedListOfDataWrappers);
        }
        #endregion
    }
}

然后,我们只需要从单元测试中通过以下方式来测试此类对象的编辑性:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;

using MVVM.ViewModels;
using Cinch;

namespace MVVM.Test
{
    [TestFixture]
    public class Tests
    {
        [Test]
        public void OrderModel_EditThenCancelEdit_Tests()
        {
            OrderModel model = new OrderModel();
            model.Quantity = new DataWrapper 
            {
              DataValue = 10,
              IsEditable = true
            };
        
            //check just assigned value        
            Assert.AreEqualmodel.Quantity.DataValue, 10);

            //Begin an Edit
            model.BeginEdit();

            //change some value
            model.Quantity = new DataWrapper 
            {
              DataValue = 22,
              IsEditable = true
            };            

            // Cancel the Edit
            model.CancelEdit(); 

            //Ensure new values are rolled back
            //to old values after a CancelEdit()
            Assert.AreEqual(model.Quantity.DataValue, 10);
        }

        [Test]
        public void OrderModel_EditThenEndEdit_Tests()
        {
            OrderModel model = new OrderModel();
            model.Quantity = new DataWrapper 
            {
              DataValue = 10,
              IsEditable = true
            };

            //check just assigned value
            Assert.AreEqual(model.Quantity.DataValue, 10);

            //Begin an Edit
            model.BeginEdit();

            //change some value
            model.Quantity = new DataWrapper 
            {
              DataValue = 22,
              IsEditable = true
            };            

            //End the Edit
            model.EndEdit(); 

            //Ensure new values is now the same as EndEdit() accepted value
            Assert.AreEqual(model.Quantity.DataValue, 22);
        }
    }
}

即将推出什么?

在后续文章中,我将大致如下展示:

  1. 使用 Cinch 的演示应用程序。
  2. 一个用于快速开发 Cinch ViewModel/Model 的代码生成器,也许还有更多,如果我还有时间的话。代码生成器将使用 Cinch 编写,因此它也将作为使用 Cinch 在你自己的项目中第二个示例。

就这些,希望你喜欢

目前我就想说这么多,但我希望从本文中,你能看到 Cinch 的发展方向以及它如何帮助你实现 MVVM。在我们继续旅程的同时,我们将学习如何使用 Cinch 开发应用程序。

谢谢

一如既往,欢迎投票/评论。

历史

  • 09/xx/xx:初次发布。
  • 09/12/05:添加了一个代码示例,解释如何使用 Cinch 的新验证方法添加验证规则。
  • 10/05/07:更新了 MediatorMessageSink 属性的使用。
© . All rights reserved.