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






4.96/5 (59投票s)
大概会是 Cinch,一个用于 WPF 的 MVVM 框架。
目录
Cinch 文章系列链接
- Cinch入门文章
- Cinch及其内部机制的演练 I
- Cinch及其内部机制的演练 II
- 如何使用Cinch开发ViewModels
- 如何使用 Cinch 对 ViewModels 进行单元测试,包括如何测试可能在 Cinch ViewModels 中运行的后台工作线程
- 使用Cinch的演示应用程序
介绍
上次,我们开始研究 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(作为一对)感谢他们出色的 Mediator 实现。你们俩都很棒,总是很愉快。
Karl Shifflett / Jaime Rodriguez(微软的哥们)他们出色的 MVVM Lob 游览,我参加了,干得好,伙计们。
Bill Kempf,感谢他就是 Bill,而且是一位疯狂的编程魔法师,他还拥有一个很棒的 MVVM 框架,名为 Onyx,我之前写过一篇关于它的 文章。Bill 总是能解答棘手的问题,谢谢 Bill。
Paul Stovell 他出色的 委派验证想法,Cinch 使用它来验证业务对象。
所有 WPF Disciples 的成员,在我看来,是最好的在线社区。
谢谢你们,伙计/女孩,你们懂的。
使用 Cinch 开发模型
我知道我接下来的说法会让一些读者觉得有争议,有些人甚至会说这是完全错误的,但我稍后会详细介绍。现在,让我直接说我想说的,那就是:
我实现 MVVM 的方式是让 ViewModel(例如 PeopleViewModel)中有一个 Model 的实例(例如 currentPerson),该实例会暴露给 View(例如 PeopleView)。View 直接绑定并编辑 Model。
这无疑与大多数人认为的 MVVM 模式的“圣杯”相悖,但这是一个相当新的模式,人们每天都在不断摸索,对我来说效果很好。我之所以这样做,有以下原因:
- 我一直有能力编写自己的 UI 特有 Model 类。即使我首先使用其他 Model 类,例如 LINQ to SQL 或 LINQ to Entity Framework,我也会这样做,因为在我看来,这些类并不具备一个真正的 WPF Model 类应有的一切。不过,它们很不错,因为它们是部分类,并使用了 `INotifyPropertyChanged/DataContract` 等。
- 我是一个实用主义者,我不喜欢为了写代码而写代码。我见过一些 MVVM 应用,作者有一个拥有 50 个属性的 Model,这些属性只是简单地在 ViewModel 抽象中重复,而 ViewModel 并没有增加任何东西。那天我决定,除非必要,我绝不会这样做。
- 我真的认为直接从 View 写入 Model 没有什么坏处,**前提是**如果 Model **无效**,其数据**永远不会**进入数据库。我真的觉得这没什么问题。
既然我已经告诉你这一点,那么假设我没有能力创建自己的 Model 层。假设有另一个团队在开发 Model 层,并且他们以自己的方式做事。那么,在这种情况下,你可以将本文的这一部分视为我实际上是在谈论对包装 Model 的 ViewModel 进行此项工作,而不是直接对 Model 进行。基本上,将我接下来要讲的内容应用于你的 ViewModel(例如 PeopleViewModel),而不要去管其他团队提供的 Model(你很可能无法控制)。
好了,争论结束(希望如此)。现在我们需要决定要为你的 Model(或者如果你无法控制你的 Model,则为 ViewModel)使用哪个基类。Cinch 提供了两个选择。
Cinch.EditableValidatingObject
(或 `EditableValidatingViewModel`,如果你需要在 ViewModel 中进行此项工作):一个可编辑(`IEditableObject`)、可验证(`IDataErrorInfo`)的对象。你需要自己提供支持编辑和验证的逻辑,在自己的 Model(或 ViewModel)中。Cinch.ValidatingObject
(或 `ValidatingViewModel`,如果你需要在 ViewModel 中进行此项工作):一个可验证(`IDataErrorInfo`)的对象。你需要自己提供进行验证的逻辑,在自己的 Model(或 ViewModel)中。
所以,我现在要做的是向你展示一个 Model 示例(但这段代码可以直接应用于 ViewModel,如果你需要使用 `ValidatingViewModel` 或 `EditableValidatingViewModel` 来做到这一点)。
继承自 Cinch.EditableValidatingObject
这是最难继承的 Cinch 基类(尽管并不算太难),因为 Cinch 已经完成了大部分工作。你需要做以下事情:
- 决定是否要使用 `DataWrapper
` 辅助器,它允许你的 ViewModel 将每个单独的属性置于编辑模式,而与其他属性无关。这是你的决定;**你**必须决定。如果你选择不为属性使用 `DataWrapper ` 对象,那么而不是像这样 `private Cinch.DataWrapper orderId = new DataWrapper ()`,你会像往常一样声明属性,例如 `private Int32 orderId = 0`。 - 你**必须**提供验证规则,以便原生 `IDataErrorInfo` 接口发挥其魔力并在 View 上显示错误。
- 你**必须**重写 `IsValid`,下面的示例将展示如何操作。
- 你必须重写从 `IEditableObject` 接口继承的 `BeginEdit()/OnEndEdit()` 和 `OnCancelEdit()` 方法,这是我们通过继承 `Cinch.EditableValidatingObject` 获得的。下面的代码再次展示了如何做到这一点。
using System;
using Cinch;
using MVVM.DataAccess;
namespace MVVM.Models
{
/// <summary>
/// Respresents a UI Order Model, which has all the
/// good stuff like Validation/INotifyPropertyChanged/IEditableObject
/// which are all ready to use within the base class.
///
/// This class also makes use of <see cref="Cinch.DataWrapper">
/// Cinch.DataWrapper</see>s. Where the idea is that the ViewModel
/// is able to control the mode for the data, and as such the View
/// simply binds to a instance of a <see cref="Cinch.DataWrapper">
/// Cinch.DataWrapper</see> for both its data and its editable state.
/// Where the View can disable a control based on the
/// <see cref="Cinch.DataWrapper">Cinch.DataWrapper</see> editable state.
/// </summary>
public class OrderModel : Cinch.EditableValidatingObject
{
#region Data
//Any data item is declared as a Cinch.DataWrapper, to allow the ViewModel
//to decide what state the data is in, and the View just renders
//the data state accordingly
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;
#endregion
#region Ctor
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
//I could not be bothered to write a full DateTime picker in
//WPF, so for the purpose of this demo, DeliveryDate is
//fixed to DateTime.Now
DeliveryDate.DataValue = DateTime.Now;
}
static OrderModel()
{
quantityRule = new SimpleRule("DataValue", "Quantity can not be < 0",
(Object domainObject)=>
{
DataWrapper<Int32> obj = (DataWrapper<Int32>)domainObject;
return obj.DataValue <= 0;
});
}
#endregion
#region Public Properties
public Cinch.DataWrapper<Int32> OrderId
{
get { return orderId; }
private set
{
orderId = value;
OnPropertyChanged(() => OrderId);
}
}
public Cinch.DataWrapper<Int32> CustomerId
{
get { return customerId; }
private set
{
customerId = value;
OnPropertyChanged(() => CustomerId);
}
}
public Cinch.DataWrapper<Int32> ProductId
{
get { return productId; }
private set
{
productId = value;
OnPropertyChanged(() => ProductId);
}
}
public Cinch.DataWrapper<Int32> Quantity
{
get { return quantity; }
private set
{
quantity = value;
OnPropertyChanged(() => Quantity);
}
}
public Cinch.DataWrapper<DateTime> DeliveryDate
{
get { return deliveryDate; }
private set
{
deliveryDate = value;
OnPropertyChanged(() => DeliveryDate);
}
}
#endregion
#region Overrides
/// <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);
}
}
#endregion
#region EditableValidatingObject overrides
/// <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
}
}
继承自 Cinch.ValidatingObject
这是一个稍微容易继承的 Cinch 基类(它基本上与我们刚才看到的几乎相同,只是没有从 `IEditableObject` 继承的 `BeginEdit()` / `OnEndEdit()` 和 `OnCancelEdit()` 方法),因为它不支持编辑功能。当然,通过使用 `DataWrapper
你需要做以下事情:
- 决定是否要使用 `DataWrapper
` 辅助器,它允许你的 ViewModel 将每个单独的属性置于编辑模式,而与其他属性无关。这是你的决定;**你**必须决定。如果你选择不为属性使用 `DataWrapper ` 对象,那么而不是像这样 `private Cinch.DataWrapper orderId = new DataWrapper ()`,你会像往常一样声明属性,例如 `private Int32 orderId = 0`。 - 你**必须**提供验证规则,以便原生 `IDataErrorInfo` 接口发挥其魔力并在 View 上显示错误。
- 你**必须**重写 `IsValid`,下面的示例将展示如何操作。
using System;
using Cinch;
using MVVM.DataAccess;
namespace MVVM.Models
{
/// <summary>
/// Respresents a UI Order Model, which has all the
/// good stuff like Validation/INotifyPropertyChanged
/// which are all ready to use within the base class.
///
/// This class also makes use of <see cref="Cinch.DataWrapper">
/// Cinch.DataWrapper</see>s. Where the idea is that the ViewModel
/// is able to control the mode for the data, and as such the View
/// simply binds to a instance of a <see cref="Cinch.DataWrapper">
/// Cinch.DataWrapper</see> for both its data and its editable state.
/// Where the View can disable a control based on the
/// <see cref="Cinch.DataWrapper">Cinch.DataWrapper</see> editable state.
/// </summary>
public class OrderModel : Cinch.ValidatingObject
{
#region Data
//Any data item is declared as a Cinch.DataWrapper, to allow the ViewModel
//to decide what state the data is in, and the View just renders
//the data state accordingly
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;
#endregion
#region Ctor
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
//I could not be bothered to write a full DateTime picker in
//WPF, so for the purpose of this demo, DeliveryDate is
//fixed to DateTime.Now
DeliveryDate.DataValue = DateTime.Now;
}
static OrderModel()
{
quantityRule = new SimpleRule("DataValue", "Quantity can not be < 0",
(Object domainObject)=>
{
DataWrapper<Int32> obj = (DataWrapper<Int32>)domainObject;
return obj.DataValue <= 0;
});
}
#endregion
#region Public Properties
public Cinch.DataWrapper<Int32> OrderId
{
get { return orderId; }
private set
{
orderId = value;
OnPropertyChanged(() => OrderId);
}
}
public Cinch.DataWrapper<Int32> CustomerId
{
get { return customerId; }
private set
{
customerId = value;
OnPropertyChanged(() => CustomerId);
}
}
public Cinch.DataWrapper<Int32> ProductId
{
get { return productId; }
private set
{
productId = value;
OnPropertyChanged(() => ProductId);
}
}
public Cinch.DataWrapper<Int32> Quantity
{
get { return quantity; }
private set
{
quantity = value;
OnPropertyChanged(() => Quantity);
}
}
public Cinch.DataWrapper<DateTime> DeliveryDate
{
get { return deliveryDate; }
private set
{
deliveryDate = value;
OnPropertyChanged(() => DeliveryDate);
}
}
#endregion
#region Overrides
/// <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);
}
}
#endregion
}
}
**注意**:你可以直接在你的 ViewModel 中完成所有这些工作。有几个简单的步骤需要遵循才能直接在你的 ViewModel 中完成这些工作。
- 你必须继承自 `ValidatingViewModelBase` 或 `EditableValidatingViewModelBase`,它们提供了上面概述的功能。这两个类也都继承自 `ViewModelBase`,所以你已经具备了服务和 View 生命周期事件等功能。
- 确保你在 使用 Cinch 开发模型 部分中概述的工作应用于 ViewModel 而不是 Model。
使用 Cinch 开发 ViewModel
如果你想使用 Cinch 来开发 ViewModel,实际上你不需要考虑太多。大部分功能可以通过继承 Cinch 的 ViewModel 基类之一并使用暴露的服务来完成,这将在下面介绍。如果你需要验证功能或编辑功能,只需遵循我上面关于 使用 Cinch 开发模型 的建议,但这次继承 `ValidatingViewModelBase` 或 `EditableValidatingViewModelBase`。
选择 ViewModel 基类
正如在 使用 Cinch 开发模型 部分所讨论的,**我**通常的做法是让 View 直接绑定到一个 Model,该 Model 通过 ViewModel 中的 `currentXXX` 属性暴露出来。我意识到并非所有人都能这样做,因此 Cinch 提供了许多基类,它们是:
- `ViewModelBase`:主基类,也是下面提到的其他两个 ViewModel 基类的基类。它提供了所有服务和窗口生命周期事件等。下面将详细介绍。
- `ValidatingViewModelBase:它基本上继承自 `ViewModelBase`,并通过你在上面看到的验证规则(使用 Cinch 开发模型)提供验证支持,就像我之前展示的 Model 一样。
- `EditableValidatingViewModelBase:它基本上继承自 `ValidatingViewModelBase`,并通过你在上面看到的 `IEditableObject` 支持的编辑规则(使用 Cinch 开发模型)提供编辑支持,就像我之前展示的 Model 一样。
选择哪个类取决于你。
ViewModelBase 类
开始使用 Cinch 最简单的方法是继承 Cinch `ViewModelBase` 类,因为它为你提供了大量预制的支持,例如:
INotifyPropertyChanged
- 窗口生命周期事件
- 服务
该类也是你可以在 Cinch 中为 ViewModels 使用的其他两个基类的基类。选择权在你。
如何使用暴露的服务
本节将重点介绍如何使用 `ViewModelBase` 类暴露的服务(如果你继承自 Cinch `ViewModelBase` 类的话)。你也应该阅读服务的 [设计方式](CinchIII.aspx),这在 [Cinch 文章系列第 3 部分](CinchIII.aspx) 中已经涵盖。
日志服务
正如我们在上一篇文章中看到的,Cinch 中包含了一个 [Simple Logging Facade (SLF)](http://slf.codeplex.com/)。我们如何使用这个日志门面呢?嗯,非常简单(如果你继承自 Cinch `ViewModelBase` 类的话);这是你应该做的:
private void LogSomething()
{
var logger = this.Resolve<ILogger>();
if (logger != null)
logger.Error("Some error occurred");
}
或者,由于 `SLF.ILogger` 也作为 `ViewModelBase` 类的公共属性暴露出来,你可以在自定义 ViewModel 中这样做,前提是它们继承自 `Cinch.ViewModelBase`。
private void LogSomething()
{
Logger.Error("Some error occurred");
}
这实际上只是使用 `ViewModelBase.Resolve()` 方法(这实际上是使用了第 3 部分提到的 `ServiceProvider` 类)来定位服务,然后直接使用该服务。
需要注意的一点是,Logger 是一个特殊情况,**必须**通过 `App.Config` 进行配置,而如果你对所有其他 Cinch 默认服务都满意,则无需进行任何配置,因为在没有配置详细信息的情况下将使用默认值。
以下是一个配置 Cinch 日志记录的 `App.Config` 示例:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
<section name="slf" type="Slf.Config.SlfConfigurationSection, slf"/>
</configSections>
<slf>
<factories>
<!-- configure single log4net factory, which will get all logging output -->
<!-- Important: Set a reference to the log4net facade library
to make sure it will be available at runtime -->
<factory type="SLF.Log4netFacade.Log4netLoggerFactory, SLF.Log4netFacade"/>
</factories>
</slf>
<!-- configures log4net to write into a local file called "log.txt" -->
<log4net>
<!-- log4net uses the concept of 'appenders' to indicate
where log messages are written to. Appenders can be files,
the console, databases, SMTP and much more
-->
<appender name="MainAppender" type="log4net.Appender.FileAppender">
<param name="File" value="log.txt" />
<param name="AppendToFile" value="true" />
<!-- log4net can optionally format the logged messages with a pattern.
This pattern string details what information
is logged and the format it takes. A wide range of information
can be logged, including message, thread, identity and more,
see the log4net documentation for details:
https://logging.apache.ac.cn/log4net/release/sdk/log4net.Layout.PatternLayout.html
-->
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%logger: %date [%thread] %-5level - %message %newline" />
</layout>
</appender>
<root>
<level value="ALL" />
<appender-ref ref="MainAppender" />
</root>
</log4net>
</configuration>
消息框服务
正如我们在上一篇文章中看到的,核心 Cinch 服务中包含了一个消息框服务。我们如何使用这个消息框服务呢?嗯,非常简单(如果你继承自 Cinch `ViewModelBase` 类的话);这是你应该做的:
private void ShowMessageBox()
{
var messager = this.Resolve<IMessageBoxService>();
if (messager != null)
{
//EXAMPLE 1 : Simple MessageBox's, expecting NO return value
messager.ShowError("There was an error");
messager.ShowInformation("Nice day for MVVVM");
messager.ShowWarning("Something could be going down");
//EXAMPLE 2 : Simple MessageBox's, expecting return value
if (messager.ShowYesNo("Do it?",
CustomDialogIcons.Question) == CustomDialogResults.Yes)
{
//YES : Do it
}
if (messager.ShowYesNoCancel("Do it?",
CustomDialogIcons.Exclamation) == CustomDialogResults.Cancel)
{
//Cancel : Unwinnd operation
}
}
}
这实际上只是使用 `ViewModelBase.Resolve()` 方法(这实际上是使用了第 3 部分提到的 `ServiceProvider` 类)来定位服务,然后直接使用该服务。
打开文件服务
正如我们在上一篇文章中看到的,核心 Cinch 服务中包含了一个打开文件服务。我们如何使用这个打开文件服务呢?嗯,非常简单(如果你继承自 Cinch `ViewModelBase` 类的话);这是你应该做的:
private void OpenFile()
{
var openFileServ = this.Resolve<IOpenFileService>();
if (openFileServ != null)
{
openFileServ.InitialDirectory = @"C:\";
openFileServ.Filter = ".txt | Text Files";
var result = openFileServ.ShowDialog(null);
if (result.HasValue && result.Value == true)
{
//use IOpenFileService.FileName
FileInfo file = new FileInfo(openFileServ.FileName);
//do something with the file
}
}
}
这实际上只是使用 `ViewModelBase.Resolve()` 方法(这实际上是使用了第 3 部分提到的 `ServiceProvider` 类)来定位服务,然后直接使用该服务。
保存文件服务
正如我们在上一篇文章中看到的,核心 Cinch 服务中包含了一个保存文件服务。我们如何使用这个保存文件服务呢?嗯,非常简单(如果你继承自 **Cinch** `ViewModelBase` 类的话);这是你应该做的:
private void SaveFile()
{
var saveFileServ = this.Resolve<ISaveFileService>();
if (saveFileServ != null)
{
saveFileServ.InitialDirectory = @"C:\";
saveFileServ.Filter = ".txt | Text Files";
saveFileServ.OverwritePrompt = true;
var result = saveFileServ.ShowDialog(null);
if (result.HasValue && result.Value == true)
{
var messagerServ = this.Resolve<IMessageBoxService>();
if (messagerServ != null)
messagerServ.ShowInformation(
String.Format("Successfully saved file {0}",
saveFileServ.FileName));
}
}
}
这实际上只是使用 `ViewModelBase.Resolve()` 方法(这实际上是使用了第 3 部分提到的 `ServiceProvider` 类)来定位服务,然后直接使用该服务。
弹出窗口服务
正如在 [第 3 部分](CinchIII.aspx) 中讨论的,弹出窗口的处理有点特殊,因为 Cinch 不了解实际的弹出窗口类型,这些类型通常不包含在 Cinch 项目中,而是包含在 WPF UI 项目中。因此,WPF UI 项目必须在 WPF 应用的主窗口中,例如,将弹出窗口提供给 Cinch 包含的 `IUIVisualizerService` 服务。[第 3 部分](CinchIII.aspx) 详细讨论了这一点。
但是,如果 WPF UI 项目完成了它的工作,并将弹出窗口实例和类型提供给 `IUIVisualizerService` 服务,那么使用 Cinch 从 ViewModel 显示弹出窗口就非常简单了。我们只需要做如下的事情:
var uiVisualizerService = this.Resolve<IUIVisualizerService>();
bool? result = uiVisualizerService.ShowDialog("AddEditOrderPopup",
addEditOrderVM);
if (result.HasValue && result.Value)
{
CloseActivePopUpCommand.Execute(true);
}
在上面的代码中,显示了 WPF UI 项目中可用的 `AddEditOrderPopup` 弹出窗口,并传递了一个名为 `addEditOrderVM` 的 ViewModel 实例变量,该变量将用作弹出窗口的 DataContext。使用这种技术和 [第 2 部分](CinchII.aspx) 中讨论的 `IEditableObject` 技术,可以非常轻松地创建能够保存状态(OK 按钮)的弹出窗口,并且还可以通过弹出窗口的 Cancel 按钮将可编辑的对象(可能由弹出窗口编辑)回滚到其先前状态,这将调用 `IEditableObject` 的 `CancelEdit()` 方法来恢复先前状态。
当这一切都能正常工作时,感觉非常美妙。演示应用的 "AddEditCustomerViewModel
" 代码中有一个很好的例子说明了这一点。
后台任务
毫无疑问,线程很难处理。也有很多不同的方法来做事情,但如果你已经阅读了到目前为止的 Cinch 文章系列,你就会知道我已经介绍了一个方便的线程辅助类,名为 `BackgroundTaskManager
这次我只是想向你展示如何在你自己的 ViewModel 中使用 `BackgroundTaskManager
using System;
using ystem.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Windows.Threading;
using System.Windows.Data;
using Cinch;
using System.IO;
namespace MVVM.ViewModels
{
/// <summary>
/// Provides ALL logic for the AddEditCustomerView
/// </summary>
public class ExampleViewModelWithBgWorker : Cinch.WorkspaceViewModel
{
#region Data
private BackgroundTaskManager<DispatcherNotifiedObservableCollection<DateTime>>
bgWorker = null;
private DispatcherNotifiedObservableCollection<DateTime> futureDates
= new DispatcherNotifiedObservableCollection<DateTime>();
private SimpleCommand getFutureDatesCommand;
#endregion
#region Ctor
public ExampleViewModelWithBgWorker()
{
//Get futures Command
getFutureDatesCommand = new SimpleCommand
{
CanExecuteDelegate = x => CanExecuteGetFutureDatesCommand,
ExecuteDelegate = x => ExecuteGetFutureDatesCommand()
};
}
#endregion
#region Public Properties
/// <summary>
/// getFutureDatesCommand : Gets future dates in background thread
/// </summary>
public SimpleCommand GetFutureDatesCommand
{
get { return getFutureDatesCommand; }
}
/// <summary>
/// Background worker which fetches
/// Date Ranges
/// </summary>
public BackgroundTaskManager<DispatcherNotifiedObservableCollection
<DateTime>> BgWorker
{
get { return bgWorker; }
set
{
bgWorker = value;
OnPropertyChanged(() => BgWorker);
}
}
/// <summary>
/// Future Dates
/// </summary>
public DispatcherNotifiedObservableCollection<DateTime> FutureDates
{
get { return futureDates; }
set
{
futureDates = value;
OnPropertyChanged(() => FutureDates);
}
}
#endregion
#region Private Methods
/// <summary>
/// Setup backgrounder worker Task/Completion action
/// to fetch Orders for Customers
/// </summary>
private void SetUpBackgroundWorker()
{
bgWorker = new BackgroundTaskManager
<DispatcherNotifiedObservableCollection<DateTime>>(
() =>
{
var dateRanges = new
DispatcherNotifiedObservableCollection<DateTime>();
for (int i = 0; i < 10000; i++)
{
dateRanges.Add(DateTime.Now.AddDays(i));
}
return dateRanges;
},
(result) =>
{
FutureDates = result;
});
}
#endregion
#region Command Implementation
#region GetFutureDatesCommand
/// <summary>
/// Logic to determine if GetFutureDatesCommand can execute
/// </summary>
private Boolean CanExecuteGetFutureDatesCommand
{
get
{
return true;
}
}
/// <summary>
/// Executes the GetFutureDatesCommand
/// </summary>
private void ExecuteGetFutureDatesCommand()
{
try
{
GetFutureDatesCommand.CommandSucceeded = false;
bgWorker.RunBackgroundTask();
GetFutureDatesCommand.CommandSucceeded = true;
}
catch (Exception ex)
{
Logger.Log(LogType.Error, ex);
var messager = this.Resolve<IMessageBoxService>();
if (messager != null)
{
messager.ShowError(
"Failed to fetch future dates");
}
}
}
#endregion
#endregion
}
}
模式支持
有些人可能还记得 [第 2 部分](CinchII.aspx) 中的一个小型辅助类,名为 `DataWrapper
**注意**:此代码仅在你使用 Cinch `DataWrapper
/// <summary>
/// The current ViewMode, when changed will loop
/// through all nested DataWrapper objects and change
/// their state also
/// </summary>
public ViewMode CurrentViewMode
{
get { return currentViewMode; }
set
{
currentViewMode = value;
switch (currentViewMode)
{
case ViewMode.AddMode:
CurrentCustomer = new CustomerModel();
this.DisplayName = "Add Customer";
break;
case ViewMode.EditMode:
CurrentCustomer.BeginEdit();
this.DisplayName = "Edit Customer";
break;
case ViewMode.ViewOnlyMode:
this.DisplayName = "View Customer";
break;
}
//Now change all the CurrentCustomer.CachedListOfDataWrappers
//Which sets all the Cinch.DataWrapper<T>s to the correct IsEditable
//state based on the new ViewMode applied to the ViewModel
//we can use the Cinch.DataWrapperHelper class for this
DataWrapperHelper.SetMode(
CurrentCustomer.CachedListOfDataWrappers,
currentViewMode);
OnPropertyChanged(() => CurrentViewMode);
}
}
最后的总结
我想说的是,我花了很多时间思考可能出错的事情以及如何做得更好,并思考好的方法,但有些东西还是会被遗漏,有些想法可能不像预期的那么好等等。
总之,长话短说……Cinch 的想法是,如果它能完整地满足你的需求,那很好;如果不能,那就只选择适合你的部分。我就是这样编写所有内容的。
所以请只使用你喜欢的部分。
接下来是什么?
在后续文章中,我将大致如下展示:
- 如何使用 Cinch 应用对 ViewModels 进行单元测试,包括如何测试可能在 Cinch ViewModels 中运行的后台工作线程
- 使用Cinch的演示应用程序
就是这样,希望你喜欢
这就是我目前想说的全部内容,但我希望通过本文你能看到 Cinch 的发展方向以及它如何能帮助你解决 MVVM 问题。随着我们的旅程继续,我们将涵盖 Cinch 中的剩余项目,然后我们将继续向你展示如何使用 Cinch 开发应用程序。
历史
- 09/xx/xx:初始发布。
- 09/12/05:添加了新的代码部分,向用户展示如何使用 Cinch 中的新验证方法来添加验证规则。
- 09/12/24:用 [Simple Logging Facade](http://slf.codeplex.com/) 替换了 `ILoggingService`。
谢谢。
一如既往,欢迎投票/评论。