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






4.94/5 (46投票s)
大概会是 Cinch,一个用于 WPF 的 MVVM 框架。
目录
- 引言
- 必备组件
- 特别感谢
- 单元测试
- 测试命令
- 测试 Mediator
- 测试后台工作线程任务
- 服务
- 测试使用测试 IMessageBox 服务
- 测试使用测试 IOpenFileService 服务
- 测试使用测试 ISaveFileService 服务
- 测试使用测试 IUIVisualizerService 服务
- 测试支持 IDataErrorInfo 的对象的有效性
- 测试支持 IEditableObject 的对象的状
- 即将推出什么?
Cinch 文章系列链接
- Cinch入门文章
- Cinch及其内部机制的演练 I
- Cinch及其内部机制的演练 II
- 如何使用Cinch开发ViewModels
- 如何使用 Cinch 进行 ViewModel 的单元测试,包括如何测试 Cinch ViewModel 中可能运行的后台工作线程。
- 使用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.Coun
t 为零。
以下是这两个测试的示例:
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 类 ValidatingObject
或 ValidatingViewModelBase
来验证 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 类 EditableValidatingObject
或 EditableValidatingViewModelBase
来编辑 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);
}
}
}
即将推出什么?
在后续文章中,我将大致如下展示:
- 使用 Cinch 的演示应用程序。
- 一个用于快速开发 Cinch ViewModel/Model 的代码生成器,也许还有更多,如果我还有时间的话。代码生成器将使用 Cinch 编写,因此它也将作为使用 Cinch 在你自己的项目中第二个示例。
就这些,希望你喜欢
目前我就想说这么多,但我希望从本文中,你能看到 Cinch 的发展方向以及它如何帮助你实现 MVVM。在我们继续旅程的同时,我们将学习如何使用 Cinch 开发应用程序。
谢谢
一如既往,欢迎投票/评论。
历史
- 09/xx/xx:初次发布。
- 09/12/05:添加了一个代码示例,解释如何使用 Cinch 的新验证方法添加验证规则。
- 10/05/07:更新了
MediatorMessageSink
属性的使用。