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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (78投票s)

2009年7月25日

CPOL

25分钟阅读

viewsIcon

315648

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

目录

Cinch 文章系列链接

介绍

上次我们开始研究 Cinch 的一些内部机制,这次我们将完成对 Cinch 内部机制的研究。

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

先决条件

演示应用程序使用了

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

特别感谢

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

在我开始之前,我想特别感谢以下各位,没有他们,本文及后续一系列文章将不可能实现。基本上,我研究了 Cinch 中大多数人的成果,看到了哪些是好的,哪些是不好的,然后提出了 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,在我看来,是最好的在线社群。

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

Cinch 内部机制 II

本节将完成对 Cinch 内部机制的深入探讨,希望能让大家不至于太无聊,并能完全理解后续关于构建演示 ViewModel/单元测试集以及展示实际演示应用程序的文章。

DI/IOC

Cinch MVVM 框架使用 IOC 容器。默认情况下,它是 Microsoft Unity IOC 容器,可以作为独立应用程序块或作为 Enterprise Library 的一部分免费获得。Cinch 允许您选择自己的 IOC 容器并将其传递给 ViewModel 构造函数;您所要做的就是实现 IIOCProvider 接口,它看起来是这样的

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

namespace Cinch
{
    /// <summary>
    /// This is an interface that allows different IOC Container providers.
    /// Providers can implement this interface in order to provide the services
    /// from the container.
    /// 
    /// Cinch uses a UnityProvider as default, but you can override this
    /// by supplying a new IIOCProvider variant to the constructor
    /// of your custom Cinch based ViewModels. If no constructor parameter
    /// is supplied to the ViewModelBase class then the default UnityProvider
    /// will be used by Cinch
    /// </summary>
    public interface IIOCProvider
    {
        /// <summary>
        /// Method that <see cref="ViewModelBase">ViewModelBase</see>
        /// can call to tell container to set its self up. You are expected
        /// to register the following Types in this method
        /// <c>ILogger</c>
        /// <c>IUIVisualizerService</c>
        /// <c>IMessageBoxService</c>
        /// <c>IOpenFileService</c>
        /// <c>ISaveFileService</c>
        /// </summary>
        void SetupContainer();

        /// <summary>
        /// Get service from container
        /// </summary>
        /// <typeparam name="T">Service type</typeparam>
        /// <returns>The service instance</returns>
        T GetTypeFromContainer<T>();
    }
}

Cinch 使用 Unity IOC 容器(默认情况下,但正如刚才所说,您可以通过创建自己的 IOC 容器来实现 IIOCProvider 接口来更换它),以便在运行时动态注入不同的服务实现。

这是默认的 UnityProvider 的样子

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

using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.Configuration;
using System.Configuration;

namespace Cinch
{
    /// <summary>
    /// Provides a Unity IOC Container resolver
    /// which Cinch uses as default, which registers defaults
    /// for the following types, but will also allow these defaults
    /// to be overriden if a Unity config section scecifies different
    /// implementations
    /// <c>ILogger</c>
    /// <c>IUIVisualizerService</c>
    /// <c>IMessageBoxService</c>
    /// <c>IOpenFileService</c>
    /// <c>ISaveFileService</c>
    /// </summary>
    public class UnityProvider : IIOCProvider
    {
        #region Ctor
        public UnityProvider()
        {

        }
        #endregion

        #region Private Methods
        /// <summary>
        /// This method registers default services with the service provider. 
        /// These can be overriden by providing a new service implementation 
        /// and a new Unity config section in the project where the new service 
        /// implementation is defined 
        /// </summary>
        private static void RegisterDefaultServices()
        {

            //try add other default services, users can override this 
            //using specific Unity App.Config section entry
            try
            {

                //ILogger : Register a default WPFSLFLogger
                UnitySingleton.Instance.Container.RegisterInstance(
                    typeof(ILogger), new WPFSLFLogger());

                //IUIVisualizerService : Register a default WPFUIVisualizerService
                UnitySingleton.Instance.Container.RegisterInstance(
                    typeof(IUIVisualizerService), new WPFUIVisualizerService());

                //IMessageBoxService : Register a default WPFMessageBoxService
                UnitySingleton.Instance.Container.RegisterInstance(
                    typeof(IMessageBoxService), new WPFMessageBoxService());

                //IOpenFileService : Register a default WPFOpenFileService
                UnitySingleton.Instance.Container.RegisterInstance(
                    typeof(IOpenFileService), new WPFOpenFileService());

                //ISaveFileService : Register a default WPFSaveFileService
                UnitySingleton.Instance.Container.RegisterInstance(
                    typeof(ISaveFileService), new WPFSaveFileService());

            }
            catch (ResolutionFailedException rex)
            {
                String err = String.Format(
                    "An exception has occurred in " + 
                    "unityProvider.RegisterDefaultServices()\r\n{0}",
                    rex.StackTrace.ToString());
#if debug
                Debug.WriteLine(err);
#endif
                Console.WriteLine(err);
                throw rex;
            }
            catch (Exception ex)
            {
                String err = String.Format(
                    "An exception has occurred in " + 
                    "unityProvider.RegisterDefaultServices()\r\n{0}",
                    ex.StackTrace.ToString());
#if debug
                Debug.WriteLine(err);
#endif
                Console.WriteLine(err);
                throw ex;
            }
        }
        #endregion

        #region IIOCProvider Members
        /// <summary>
        /// Register defaults and sets up Unity container
        /// Method that <see cref="ViewModelBase">ViewModelBase</see>
        /// can call to tell container to set its self up
        /// </summary>
        public void SetupContainer()
        {
            try
            {
                //regiser defaults
                RegisterDefaultServices();

                //configure Unity (there could be some different Service implementations
                //in the config that override the defaults just setup
                UnityConfigurationSection section = (UnityConfigurationSection)
                               ConfigurationManager.GetSection("unity");
                if (section != null && section.Containers.Count > 0)
                {
                    section.Containers.Default.Configure(UnitySingleton.Instance.Container);
                }

            }
            catch (Exception ex)
            {
                throw new ApplicationException(
                  "There was a problem configuring the Unity container\r\n" + ex.Message);
            }
        }


        /// <summary>
        /// Get service from container
        /// </summary>
        /// <typeparam name="T">Service type</typeparam>
        /// <returns>The service instance</returns>
        public T GetTypeFromContainer<T>()
        {
            return (T)UnitySingleton.Instance.Container.Resolve(typeof(T));
        }
        #endregion
    }
}

存在一些默认假设,但这些可以通过在 App.Config 中指定一个条目来覆盖,这将覆盖可能否则会使用的默认服务实现。

由于 Cinch 定位于 WPF 框架,大多数服务的默认值都是 WPF 实现。因此,当您进行单元测试项目时,您**必须**通过单元测试项目的 App.Config 提供测试服务实现(Cinch 提供了这些)。

这是通过自定义 UnityConfigurationSection 实现的,该配置在实际 UI 项目和单元测试项目中都已填充。Unity 容器只是检查活动项目的 App.Config 并从 UnityConfigurationSection 读取类型,然后创建并持有所配置类型的实例。

Cinch 将 unity 容器包装在单例中,以确保在 Cinch 应用程序中只有一个 Unity 容器可用。

下图说明了 Unity IOC 容器的工作原理

可选的 App.Config Unity 项

下表说明了在使用 Cinch 时可以在 App.Config 中提供的内容。

服务项 WPF 应用 测试项目
ILogger 非必需,使用默认值 您可以使用 Cinch WPF 服务版本。
IMessageBoxService 非必需,使用默认值 您可以使用 Cinch 测试服务默认版本,但**必须**在 Unity 配置部分提供一个条目,以确保 Cinch 中的默认 WPF 实现被覆盖以使用测试版本。
IOpenFileService 非必需,使用默认值 您可以使用 Cinch 测试服务默认版本,但**必须**在 Unity 配置部分提供一个条目,以确保 Cinch 中的默认 WPF 实现被覆盖以使用测试版本。
ISaveFileService 非必需,使用默认值 您可以使用 Cinch 测试服务默认版本,但**必须**在 Unity 配置部分提供一个条目,以确保 Cinch 中的默认 WPF 实现被覆盖以使用测试版本。
IUIVisualizerService 非必需,使用默认值,但您应该在 WPF 应用的主窗口构造函数或某些其他合适的位置提供此服务管理的弹出窗口 您可以使用 Cinch 测试服务默认版本,但**必须**在 Unity 配置部分提供一个条目,以确保 Cinch 中的默认 WPF 实现被覆盖以使用测试版本。

如上所示,如果您计划使用所有默认的 Cinch 服务,则不需要为主 WPF 应用提供 App.Config,但**必须**为任何测试项目提供 App.Config,以确保默认服务(WPF 实现)被覆盖在 Cinch 服务解析代码内部。

实际应用

任何基于 Cinch 的应用程序实际上不需要提供任何服务实现,因为默认的 WPF 服务已添加并在内部使用。但是,如果您想更改默认的 WPF 服务之一,则**必须**提供一个新的 Unity 容器 App.Config 部分,该部分将覆盖服务类型的默认实现。

这是 Cinch 应用的 App.Config 示例。请记住,这**必须**包括您可能已更改的所有服务。下面显示了一个 Cinch.IUIVisualizerService 服务的特例,该服务可能提供额外功能。

您**还**必须为 Cinch 中的日志记录提供配置,它现在使用一个名为 Simple Logging Facade (SLF) 的日志门面,该门面支持多种不同的日志记录器。对于 Cinch,我选择了使用 log4Net,因此 Cinch 的 App.Config 应用应与下面所示的非常相似

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>

    <section name="unity"
        type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
              Microsoft.Practices.Unity.Configuration" />  

    <section name="log4net" 
             type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
    <section name="slf" 
             type="Slf.Config.SlfConfigurationSection, slf"/>
  </configSections>

 <!-- Unity Config Section -->
  <unity>
    <containers>
      <container>
 
        <types>
 
          <type
              type="Cinch.IUIVisualizerService, Cinch"
              mapTo="MVVM.Demo.MyFunkyWPFUIVisualizerService, MVVM.Demo"/>
 
        </types>
      </container>
    </containers>
 
  </unity>

  <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.IUIVisualizerService 服务的实现通过 Unity 注入到 Cinch 中,并将覆盖 Cinch 中 Cinch.IUIVisualizerService 的默认实现,而 Cinch 之前会尝试使用默认的 WPF 实现。

单元测试

Cinch 被设计为可进行单元测试,因此,您可以通过注入/Unity 提供替代服务给 Cinch。但说实话,Cinch 默认提供的实现很可能已经足够了,您可以在下面阅读有关它们的介绍后再做决定。

无论如何,您要么创建所有 Cinch 所需服务自己的测试实现,要么使用提供的默认实现,然后必须使用 Unity 将它们注入到 Cinch 中。

您的单元测试项目 App.Config 应该看起来像这样

<?xml version="1.0"?>
<configuration>

  <configSections>
    <section name="unity"
        type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
              Microsoft.Practices.Unity.Configuration" />
    <section name="log4net" 
      type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
    <section name="slf" type="Slf.Config.SlfConfigurationSection, slf"/>    
    
  </configSections>

  <!-- Unity Config Section -->
  <unity>
    <containers>
      <container>
        <types>

          <type
              type="Cinch.IUIVisualizerService, Cinch"
              mapTo="Cinch.TestUIVisualizerService, Cinch"/>

          <type
              type="Cinch.IMessageBoxService, Cinch"
              mapTo="Cinch.TestMessageBoxService, Cinch"/>

          <type
              type="Cinch.IOpenFileService, Cinch"
              mapTo="Cinch.TestOpenFileService, Cinch"/>

          <type
              type="Cinch.ISaveFileService, Cinch"
              mapTo="Cinch.TestSaveFileService, Cinch"/>

        </types>
      </container>
    </containers>
  </unity>

  <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>

这确保了默认的 WPF 服务实现被单元测试服务实现所覆盖。

暴露的服务

所以,您已经看到了一个 Unity IOC 容器,它负责根据当前的 App.Config 文件定位和加载正确的服务。那么,在 Cinch 中,故事并没有就此结束。您看,Cinch 的一般理念是有一个功能强大的 ViewModel 基类(Cinch.ViewModelBase),只要您继承它,您就能免费获得一些很棒的功能。暴露的服务就是其中之一。

您可能会问:为什么我们需要对服务做更多的事情?我以为那就是 Unity IOC 容器在为我们做的。嗯,这只说对了一半。Unity IOC 容器所做的就是将当前的服务(由 App.Config 指定)获取到 Cinch.ViewModelBase 中。这很棒。Unity IOC 容器的问题在于,如果您尝试从中请求服务,它似乎每次都会给您不同的实例。如果您的服务实现不需要任何状态,那没问题,但在 Cinch 中,某些测试服务**需要**状态。所以这就不行了。因此,Unity 读取的服务被添加到 Cinch.ViewModelBase 上的一个静态可用属性中。这个属性是一个 ServiceProvider,它本质上只是一个包装了 Dictionary 的东西。所以,当您稍后会看到的,使用 Resolve<T> 方法从 Cinch.ViewModelBase 请求服务时,ServiceProvider 会检查其内部的 Dictionary 并返回该服务的单个实例。这确保了我们始终使用同一个服务实例。

这张图有助于更好地解释这一点。

这是 Cinch.ViewModelBase 类中相关的代码

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

using System.Linq.Expressions;

namespace Cinch
{

    public abstract class ViewModelBase : INotifyPropertyChanged, 
                    IDisposable, IParentablePropertyExposer
    {
        private IIOCProvider iocProvider = null;
        private static ILogger logger;
        private static Boolean isInitialised = false;
        private static Action<IUIVisualizerService> setupVisualizer = null;

        private Boolean isCloseable = true;

        /// <summary>
        /// Service resolver for view models. Allows derived types to add/remove
        /// services from mapping.
        /// </summary>
        public static readonly ServiceProvider ServiceProvider = new ServiceProvider();

        /// <summary>
        /// Constructs a new ViewModelBase and wires up all the Window based Lifetime
        /// commands such as activatedCommand/deactivatedCommand/
        ///                  loadedCommand/closeCommand
        /// </summary>
        public ViewModelBase() : this(new UnityProvider())
        {

        }

        public ViewModelBase(IIOCProvider iocProvider)
        {
            if (iocProvider == null)
                throw new InvalidOperationException(
                    String.Format(
                        "ViewModelBase constructor requires " + 
                        "a IIOCProvider instance in order to work"));

            this.iocProvider = iocProvider;

            if (!ViewModelBase.isInitialised)
            {
                iocProvider.SetupContainer();
                FetchCoreServiceTypes();
            }

            //Register all decorated methods to the Mediator
            //Register all decorated methods to the Mediator
            Mediator.Instance.Register(this);
        }

        /// <summary>
        /// This resolves a service type and returns the implementation.
        /// </summary>
        /// <typeparam name="T">Type to resolve</typeparam>
        /// <returns>Implementation</returns>
        protected T Resolve<T>()
        {
            return ServiceProvider.Resolve<T>();
        }

        /// <summary>
        /// Delegate that is called when the services are injected
        /// </summary>
        public static Action<IUIVisualizerService> SetupVisualizer
        {
            get { return setupVisualizer; }
            set { setupVisualizer=value; }
        }
 
        /// <summary>
        /// Logger : The ILogger implementation in use
        /// </summary>
        public ILogger Logger
        {
            get { return logger; }
        }

        /// <summary>
        /// This method registers services with the service provider.
        /// </summary>
        private void FetchCoreServiceTypes()
        {
            try
            {
                ViewModelBase.isInitialised = false;

                //ILogger : Allows MessageBoxs to be shown 
                logger = (ILogger)this.iocProvider.GetTypeFromContainer<ILogger>();

                ServiceProvider.Add(typeof(ILogger), logger);

                 //IMessageBoxService : Allows MessageBoxs to be shown 
                IMessageBoxService messageBoxService =
                    (IMessageBoxService)
                     this.iocProvider.GetTypeFromContainer<imessageboxservice>();

                ServiceProvider.Add(typeof(IMessageBoxService), messageBoxService);

                //IOpenFileService : Allows Opening of files 
                IOpenFileService openFileService =
                    (IOpenFileService)
                     this.iocProvider.GetTypeFromContainer<iopenfileservice>();
                ServiceProvider.Add(typeof(IOpenFileService), openFileService);

                //ISaveFileService : Allows Saving of files 
                ISaveFileService saveFileService =
                    (ISaveFileService)
                     this.iocProvider.GetTypeFromContainer<isavefileservice>();
                ServiceProvider.Add(typeof(ISaveFileService), saveFileService);

                //IUIVisualizerService : Allows popup management
                IUIVisualizerService uiVisualizerService =
                   (IUIVisualizerService)
                    this.iocProvider.GetTypeFromContainer<iuivisualizerservice>();
                ServiceProvider.Add(typeof(IUIVisualizerService), uiVisualizerService);

                //call the callback delegate to setup IUIVisualizerService managed
                //windows
                if (SetupVisualizer != null)
                    SetupVisualizer(uiVisualizerService);

                ViewModelBase.isInitialised = true;

            }
            catch (Exception ex)
            {
                LogExceptionIfLoggerAvailable(ex);
            }
        }

     
        /// <summary>
        /// Logs a message if there is a ILoggerService available. And then throws
        /// new ApplicationException which should be caught somewhere external
        /// to this class
        /// </summary>
        /// <param name="ex">Exception to log</param>
        private static void LogExceptionIfLoggerAvailable(Exception ex)
        {
            if (logger != null)
                logger.Error("An error occurred", ex);

            throw new ApplicationException(ex.Message);
        }
    }
}

Cinch 可用的服务

服务本质上就是一个接口,可以以您喜欢的任何方式实现。所以,要制作一个 WPF 服务实现,您需要实现 WPF 服务接口。要制作一个测试服务,您需要实现用于单元测试的服务,依此类推。

以下子章节将概述实际的测试/WPF 服务。

注意:我说的是 WPF 而不是 Silverlight。Cinch 是一个 WPF 框架,它不针对 Silverlight,也许可以将其改为支持 Silverlight,但这并非其初衷。

日志服务

默认情况下,Cinch 使用基于 ILogger 的服务,该服务默认使用名为 Simple Logging Facade (SLF) 的日志门面工作,并使用 log4Net 门面,因此日志条目采用 log4Net 风格,这可以通过 App.Config 进行配置。

如果您不喜欢这样,而更喜欢使用不同的日志记录设施,您所要做的就是使用您选择的 IOC 容器在 Cinch 中注入一个新的 ILogger 实现。

注意:Cinch 不提供此服务的测试版本,因为它被认为测试代码和运行时代码都可以使用相同的 Logger 实现。

ILogger 服务看起来是这样的

using System;

namespace Cinch
{
    /// <summary>
    /// This interface defines a interface that will allow 
    /// a ViewModel to output a Logmessage to whatever format 
    /// the consumer specifies when they provide a ILogger based
    /// service. Note Cinch supplies a SLF default ILogger based
    /// service, but a new one can be injected using any IOC mechanism
    /// you like
    /// </summary>
    public interface ILogger
    {
        void Error(Exception exception);
        void Error(object obj);
        void Error(string message);
        void Error(Exception exception, string message);
        void Error(string format, params object[] args);
        void Error(Exception exception, string format, params object[] args);
        void Error(IFormatProvider provider, string format, params object[] args);
        void Error(Exception exception, string format, 
                   IFormatProvider provider, params object[] args);
    }
}

消息框服务

  1. 适用于:WPF UI 项目
  2. 适用于:单元测试项目

Cinch 提供了一种新颖的处理单元测试服务实现的方法。虽然可以使用您喜欢的模拟框架(RhinoMocks/Moq 等),但有时这还不够。想象一下,ViewModel 中有一段代码,如下所示

var messageBoxService = this.Resolve<imessageboxservice>();
 
if (messageBoxService.ShowYesNo("You sure",
    CustomDialogIcons.Question) == CustomDialogResults.Yes)
{
    if (messageBoxService.ShowYesNo("You totally sure",
        CustomDialogIcons.Question) == CustomDialogResults.Yes)
    {
        //DO IT
    }
}

在这里,ViewModel 中有一个原子代码段,需要通过单元测试进行充分测试。使用模拟,我们可以提供一个模拟的 Cinch.IMessageBoxService 服务实现。但这**行不通**,因为我们只能提供一个单一的响应,这与真实的 WPF Cinch.IMessageBoxService 的行为不同,因为用户可以自由使用实际的消息框,并可能随机选择是/否/取消。因此,显然,模拟是不够的。我们需要一个更好的想法。

因此,Cinch 所做的是提供一个单元测试 Cinch.IMessageBoxService 服务实现,该实现允许单元测试将响应 Func<CustomDialogResults>(实际上就是委托)排队,这允许我们提供将在 ViewModel 代码中调用的回调代码。这允许我们做任何我们想在已排队的それに列的 Func<CustomDialogResults> 回调中做的事情,正如单元测试所提供的。

这张图有助于更好地解释这个概念。

因此,发生的情况是,单元测试通过使用 Func<CustomDialogResults>(即回调委托)对所有必需的响应进行排队,然后由 Cinch.IMessageBoxService 服务实现的单元测试实现调用它们。

下面是一个示例,展示了 Cinch.IMessageBoxService 服务实现的单元测试实现对于 ShowYesNo() Cinch.IMessageBoxService 服务实现方法调用的样子

/// <summary>
/// Returns the next Dequeue ShowYesNo response expected. See the tests for 
/// the Func callback expected values
/// </summary>
/// <param name="message">The message to be displayed.</param>
/// <param name="icon">The icon to be displayed.</param>
 
/// <returns>User selection.</returns>
public CustomDialogResults ShowYesNo(string message, CustomDialogIcons icon)
{
    if (ShowYesNoResponders.Count == 0)
        throw new ApplicationException(
            "TestMessageBoxService ShowYesNo method expects " + 
            "a Func<CustomDialogResults> callback \r\n" +
            "delegate to be enqueued for each Show call");
    else
    {
        Func<CustomDialogResults> responder = ShowYesNoResponders.Dequeue();
        return responder();
    }
}

可以看到,用于 ShowYesNo() 方法的 Cinch.IMessageBoxService 服务实现的单元测试实现只是出队下一个 Func<CustomDialogResults>(实际上就是委托),并调用 Func<CustomDialogResults>(在实际单元测试中排队),并使用从对 Func<CustomDialogResults> 的调用中获得的结果。

下面是一个示例,展示了如何设置单元测试代码以对我们上面看到的 ViewModel 代码进行排队正确的 Func<CustomDialogResults> 响应

testMessageBoxService.ShowYesNoResponders.Enqueue
    (() =>
        {
            //return Yes for "Are sure" ViewModel prompt
            return CustomDialogResults.Yes;
        }
    );
 
testMessageBoxService.ShowYesNoResponders.Enqueue
    (() =>
        {
            //return Yes for "Are totally sure" ViewModel prompt
            return CustomDialogResults.Yes;
        }
    );

通过使用这种方法,我们可以保证按照我们想要的任何测试路径来驱动 ViewModel 代码。这是一种非常强大的技术。

Cinch.IMessageBoxService 服务接口如下所示

/// <summary>
/// This interface defines a interface that will allow 
/// a ViewModel to show a messagebox
/// </summary>
public interface IMessageBoxService
{
    /// <summary>
    /// Shows an error message
    /// </summary>
 
    /// <param name="message">The error message</param>
    void ShowError(string message);
 
    /// <summary>
    /// Shows an information message
    /// </summary>
    /// <param name="message">The information message</param>
 
    void ShowInformation(string message);
 
    /// <summary>
    /// Shows an warning message
    /// </summary>
    /// <param name="message">The warning message</param>
    void ShowWarning(string message);
 
    /// <summary>
 
    /// Displays a Yes/No dialog and returns the user input.
    /// </summary>
    /// <param name="message">The message to be displayed.</param>
    /// <param name="icon">The icon to be displayed.</param>
 
    /// <returns>User selection.</returns>
    CustomDialogResults ShowYesNo(string message, CustomDialogIcons icon);
 
    /// <summary>
    /// Displays a Yes/No/Cancel dialog and returns the user input.
    /// </summary>
    /// <param name="message">The message to be displayed.</param>
 
    /// <param name="icon">The icon to be displayed.</param>
    /// <returns>User selection.</returns>
    CustomDialogResults ShowYesNoCancel(string message, CustomDialogIcons icon);
 
    /// <summary>
    /// Displays a OK/Cancel dialog and returns the user input.
    /// </summary>
 
    /// <param name="message">The message to be displayed.</param>
    /// <param name="icon">The icon to be displayed.</param>
    /// <returns>User selection.</returns>
 
    CustomDialogResults ShowOkCancel(string message, CustomDialogIcons icon);
}

打开文件服务

  1. 适用于:WPF UI 项目
  2. 适用于:单元测试项目

它的工作方式与上面为 Cinch.IMessageBoxService 服务概述的非常相似,但这次,排队的 are Queue<Func<bool?>>。这意味着您可以模拟在单元测试中打开文件,通过排队 ViewModel 代码当前正在测试所需的 Func<bool?> 值来实现。

Cinch.IOpenFileService 服务接口如下所示

/// <summary>
/// This interface defines a interface that will allow 
/// a ViewModel to open a file
/// </summary>
public interface IOpenFileService
{
    /// <summary>
    /// FileName
    /// </summary>
 
    String FileName { get; set; }
 
    /// <summary>
    /// Filter
    /// </summary>
    String Filter { get; set; }
 
    /// <summary>
    /// Filter
    /// </summary>
    String InitialDirectory { get; set; }
 
    /// <summary>
 
    /// This method should show a window that allows a file to be selected
    /// </summary>
    /// <param name="owner">The owner window of the dialog</param>
    /// <returns>A bool from the ShowDialog call</returns>
    bool? ShowDialog(Window owner);
}

保存文件服务

  1. 适用于:WPF UI 项目
  2. 适用于:单元测试项目

它的工作方式与上面为 Cinch.IMessageBoxService 服务概述的非常相似,但这次,排队的 are Queue<Func<bool?>>。这意味着您可以模拟在单元测试中保存文件,通过排队 ViewModel 代码当前正在测试所需的 Func<bool?> 值来实现。

例如,您可能希望实际在您在单元测试中排队的 Func<bool?> 中创建一个文件,然后才返回 true,ViewModel 可以随后检查并使用您在单元测试中实际保存的文件。

例如,您可以在单元测试中执行类似的操作

testSaveFileService.ShowDialogResponders.Enqueue
    (() =>
      {
        String path = @"c:\test.txt";
        if (!File.Exists(path)) 
        {
            // Create a file to write to.
            using (StreamWriter sw = File.CreateText(path)) 
            {
                sw.WriteLine("Hello");
                sw.WriteLine("Cinch");
            }    
        }
 
        testSaveFileService.FileName = path ;
        return true;
      }
    );

Cinch.ISaveFileService 服务接口如下所示

/// <summary>
/// This interface defines a interface that will allow 
/// a ViewModel to save a file
/// </summary>
public interface ISaveFileService
{
    /// <summary>
    /// FileName
    /// </summary>
    Boolean OverwritePrompt { get; set; }
 
    /// <summary>
 
    /// FileName
    /// </summary>
    String FileName { get; set; }
 
    /// <summary>
    /// Filter
    /// </summary>
    String Filter { get; set; }
 
    /// <summary>
    /// Filter
    /// </summary>
 
    String InitialDirectory { get; set; }
 
    /// <summary>
    /// This method should show a window that allows a file to be saved
    /// </summary>
    /// <param name="owner">The owner window of the dialog</param>
    /// <returns>A bool from the ShowDialog call</returns>
 
    bool? ShowDialog(Window owner);
}

弹出窗口服务

  • 适用于:WPF UI 项目(但在 Cinch 代码内,请参阅演示应用程序)
  • 适用于:单元测试项目

我不知道你们怎么样,但我们在公司正在进行一个非常大的 WPF 项目,虽然我不是弹出窗口的粉丝,但我们还是有一些。弹出窗口与大多数人通常的 MVVM 方式不太兼容。大多数人会把 View 做成一个 UserControl,并将 ViewModel 作为 DataContext。这很酷。但偶尔,我们需要显示一个弹出窗口,并让它编辑当前 ViewModel 中的某个对象,或者允许用户取消编辑。

Cinch 的处理方式是提供一个名为 Cinch.IUIVisualizerService 的服务,它是一个相当复杂的实体。但它必须如此。基本思路是这样的

拥有弹出窗口的 WPF 应用**必须**将弹出窗口提供给 Cinch.IUIVisualizerService(附带的演示应用程序对此进行了说明)。期望通过 Unity 将此提供的实现注入到 Cinch 中。

演示应用程序提供的 Cinch.IUIVisualizerService 实现提供了仅 WPF 应用才能使用的弹出窗口类型。这就是为什么 Cinch 无法将这些弹出窗口提供给 Cinch.IUIVisualizerService WPF 实现。因此,必须由 WPF 应用告知 Cinch.IUIVisualizerService WPF 实现预期的弹出窗口。这可以在应用程序主窗口的构造函数中完成,通过一个回调委托,当 Cinch.IUIVisualizerService 服务被注入时,Cinch 会调用它

namespace MVVM.Demo
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            //register known windows via callback from ViewModelBase
            //when services are ready
            ViewModelBase.SetupVisualizer = (x) =>
                {
                    x.Register("AddEditOrderPopup", typeof(AddEditOrderPopup));
                };

            this.DataContext = new MainWindowViewModel();
            InitializeComponent();
        }
    }
}

演示应用程序提供的 Cinch.IUIVisualizerService 服务的 WPF 实现如下所示

using System;
using System.Collections.Generic;
using System.Windows;
 
using Cinch;
 
namespace MVVM.Demo
{
    /// <summary>
    /// This class implements the IUIVisualizerService for WPF purposes.
    /// This implementation HAD TO be in the Main interface project, as
    /// it needs to know about Popup windows that are not known about in 
    /// the ViewModel or Cinch projects.
    /// </summary>
 
    public class WPFUIVisualizerService : Cinch.IUIVisualizerService
    {
        #region Data
        private readonly Dictionary<string, Type> _registeredWindows;
        #endregion
 
        #region Ctor
        /// <summary>
        /// Constructor
        /// </summary>
        public WPFUIVisualizerService()
        {
            _registeredWindows = new Dictionary<string, Type>();
 
            //register known windows
            Register("AddEditOrderPopup", typeof(AddEditOrderPopup));
        }
        #endregion
 
        #region Public Methods
        /// <summary>
 
        /// Registers a collection of entries
        /// </summary>
        /// <param name="startupData"></param>
        public void Register(Dictionary<string, Type> startupData)
        {
            foreach (var entry in startupData)
                Register(entry.Key, entry.Value);
        }
 
        /// <summary>
        /// Registers a type through a key.
        /// </summary>
 
        /// <param name="key">Key for the UI dialog</param>
        /// <param name="winType">Type which implements dialog</param>
        public void Register(string key, Type winType)
        {
            if (string.IsNullOrEmpty(key))
                throw new ArgumentNullException("key");
            if (winType == null)
                throw new ArgumentNullException("winType");
            if (!typeof(Window).IsAssignableFrom(winType))
                throw new ArgumentException("winType must be of type Window");
 
            lock (_registeredWindows)
            {
                _registeredWindows.Add(key, winType);
            }
        }
 
        /// <summary>
        /// This unregisters a type and removes it from the mapping
        /// </summary>
 
        /// <param name="key">Key to remove</param>
        /// <returns>True/False success</returns>
        public bool Unregister(string key)
        {
            if (string.IsNullOrEmpty(key))
                throw new ArgumentNullException("key");
 
            lock (_registeredWindows)
            {
                return _registeredWindows.Remove(key);
            }
        }
 
        /// <summary>
        /// This method displays a modaless dialog associated with the given key.
        /// </summary>
 
        /// <param name="key">Key previously registered with the UI controller.</param>
        /// <param name="state">Object state to associate with the dialog</param>
        /// <param name="setOwner">Set the owner of the window</param>
 
        /// <param name="completedProc">Callback used when UI closes (may be null)</param>
        /// <returns>True/False if UI is displayed</returns>
        public bool Show(string key, object state, bool setOwner, 
            EventHandler<UICompletedEventArgs> completedProc)
        {
            Window win = CreateWindow(key, state, setOwner, completedProc, false);
            if (win != null)
            {
                win.Show();
                return true;
            }
            return false;
        }
 
        /// <summary>
 
        /// This method displays a modal dialog associated with the given key.
        /// </summary>
        /// <param name="key">Key previously registered with the UI controller.</param>
        /// <param name="state">Object state to associate with the dialog</param>
        /// <returns>True/False if UI is displayed.</returns>
 
        public bool? ShowDialog(string key, object state)
        {
            Window win = CreateWindow(key, state, true, null, true);
            if (win != null)
                return win.ShowDialog();
 
            return false;
        }
        #endregion
 
        #region Private Methods
        /// <summary>
        /// This creates the WPF window from a key.
        /// </summary>
        /// <param name="key">Key</param>
        /// <param name="dataContext">DataContext (state) object</param>
 
        /// <param name="setOwner">True/False to set ownership to MainWindow</param>
        /// <param name="completedProc">Callback</param>
        /// <param name="isModal">True if this is a ShowDialog request</param>
 
        /// <returns>Success code</returns>
        private Window CreateWindow(string key, object dataContext, bool setOwner, 
            EventHandler<UICompletedEventArgs> completedProc, bool isModal)
        {
            if (string.IsNullOrEmpty(key))
                throw new ArgumentNullException("key");
 
            Type winType;
            lock (_registeredWindows)
            {
                if (!_registeredWindows.TryGetValue(key, out winType))
                    return null;
            }
 
            var win = (Window)Activator.CreateInstance(winType);
            win.DataContext = dataContext;
            if (setOwner && Application.Current != null)
                win.Owner = Application.Current.MainWindow;
 
            if (dataContext != null)
            {
                var bvm = dataContext as ViewModelBase;
                if (bvm != null)
                {
                    if (isModal)
                    {
                        bvm.CloseRequest += ((s, e) =>
                        {
                            try
                            {
                                win.DialogResult = e.Result;
                            }
                            catch (InvalidOperationException)
                            {
                                win.Close();
                            }
                        });
                    }
                    else
                    {
                        bvm.CloseRequest += ((s, e) => win.Close());
                    }
                    bvm.ActivateRequest += ((s, e) => win.Activate());
                }
            }
 
            if (completedProc != null)
            {
                win.Closed +=
                    (s, e) =>
 
                        completedProc
                        (this,new UICompletedEventArgs 
                            {   State = dataContext, 
                                Result = (isModal) ? win.DialogResult : null 
                            }
                        );
 
            }
 
            return win;
        }
        #endregion
    }
}

它的作用是设置新请求的弹出窗口,使其 DataContext 设置为某个对象,并监听来自启动 ViewModel 的关闭命令,该命令指示弹出窗口关闭。

因此,从 ViewModel 使用此服务来显示弹出窗口并设置其 DataContext,我们会这样做

addEditOrderVM.CurrentViewMode = ViewMode.AddMode;
addEditOrderVM.CurrentCustomer = CurrentCustomer;
bool? result = uiVisualizerService.ShowDialog("AddEditOrderPopup", addEditOrderVM);
 
if (result.HasValue && result.Value)
{
    CloseActivePopUpCommand.Execute(true);
}

可以看到,这段 ViewModel 代码片段使用了已注册的(来自 WPF 应用的 Cinch.IUIVisualizerService 实现)弹出窗口的名称之一,然后以模态方式显示弹出窗口,并等待 DialogResultbool?),如果 DialogResult 为 true,则 ViewModel 关闭弹出窗口。

请注意,我们可以设置当执行 CloseActivePopupCommand 时返回的 DialogResult 值,这在您以编程方式关闭活动弹出窗口而不是让用户使用与 CloseActivePopUpCommand 关联的按钮时可能很有用,该按钮将始终返回 true,如下所示

这一切都很棒,所以我们现在可以从 ViewModel 显示弹出窗口,设置 DataContext,监听 DialogResult,然后关闭弹出窗口。听起来很酷。但是,有些技巧你需要知道。它们如下

处理弹出窗口和 Cinch 的技巧

遵循这些简单的规则应该会有所帮助

确保您的保存和取消按钮设置了 IsDefault 和 IsCancel,如下所示

<Button Content="Save" IsDefault="True" 
  Command="{Binding CloseActivePopUpCommand}"
  CommandParameter="True"/>
 
<Button Content="Cancel" IsCancel="True"/>

Cinch.IUIVisualizerService 如下所示

/// <summary>
/// This interface defines a UI controller which can be used to display dialogs
/// in either modal or modaless form from a ViewModel.
/// </summary>
public interface IUIVisualizerService
{
    /// <summary>
    /// Registers a type through a key.
    /// </summary>
 
    /// <param name="key">Key for the UI dialog</param>
    /// <param name="winType">Type which implements dialog</param>
    void Register(string key, Type winType);
 
    /// <summary>
    /// This unregisters a type and removes it from the mapping
    /// </summary>
 
    /// <param name="key">Key to remove</param>
    /// <returns>True/False success</returns>
    bool Unregister(string key);
 
    /// <summary>
    /// This method displays a modaless dialog associated with the given key.
    /// </summary>
 
    /// <param name="key">Key previously registered with the UI controller.</param>
    /// <param name="state">Object state to associate with the dialog</param>
    /// <param name="setOwner">Set the owner of the window</param>
 
    /// <param name="completedProc">Callback used when UI closes (may be null)</param>
    /// <returns>True/False if UI is displayed</returns>
    bool Show(string key, object state, bool setOwner, 
        EventHandler<UICompletedEventArgs> completedProc);
 
    /// <summary>
 
    /// This method displays a modal dialog associated with the given key.
    /// </summary>
    /// <param name="key">Key previously registered with the UI controller.</param>
    /// <param name="state">Object state to associate with the dialog</param>
    /// <returns>True/False if UI is displayed.</returns>
 
    bool? ShowDialog(string key, object state);
}

如果您的应用程序不使用弹出窗口,您可以

  1. 只需在您的应用程序中提供此服务的虚拟实现,并确保它通过 Unity 注入到 Cinch 中
  2. 编辑 Cinch.ViewModelBase 类以删除所有对 IUIVisualizerService 的引用

线程辅助

多线程是我们不经常做的事情之一(或者您可能经常做),但每次我们需要再次做的时候,它似乎又会让我们抓狂。为此,Cinch 提供了一些有用的线程辅助类,下面将对此进行介绍。

Dispatcher 扩展方法

Cinch 包含一些在处理 Dispatcher 时非常实用的扩展方法;这些扩展方法允许扩展方法的调用者使用正确的 UI Dispatcher 线程,并可选地以给定的 DispatcherPriority 来调用一段代码。它非常简单且易于使用。这是 Dispatcher 扩展方法的全部代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Threading;
 
namespace Cinch
{
    /// <summary>
    /// Provides a set of commonly used Dispatcher extension methods
    /// </summary>
    public static class DispatcherExtensions
    {
        #region Dispatcher Extensions
        /// <summary>
        /// A simple threading extension method, to invoke a delegate
        /// on the correct thread if it is not currently on the correct thread
        /// which can be used with DispatcherObject types.
        /// </summary>
 
        /// <param name="dispatcher">The Dispatcher object on which to 
        /// perform the Invoke</param>
        /// <param name="action">The delegate to run</param>
        /// <param name="priority">The DispatcherPriority for the invoke.</param>
 
        public static void InvokeIfRequired(this Dispatcher dispatcher,
            Action action, DispatcherPriority priority)
        {
            if (!dispatcher.CheckAccess())
            {
                dispatcher.Invoke(priority, action);
            }
            else
            {
                action();
            }
        }
 
        /// <summary>
        /// A simple threading extension method, to invoke a delegate
        /// on the correct thread if it is not currently on the correct thread
        /// which can be used with DispatcherObject types.
        /// </summary>
        /// <param name="dispatcher">The Dispatcher object on which to 
        /// perform the Invoke</param>
        /// <param name="action">The delegate to run</param>
 
        public static void InvokeIfRequired(this Dispatcher dispatcher, Action action)
        {
            if (!dispatcher.CheckAccess())
            {
                dispatcher.Invoke(DispatcherPriority.Normal, action);
            }
            else
            {
                action();
            }
        }
 
        /// <summary>
        /// A simple threading extension method, to invoke a delegate
        /// on the correct thread if it is not currently on the correct thread
        /// which can be used with DispatcherObject types.
        /// </summary>
        /// <param name="dispatcher">The Dispatcher object on which to 
        /// perform the Invoke</param>
        /// <param name="action">The delegate to run</param>
 
        public static void InvokeInBackgroundIfRequired(
            this Dispatcher dispatcher, 
            Action action)
        {
            if (!dispatcher.CheckAccess())
            {
                dispatcher.Invoke(DispatcherPriority.Background, action);
            }
            else
            {
                action();
            }
        }
 
        /// <summary>
        /// A simple threading extension method, to invoke a delegate
        /// on the correct thread asynchronously if it is not currently 
        /// on the correct thread which can be used with DispatcherObject types.
        /// </summary>
        /// <param name="dispatcher">The Dispatcher object on which to 
        /// perform the Invoke</param>
        /// <param name="action">The delegate to run</param>
 
        public static void InvokeAsynchronouslyInBackground(
            this Dispatcher dispatcher, Action action)
        {
            if (dispatcher != null)
                dispatcher.BeginInvoke(DispatcherPriority.Background, action);
            else
                action();
        }
        #endregion
    }
}

使用这个扩展方法非常简单,您只需这样做

Dispatcher.InvokeIfRequired(() =>
{
    //run some code on the correct UI Dispatcher thread
    //at the DispatcherPriority stated
 
},DispatcherPriority.Background);

显然,如果您想从 ViewModel 为 View 做一些与 Dispatcher 相关的事情,您需要使用 CurrentDispatcher 静态属性。

App.DoEvents

在 WPF 中,没有 App.DoEvents(),一些从 WinForms 转来的开发者可能会期待这个。对于那些没有使用过旧的 WinForms App.DoEvents() 的人来说,那个方法的作用是强制消息泵处理所有排队的消息。这有时有助于解决选择更改、待处理事件等问题。

基本上,这是一个非常有用的功能,但正如我所说,WPF 默认不提供这样的功能。幸运的是,使用 Dispatcher(消息排队的地方)和 DispatcherFrame 来自己创建一个并不麻烦。Cinch 提供了两种 App.DoEvents 的形式

  • 一种是用户指定 DispatcherPriority,作为所有待处理 Dispatcher 消息应有效处理的下限。
  • 一种是所有 Dispatcher 消息都应有效处理。

下面的代码展示了它是如何工作的

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Threading;
using System.Security.Permissions;
 
namespace Cinch
{
    public static class ApplicationHelper
    {
        #region DoEvents
        /// <summary>
        /// Forces the WPF message pump to process all enqueued messages
        /// that are above the input parameter DispatcherPriority.
        /// </summary>
        /// <param name="priority">The DispatcherPriority to use
        /// as the lowest level of messages to get processed</param>
        [SecurityPermissionAttribute(SecurityAction.Demand,
            Flags = SecurityPermissionFlag.UnmanagedCode)]
        public static void DoEvents(DispatcherPriority priority)
        {
            DispatcherFrame frame = new DispatcherFrame();
            DispatcherOperation dispatcherOperation = 
                Dispatcher.CurrentDispatcher.BeginInvoke(priority, 
                    new DispatcherOperationCallback(ExitFrameOperation), frame);
            
            Dispatcher.PushFrame(frame);
 
            if (dispatcherOperation.Status != DispatcherOperationStatus.Completed)
            {
                dispatcherOperation.Abort();
            }
        }
  
        /// <summary>
        /// Forces the WPF message pump to process all enqueued messages
        /// that are DispatcherPriority.Background or above
        /// </summary>
        [SecurityPermissionAttribute(SecurityAction.Demand,
            Flags = SecurityPermissionFlag.UnmanagedCode)]
        public static void DoEvents()
        {
            DoEvents(DispatcherPriority.Background);
        }
 
        /// <summary>
        /// Stops the dispatcher from continuing
        /// </summary>
        private static object ExitFrameOperation(object obj)
        {
            ((DispatcherFrame)obj).Continue = false;
            return null;
        }
        #endregion
    }
}

所以,要使用这个 WPF App.DoEvents(),您只需要像这样调用它

ApplicationHelper.DoEvents();

或者处理高于特定 DispatcherPriority 的所有消息,请使用以下方法

ApplicationHelper.DoEvents(DispatcherPriority.Background);

后台任务

我在处理大型数据集和 MVVM 时遇到的一个困难是如何使用后台任务进行单元测试和同步。有些人使用 ThreadPool(或者这里托管在 CodeProject 上的非常棒的 SmartThreadPool),有些人使用 BackgroundWorker。我将尝试使用适合工作的通用方法。但是,对于 Cinch,我决定包含一个我在网上找到的 BackgroundWorker 包装类。我对此做了一些更改,以改变完成回调的顺序。我必须说,它非常整洁。

它被称为 BackgroundTaskManager<T>,它接受一个泛型,表示运行后台任务时的返回值。所以理想情况下,您只会有一个操作在后台返回单个类型,并将其用作结果。当然,您可以在单个 ViewModel 中有多个 BackgroundTaskManager<T> 对象,每个对象执行不同的后台活动。

它允许您在构造函数中挂钩 Func<T> taskFunc, Action<T> completionActionFunc<T> taskFunc 用于挂钩到 BackgroundWorker.DoWork() 方法,而 Action<T> completionActionBackgroundWorker.Completed 事件引发时调用。

我添加的一个功能是 AutoResetEvent WaitHandle 属性,单元测试可以设置它以允许单元测试等待 AutoResetEvent WaitHandle 属性发出的信号,该属性是通过单元测试提供的。稍后将详细介绍。

现在,让我们继续看看这个 BackgroundTaskManager<T> 类是什么样的;这是

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading;
 
namespace MVVM.ViewModels
{
    public class BackgroundTaskManager<T>
    {
        #region Data
        private Func<T> TaskFunc { get; set; }
        private Action<T> CompletionAction { get; set; }
        #endregion
 
        #region Ctor
        /// <summary>
        /// Constructs a new BackgroundTaskManager with
        /// the function to run, and the action to call when the function to run
        /// completes
        /// </summary>
 
        /// <param name="taskFunc">The function to run in the background</param>
        /// <param name="completionAction">The completed action to call
        /// when the background function completes</param>
        public BackgroundTaskManager(Func<T> taskFunc, Action<T> completionAction)
        {
            this.TaskFunc = taskFunc;
            this.CompletionAction = completionAction;
        }
        #endregion
 
        #region Public Properties
        /// <summary>
 
        /// Event invoked when a background task is started.
        /// </summary>
        [SuppressMessage("Microsoft.Usage", 
            "CA2211:NonConstantFieldsShouldNotBeVisible",
            Justification = "Add/remove is thread-safe for events in .NET.")]
        public EventHandler<EventArgs> BackgroundTaskStarted;
 
        /// <summary>
        /// Event invoked when a background task completes.
        /// </summary>
        [SuppressMessage("Microsoft.Usage", 
            "CA2211:NonConstantFieldsShouldNotBeVisible",
            Justification = "Add/remove is thread-safe for events in .NET.")]
        public EventHandler<EventArgs> BackgroundTaskCompleted;
 
        /// <summary>
 
        /// Allows the Unit test to be notified on Task completion
        /// </summary>
        public AutoResetEvent CompletionWaitHandle { get; set; }
        #endregion
        
        #region Public Methods
        /// <summary>
        /// Runs a task function on a background thread; 
        /// invokes a completion action on the main thread.
        /// </summary>
        public void RunBackgroundTask()
        {
            // Create a BackgroundWorker instance
            var backgroundWorker = new BackgroundWorker();
 
            // Attach to its DoWork event to run the task function and capture the result
            backgroundWorker.DoWork += delegate(object sender, DoWorkEventArgs e)
            {
                e.Result = TaskFunc();
            };
 
            // Attach to its RunWorkerCompleted event to run the completion action
            backgroundWorker.RunWorkerCompleted += 
                delegate(object sender, RunWorkerCompletedEventArgs e)
            {
                // Call the completion action
                CompletionAction((T)e.Result);
                
                // Invoke the BackgroundTaskCompleted event
                var backgroundTaskFinishedHandler = BackgroundTaskCompleted;
                if (null != backgroundTaskFinishedHandler)
                {
                    backgroundTaskFinishedHandler.Invoke(null, EventArgs.Empty);
                }
            };
 
            // Invoke the BackgroundTaskStarted event
            var backgroundTaskStartedHandler = BackgroundTaskStarted;
            if (null != backgroundTaskStartedHandler)
            {
                backgroundTaskStartedHandler.Invoke(null, EventArgs.Empty);
            }
 
            // Run the BackgroundWorker asynchronously
            backgroundWorker.RunWorkerAsync();
        }
        #endregion
    }
}

那么,如何使用这些 BackgroundTaskManager<T> 类之一呢?嗯,实际上非常简单。我暂时不会介绍单元测试,因为那是另一篇文章的主题。现在,我将向您展示如何在 ViewModel 中使用它,如下所示

  1. 创建一个私有属性
  2. 创建一个公共属性以允许单元测试访问 BackgroundTaskManager<T>
  3. BackgroundTaskManager<T> 的构造函数中挂钩 BackgroundTaskManager<T> Func<T> taskFunc, Action<T> completionAction
  4. 调用一个方法来处理 BackgroundTaskManager<T>

这都将在下面的代码片段中演示。当我们在后续文章中创建使用 Cinch 的 ViewModel 和使用 Cinch 进行单元测试时,我们将看到更多内容。

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 SomeViewModel : Cinch.WorkspaceViewModel
    {
        //background workers
        private 
          BackgroundTaskManager<DispatcherNotifiedObservableCollection<OrderModel>> 
          bgWorker = null;

        public AddEditCustomerViewModel()
        {
            //setup background worker
            SetUpBackgroundWorker();
        }
        
        /// <summary>
        /// Background worker which lazy fetches 
        /// Customer Orders
        /// </summary>
        public BackgroundTaskManager
                 <DispatcherNotifiedObservableCollection<OrderModel>> BgWorker
        {
            get { return bgWorker; }
            set
            {
                bgWorker = value;
                OnPropertyChanged(() => BgWorker);
            }
        }
 
        /// <summary>
        /// Setup backgrounder worker Task/Completion action
        /// to fetch Orders for Customers
        /// </summary>
 
        private void SetUpBackgroundWorker()
        {
            bgWorker = new BackgroundTaskManager
                         <DispatcherNotifiedObservableCollection<OrderModel>>(
                () =>
                {
                    return new DispatcherNotifiedObservableCollection<OrderModel>(
                        DataAccess.DataService.FetchAllOrders(
                            CurrentCustomer.CustomerId.DataValue).ConvertAll(
                                new Converter<Order, OrderModel>(
                                      OrderModel.OrderToOrderModel)));
                },
                (result) =>
                {
 
                    CurrentCustomer.Orders = result;
                    if (customerOrdersView != null)
                        customerOrdersView.CurrentChanged -=
                            CustomerOrdersView_CurrentChanged;

                    customerOrdersView =
                        CollectionViewSource.GetDefaultView(CurrentCustomer.Orders);
                    customerOrdersView.CurrentChanged +=
                        CustomerOrdersView_CurrentChanged;
                    customerOrdersView.MoveCurrentToPosition(-1);
 
                    HasOrders = CurrentCustomer.Orders.Count > 0;
                });
        }

        /// <summary>
        /// Fetches all Orders for customer using BackgroundTaskManager<T>
        /// </summary>
        private void LazyFetchOrdersForCustomer()
        {
            bgWorker.RunBackgroundTask();
        }
    }
}

ObservableCollection

有时还会发生另一种情况,您可能有一个 ObservableCollection,其中添加了需要 marshaled 到 UI 线程才能使用新项的项。Cinch 使用以下代码来实现此目的

/// <summary>
/// This class provides an ObservableCollection which supports the 
/// Dispatcher thread marshalling for added items. 
/// 
/// This class does not take support any thread sycnhronization of
/// adding items using multiple threads, that level of thread synchronization
/// is left to the user. This class simply marshalls the CollectionChanged
/// call to the correct Dispatcher thread
/// </summary>
/// <typeparam name="T">Type this collection holds</typeparam>
public class DispatcherNotifiedObservableCollection<T> : ObservableCollection<T>
{
    #region Ctors
 
    public DispatcherNotifiedObservableCollection()
        : base()
    {
    }
 
    public DispatcherNotifiedObservableCollection(List<T> list)
        : base(list)
    {
    }
 
    public DispatcherNotifiedObservableCollection(IEnumerable<T> collection) 
        : base(collection)
    {
 
    }
    #endregion
 
    #region Overrides
    /// <summary>
    /// Occurs when an item is added, removed, changed, moved, 
    /// or the entire list is refreshed.
    /// </summary>
    public override event NotifyCollectionChangedEventHandler CollectionChanged;
 
    /// <summary>
    /// Raises the <see cref="E:System.Collections.ObjectModel.
    /// ObservableCollection`1.CollectionChanged"/> 
    /// event with the provided arguments.
    /// </summary>
    /// <param name="e">Arguments of the event being raised.</param>
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        var eh = CollectionChanged;
        if (eh != null)
        {
            Dispatcher dispatcher = 
                (from NotifyCollectionChangedEventHandler nh in eh.GetInvocationList()
                 let dpo = nh.Target as DispatcherObject
                 where dpo != null
                 select dpo.Dispatcher).FirstOrDefault();
 
            if (dispatcher != null && dispatcher.CheckAccess() == false)
            {
                dispatcher.Invoke(DispatcherPriority.DataBind, 
                    (Action)(() => OnCollectionChanged(e)));
            }
            else
            {
                foreach (NotifyCollectionChangedEventHandler nh 
                        in eh.GetInvocationList())
                    nh.Invoke(this, e);
            }
        }
    }
    #endregion
}

以 MVVM 的方式处理 MenuItems

MVVM 模式的新手可能会在以 MVVM 模式处理菜单时遇到困难。这很可惜,因为它们实际上相当简单且易于驾驭。毕竟,它们本质上是分层结构(想象一棵树),允许通过 Click 或 ICommand 来运行一些代码。它们听起来(至少在操作上)很像 Button 对象,它们也有 Click 事件并可以使用 ICommand,我们知道如何处理它们。我们只需将其 Command 属性绑定到 ViewModel 公开的 ICommand 属性。那么菜单为什么会不同呢?事实证明它们并非如此,我们遇到的唯一困难在于如何创建菜单集合并在 View 中进行样式设置。

在 ViewModel 中表示 MenuItem

让我们从看看如何表示一个 ViewModel 友好的 MenuItem 对象开始,好吗?我们可以这样做

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
 
namespace Cinch
{
    /// <summary>
    /// Provides a mechanism for constructing MenuItems
    /// within a ViewModel
    /// </summary>
    public class WPFMenuItem
    {
        #region Public Properties
        public String Text { get; set; }
        public String IconUrl { get; set; }
        public List<WPFMenuItem> Children { get; private set; }
        public SimpleCommand Command { get; set; }
        #endregion
 
        #region Ctor
        public WPFMenuItem(string item)
        {
            Text = item;
            Children = new List<WPFMenuItem>();
        }
        #endregion
    }
}

很简单,对吧?那么我们如何从 ViewModel 公开一个 View 可以使用的菜单呢?让我们接下来看看。

从 ViewModel 公开 MenuItems

嗯,这也实际上很简单。我们只需公开 ViewModel 中一个属性来存储我们想要公开的 MenuItem。这是一个 ViewModel 中的示例属性。

/// <summary>
/// Returns the bindbable Menu options
/// </summary>
public List<WPFMenuItem> OrderMenuOptions
{
    get
    {
        return CreateMenus();
    }
}

唯一需要解释的部分是对 CreateMenus() 方法的调用。这没什么巧妙的;它只是创建了要从 ViewModel 公开的 MenuItem。下面是一个示例,它可能看起来像这样

/// <summary>
/// Creates and returns the menu items
/// </summary>
private List<WPFMenuItem> CreateMenus()
{
    var menu = new List<WPFMenuItem>();
 
    var miAddOrder = new WPFMenuItem("Add Order");
    miAddOrder.Command = AddOrderCommand;
    menu.Add(miAddOrder);
 
    var miEditOrder = new WPFMenuItem("Edit Order");
    miEditOrder.Command = EditOrderCommand;
    menu.Add(miEditOrder);
 
    var miDeleteOrder = new WPFMenuItem("Delete Order");
    miDeleteOrder.Command = DeleteOrderCommand;
    menu.Add(miDeleteOrder);
 
    return menu;

}

您可以看到,在此方法中,我们不仅创建了 MenuItem 的结构,还将其与 ViewModel 中可用的正确 ICommand 绑定。就是这么简单。

在 View 中渲染 ViewModel 公开的 MenuItems

拼图的最后一部分是如何在特定 View 中渲染 ViewModel 公开的 MenuItem。这也很简单。首先,在 View 中声明一个 Menu 并将其绑定到 ViewModel 公开的 MenuItem

<Menu x:Name="menu" Margin="0,0,0,0" 
      Height="Auto" Foreground="White"
      ItemContainerStyle="{StaticResource ContextMenuItemStyle}"
      ItemsSource="{Binding MenuOptions}"
      VerticalAlignment="Top" Background="#FF000000">
</Menu>

其中每个单独的 MenuItem 都使用以下 Style 进行样式设置

<Style x:Key="ContextMenuItemStyle">
    <Setter Property="MenuItem.Header" Value="{Binding Text}"/>
    <Setter Property="MenuItem.ItemsSource" Value="{Binding Children}"/>
    <Setter Property="MenuItem.Command" Value="{Binding Command}" />
    <Setter Property="MenuItem.Icon" Value="{Binding IconUrl, 
        Converter={StaticResource MenuIconConv}}" />
</Style>

实际上,很多人都要求能够创建分隔线。我没有为此提供支持,但幸运的是,一位 Cinch 用户向我介绍了这样做的一个很棒的方法。所以您在 XAML 中会看到这样的内容

<local:SeparatorStyleSelector x:Key="separatorSelector" />
<Style x:Key="ContextMenuItemStyle">
     <Setter Property="MenuItem.Header" Value="{Binding Text}"/>
     <Setter Property="MenuItem.ItemsSource" Value="{Binding Children}"/>
     <Setter Property="MenuItem.Command" Value="{Binding Command}" />
     <Setter Property="MenuItem.Icon" Value="{Binding IconUrl, 
        Converter={StaticResource MenuIconConv}}" />
</Style>

<Style x:Key="SeparatorStyle" TargetType="{x:Type MenuItem}">
     <Setter Property="Template">
    <Setter.Value>
           <ControlTemplate>
                  <Separator />
           </ControlTemplate>
        </Setter.Value>
     </Setter>
</Style>

在这里,您可以使用以下 StyleSelector 来为 Seperator 创建一个替代的 Style

public class SeparatorStyleSelector : StyleSelector
{
    public override Style SelectStyle(object item, DependencyObject container)
    {
        if (item is ViewModels.MenuItem)
        {
            ViewModels.MenuItem mi =
                item as ViewModels.MenuItem;
            if (mi.Text.Equals("--", StringComparison.OrdinalIgnoreCase))
            {
                return (Style)((FrameworkElement)
                          container).FindResource("SeparatorStyle");
            }
            else
            {
                return (Style)((FrameworkElement)
                         container).FindResource("ContextMenuItemStyle");
            }
        }
        return null;
    }
}

完整的条目可以在新的 Cinch 文章论坛条目之一 https://codeproject.org.cn/Messages/3555500/Separator-menu-item.aspx 中找到。

可关闭的 ViewModels

我不知道有多少读者是 WPF 新手,从 WinForms 转过来的,或者有多少读者实际上正在做 WPF 开发,但我可以肯定一件事。当 WPF 发布时,它并没有开箱即用地提供 MDI 界面。事实上,如果您看看 Expression Blend(这是一个微软的 WPF 工具,用于处理 WPF,顺便说一句,它也是用 WPF 编写的),您会发现它与之前的微软开发工具(如 Visual Studio)截然不同。Expression Blend 是一个单窗口应用程序,它使用一些简单的技巧来管理内容。这些技巧是真正巧妙的布局,例如使用大量的展开器/标签页。但它是一个单窗口应用程序。

您看到的多数 WPF 应用也是单窗口应用。当我做一个新的 WPF 应用时,我尽量让它成为一个单窗口应用。当然,有时弹出窗口很难避免。事实上,演示代码就有一个弹出窗口,您将在以后的文章中看到。

但现在,让我们想象一下我们想用标签页来构建一个单窗口应用。我们如何以 MVVM 的方式做到这一点?

我们来看看,好吗?

Cinch actually provides a base ViewModel called WorkspaceViewModel, which provides a single Closed event, which can be used as a base class for your own ViewModels. Here is the code for that

using System;
 
namespace Cinch
{
    /// <summary>
    /// This ViewModelBase subclass requests to be removed 
    /// from the UI when its CloseWorkSpace executes.
    /// This class is abstract.
    /// </summary>
    public abstract class WorkspaceViewModel : ViewModelBase
    {
        #region Data
 
        private SimpleCommand closeWorkSpaceCommand;
        private Boolean isCloseable = true;
 
        #endregion 
 
        #region Constructor
 
        protected WorkspaceViewModel()
        {
            //This is used for popup control only
            closeWorkSpaceCommand = new SimpleCommand
            {
                CanExecuteDelegate = x => true,
                ExecuteDelegate = x => ExecuteCloseWorkSpaceCommand()
            };
        }
 
        #endregion // Constructor
 
        #region Public Properties
 
        /// <summary>
 
        /// Returns the command that, when invoked, attempts
        /// to remove this workspace from the user interface.
        /// </summary>
        public SimpleCommand CloseWorkSpaceCommand
        {
            get
            {
                return closeWorkSpaceCommand;
            }
        }
 
        public Boolean IsCloseable
        {
            get { return isCloseable; }
            set
            {
                isCloseable = value;
                OnPropertyChanged(() => IsCloseable);
            }
        }
 
        #endregion // CloseCommand
 
        #region Private Methods
 
        /// <summary>
        /// Executes the CloseWorkSpace Command
        /// </summary>
        private void ExecuteCloseWorkSpaceCommand()
        {
            CloseWorkSpaceCommand.CommandSucceeded = false;
 
            EventHandler<EventArgs> handlers = CloseWorkSpace;
 
            // Invoke the event handlers
            if (handlers != null)
            {
                try
                {
                    handlers(this, EventArgs.Empty);
                    CloseWorkSpaceCommand.CommandSucceeded = true;
                }
                catch
                {
                    Logger.Log(LogType.Error, "Error firing CloseWorkSpace event");
                }
            }
        }
 
        #endregion
 
        #region CloseWorkSpace Event
        /// <summary>
 
        /// Raised when this workspace should be removed from the UI.
        /// </summary>
        public event EventHandler<EventArgs> CloseWorkSpace;
        #endregion 
    }
}

要使用它,只需继承此类即可。现在我们需要做的是构建一个 WorkspaceViewModel ViewModel 的集合,并将其绑定到 TabControl。让我们接下来看看。这是一个示例 ViewModel,它包含一个 WorkspaceViewModel ViewModel 的集合,并处理它们的添加/删除

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 ObservableCollection<WorkspaceViewModel> workspaces;
 
        public MainWindowViewModel()
        {
            Workspaces = new ObservableCollection<WorkspaceViewModel>();
            Workspaces.CollectionChanged += this.OnWorkspacesChanged;
            StartPageViewModel startPageViewModel = new StartPageViewModel();
            startPageViewModel.IsCloseable = false;
            Workspaces.Add(startPageViewModel);
        }
 
        /// <summary>
        /// If we get a request to add a new Workspace, add a new WorkSpace to the 
        /// collection and hook up the CloseWorkSpace event in a weak manner
        /// </summary>
        private void OnWorkspacesChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null && e.NewItems.Count != 0)
                foreach (WorkspaceViewModel workspace in e.NewItems)
                    workspace.CloseWorkSpace +=
                         new EventHandler<EventArgs>(OnCloseWorkSpace).
                             MakeWeak(eh => workspace.CloseWorkSpace -= eh);
        }
 
        /// <summary>
        /// If we get a request to close a new Workspace, remove the WorkSpace from the 
        /// collection
        /// </summary>
        private void OnCloseWorkSpace(object sender, EventArgs e)
        {
            WorkspaceViewModel workspace = sender as WorkspaceViewModel;
            workspace.Dispose();
            this.Workspaces.Remove(workspace);
        }

        /// <summary>
        /// Sets a ViewModel to be active, which for the View equates
        /// to selected Tab
        /// </summary>
        /// <param name="workspace">workspace to activate</param>
 
        private void SetActiveWorkspace(WorkspaceViewModel workspace)
        {
            ICollectionView collectionView = 
                CollectionViewSource.GetDefaultView(this.Workspaces);
 
            if (collectionView != null)
                collectionView.MoveCurrentTo(workspace);
        }
 
        /// <summary>
        /// The active workspace ViewModels
        /// </summary>
        public ObservableCollection<WorkspaceViewModel> Workspaces
        {
            get { return workspaces; }
            set
            {
                if (workspaces == null)
                {
                    workspaces = value;
                    OnPropertyChanged("Workspaces");
                }
            }
        }
    }
}

现在我们有了要绑定到 WorkspaceViewModel ViewModel 的集合,我们只需使用一个合适的 View 控件来表示它们,例如 TabControl。这是一个示例

<local:TabControlEx x:Name="tabControl" 
        Grid.Row="2" Grid.Column="0" 
        IsSynchronizedWithCurrentItem="True" 
        ItemsSource="{Binding Path=Workspaces}" 
        RenderTransformOrigin="0.5,0.5"
        Template="{StaticResource MainTabControlTemplateEx}">
</local:TabControlEx>

其中 TabControlEx 模板如下所示

<!-- MainTabControlTemplateEx -->
<ControlTemplate x:Key="MainTabControlTemplateEx"
                TargetType="{x:Type controls:TabControlEx}">
    <Grid>
        
        <Grid.RowDefinitions>
 
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="4"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        
        <TabPanel x:Name="tabpanel" 
            Background="{StaticResource OutlookButtonHighlight}"
            Margin="0"
            Grid.Row="0"
            IsItemsHost="True" />
        
        <Grid Grid.Row="1" Background="Black" 
              HorizontalAlignment="Stretch"/>
 
              
        <Grid x:Name="PART_ItemsHolder"
              Grid.Row="2"/>
    </Grid>
    <!-- no content presenter -->
    <ControlTemplate.Triggers>
        <Trigger Property="TabStripPlacement" 
        Value="Top">
            <Setter TargetName="tabpanel" 
        Property="DockPanel.Dock" Value="Top"/>
 
            <Setter TargetName="PART_ItemsHolder" 
        Property="DockPanel.Dock" Value="Bottom"/>
        </Trigger>
        <Trigger Property="TabStripPlacement" 
        Value="Bottom">
            <Setter TargetName="tabpanel" 
        Property="DockPanel.Dock" Value="Bottom"/>
            <Setter TargetName="PART_ItemsHolder" 
        Property="DockPanel.Dock" Value="Top"/>
        </Trigger>
 
    </ControlTemplate.Triggers>
 
</ControlTemplate>

拼图的最后一块是确保正确的 View 显示在正确的绑定 TabItem.Content(继承自 WorkspaceViewModel 且在绑定列表中)旁边。基本上,这只是确保在 Resources 部分的某个地方有正确的 View DataTemplates。

<DataTemplate DataType="{x:Type VM:StartPageViewModel}">
 
    <AdornerDecorator>
        <local:StartPageView />
    </AdornerDecorator>
</DataTemplate>
 
<DataTemplate DataType="{x:Type VM:AddEditCustomerViewModel}">
    <AdornerDecorator>
 
        <local:AddEditCustomerView />
    </AdornerDecorator>
</DataTemplate>
 
<DataTemplate DataType="{x:Type VM:SearchCustomersViewModel}">
    <AdornerDecorator>
        <local:SearchCustomersView />
 
    </AdornerDecorator>
</DataTemplate>

设置焦点

Cinch 包含一个小的辅助类,可用于将焦点设置到特定的 UI 元素,而在 WPF 中,这比您想象的要困难得多。您需要确保通过 Dispatcher 泵送一条消息,以真正确保将焦点设置到控件上。无论如何,这是相关的 Cinch 代码

/// <summary>
/// This class forces focus to set on the specified UIElement 
/// </summary>
public class FocusHelper
{
    /// <summary>
    /// Set focus to UIElement
    /// </summary>
    /// <param name="element">The element to set focus on</param>
 
    public static void Focus(UIElement element)
    {
        //Focus in a callback to run on another thread, ensuring the main 
        //UI thread is initialized by the time focus is set
        ThreadPool.QueueUserWorkItem(delegate(Object theElement)
        {
            UIElement elem = (UIElement)theElement;
            elem.Dispatcher.Invoke(DispatcherPriority.Normal,
                (Action)delegate()
                {
                    elem.Focus();
                    Keyboard.Focus(elem);
                });
        }, element);
    }
}

即将推出什么?

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

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

就是这样,希望您喜欢

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

谢谢

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

历史

  • 初始问题。
  • 2009/12/24:用 Simple Logging Facade 替换了 ILoggingService
  • 2010/05/07:添加了文本以显示 ILogger,并讨论了如何将 IOC 容器设置在 ViewModel 构造函数中。
© . All rights reserved.