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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (59投票s)

2009 年 8 月 2 日

CPOL

17分钟阅读

viewsIcon

140826

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

目录

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 模式的“圣杯”相悖,但这是一个相当新的模式,人们每天都在不断摸索,对我来说效果很好。我之所以这样做,有以下原因:

  1. 我一直有能力编写自己的 UI 特有 Model 类。即使我首先使用其他 Model 类,例如 LINQ to SQL 或 LINQ to Entity Framework,我也会这样做,因为在我看来,这些类并不具备一个真正的 WPF Model 类应有的一切。不过,它们很不错,因为它们是部分类,并使用了 `INotifyPropertyChanged/DataContract` 等。
  2. 我是一个实用主义者,我不喜欢为了写代码而写代码。我见过一些 MVVM 应用,作者有一个拥有 50 个属性的 Model,这些属性只是简单地在 ViewModel 抽象中重复,而 ViewModel 并没有增加任何东西。那天我决定,除非必要,我绝不会这样做。
  3. 我真的认为直接从 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 已经完成了大部分工作。你需要做以下事情:

  1. 决定是否要使用 `DataWrapper` 辅助器,它允许你的 ViewModel 将每个单独的属性置于编辑模式,而与其他属性无关。这是你的决定;**你**必须决定。如果你选择不为属性使用 `DataWrapper` 对象,那么而不是像这样 `private Cinch.DataWrapper orderId = new DataWrapper()`,你会像往常一样声明属性,例如 `private Int32 orderId = 0`。
  2. 你**必须**提供验证规则,以便原生 `IDataErrorInfo` 接口发挥其魔力并在 View 上显示错误。
  3. 你**必须**重写 `IsValid`,下面的示例将展示如何操作。
  4. 你必须重写从 `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` 辅助器,仍然可以使各个 Model(或 ViewModel)字段可编辑/只读,但这取决于你的选择。基本上,`BeginEdit()` 会将对象的当前值复制到临时存储中,这与 `DataWrapper` 辅助器无关。`DataWrapper` 辅助器只是允许 ViewModel 通过绑定告诉 View,某个 Model/ViewModel 字段不应可编辑。这是两件不同的事情。如果你选择使用 `EditableValidatingObject`,那么就需要做一些工作来存储 `DataWrapper` 辅助器的状态,但这主要由你负责,只要你像上面演示的那样使用 `DataWrapperHelper` 类的静态方法。

你需要做以下事情:

  1. 决定是否要使用 `DataWrapper` 辅助器,它允许你的 ViewModel 将每个单独的属性置于编辑模式,而与其他属性无关。这是你的决定;**你**必须决定。如果你选择不为属性使用 `DataWrapper` 对象,那么而不是像这样 `private Cinch.DataWrapper orderId = new DataWrapper()`,你会像往常一样声明属性,例如 `private Int32 orderId = 0`。
  2. 你**必须**提供验证规则,以便原生 `IDataErrorInfo` 接口发挥其魔力并在 View 上显示错误。
  3. 你**必须**重写 `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 中完成这些工作。

  1. 你必须继承自 `ValidatingViewModelBase` 或 `EditableValidatingViewModelBase`,它们提供了上面概述的功能。这两个类也都继承自 `ViewModelBase`,所以你已经具备了服务和 View 生命周期事件等功能。
  2. 确保你在 使用 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`,它确实很有帮助(至少我是这么认为的)。这个类在 [第 3 部分](CinchIII.aspx) 中进行了讨论。所以我不会详细介绍它是如何工作的,因为上一篇文章已经做过了。

这次我只是想向你展示如何在你自己的 ViewModel 中使用 `BackgroundTaskManager` 类来执行后台任务。所以,废话不多说,这里有一个例子。这个例子非常虚构,只是获取了一系列未来的日期。重点是它是在后台使用 `BackgroundTaskManager` 类在 ViewModel 中完成的,所以这就是从以下代码中可以学到的。

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`,它允许 ViewModel 将当前 Model(**我**的实现方式)或实际 ViewModel 可绑定属性中的每一块可绑定数据置于编辑/只读状态。这是通过 `DataWrapper` 辅助器完成的,你可能决定使用它,也可能不使用。这完全取决于你。如果你使用,以下是我建议的做法。我假设你有一个 `CurrentXXX` Model 对象作为一个可绑定属性暴露在当前的 ViewModel 中(基本上是**我**的实现方式),但即使不是这样,你也可以继续阅读,因为即使你重复了 ViewModel 中的所有 Model 属性,我也有一些建议。

**注意**:此代码仅在你使用 Cinch `DataWrapper` 辅助器时适用。如果你使用标准的类型,如 `Int32`/`Double` 等,请完全忽略此部分。

/// <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 的想法是,如果它能完整地满足你的需求,那很好;如果不能,那就只选择适合你的部分。我就是这样编写所有内容的。

所以请只使用你喜欢的部分。

接下来是什么?

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

  1. 如何使用 Cinch 应用对 ViewModels 进行单元测试,包括如何测试可能在 Cinch ViewModels 中运行的后台工作线程
  2. 使用Cinch的演示应用程序

就是这样,希望你喜欢

这就是我目前想说的全部内容,但我希望通过本文你能看到 Cinch 的发展方向以及它如何能帮助你解决 MVVM 问题。随着我们的旅程继续,我们将涵盖 Cinch 中的剩余项目,然后我们将继续向你展示如何使用 Cinch 开发应用程序。

历史

  1. 09/xx/xx:初始发布。
  2. 09/12/05:添加了新的代码部分,向用户展示如何使用 Cinch 中的新验证方法来添加验证规则。
  3. 09/12/24:用 [Simple Logging Facade](http://slf.codeplex.com/) 替换了 `ILoggingService`。

谢谢。

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

© . All rights reserved.