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

CinchV2: 我的 Cinch MVVM 框架的第 2 版:第 2 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (39投票s)

2010年6月17日

CPOL

30分钟阅读

viewsIcon

142668

如果 Jack Daniels 制造 MVVM 框架。

目录

引言

上次我们讨论了 Cinch V2 如何使用 MefedMVVM 来解析 ViewModel/Service,并且我们简要讨论了核心服务与设计时/运行时服务(我称之为 UI 服务)的区别。在本文中,我们将详细探讨 Cinch V2 支持的核心服务,并更详细地讨论设计时/运行时服务(UI 服务)。

正如我所承诺的,在每篇文章中,我都会展示 Cinch V2 兼容性矩阵。

兼容性矩阵列出了类及其一般工作区域,以及它们是否与WPF或SL或两者兼容。

工作区域 类名 WPF Silverlight(4或更高版本) 两者
业务对象 EditableValidatingObject.cs    
业务对象 ValidatingObject.cs    
业务对象 DataWrapper.cs    
Commands EventToCommandArgs.cs    
Commands SimpleCommand.cs    
Commands WeakEventHandlerManager.cs    
事件 CloseRequestEventArgs.cs    
事件 UICompletedEventArgs.cs    
弱事件 WeakEvent.cs    
弱事件 WeakEventHelper.cs    
弱事件 WeakEventProxy.cs    
扩展方法 DispatcherExtensions.cs    
扩展方法 GenericListExtensions.cs    
交互行为 CommandDrivenGoToStateAction.cs    
交互行为 FocusBehaviourBase.cs    
交互行为 NumericTextBoxBehaviour.cs    
交互行为 SelectorDoubleClickCommandBehavior.cs    
交互行为 TextBoxFocusBehavior.cs    
交互触发器 CompletedAwareCommandTrigger.cs    
交互触发器 CompletedAwareGotoStateCommandTrigger.cs    
交互触发器 EventToCommandTrigger.cs    
消息中介者 MediatorMessageSinkAttribute.cs    
消息中介者 MediatorSingleton.cs    
服务实现 ChildWindowService.cs    
服务实现 SLMessageBoxService.cs    
服务实现 ViewAwareStatus.cs    
服务实现 ViewAwareStatusWindow.cs    
服务实现 VSMService.cs    
服务实现 WPFMessageBoxService.cs    
服务实现 WPFOpenFileService.cs    
服务实现 WPFSaveFileService.cs    
服务实现 WPFUIVisualizerService.cs 是     
服务接口 IChildWindowService.cs    
服务接口 IMessageBoxService.cs    
服务接口 IViewAwareStatus.cs    
服务接口 IViewAwareStatusWindow.cs    
服务接口 IVSM.cs    
服务接口 IMessageBoxService.cs    
服务接口 IOpenFileService.cs    
服务接口 ISaveFileService.cs    
服务接口 IUIVisualizerService.cs    
服务测试实现 TestChildWindowService.cs    
服务测试实现 TestMessageBoxService.cs    
服务测试实现 TestViewAwareStatus.cs    
服务测试实现 TestViewAwareStatusWindow.cs    
服务测试实现 TestVSMService.cs    
服务测试实现 TestMessageBoxService.cs    
服务测试实现 TestOpenFileService.cs    
服务测试实现 TestSaveFileService.cs    
服务测试实现 TestUIVisualizerService.cs    
多线程 AddRangeObservableCollection.cs(这是特定的 SL 实现)    
多线程 AddRangeObservableCollection.cs(这是特定的 WPF 实现)    
多线程 BackgroundTaskManager.cs    
多线程 ISynchronizationContext.cs    
多线程 UISynchronizationContext.cs    
多线程 ApplicationHelper.cs    
多线程 DispatcherNotifiedObservableCollection.cs    
菜单 CinchMenuItem.cs    
实用程序 ArgumentValidator.cs    
实用程序 IWeakEventListener.cs(这是 SL 中缺失的 System 类,所以我创建了一个)    
实用程序 ObservableHelper.cs    
实用程序 PropertyChangedEventManager.cs(这是 SL 中缺失的 System 类,所以我创建了一个)    
实用程序 PropertyObserver.cs    
实用程序 BindingEvaluator.cs    
实用程序 ObservableDictionary.cs    
实用程序 TreeHelper.cs    
验证 RegexRule.cs    
验证 Rule.cs    
验证 SimpleRule.cs    
ViewModels EditableValidatingViewModelBase.cs    
ViewModels IViewStatusAwareInjectionAware.cs    
ViewModels ValidatingViewModelBase.cs    
ViewModels ViewMode.cs    
ViewModels ViewModelBase.cs    
ViewModels ViewModelBaseSLSpecific.cs    
ViewModels ViewModelBaseWPFSpecific.cs    
Workspaces ChildWindowResolver.cs    
Workspaces CinchBootStrapper.cs(SL 版本)    
Workspaces CinchBootStrapper.cs(WPF版本)    
Workspaces PopupNameToViewLookupKeyMetadataAttribute.cs    
Workspaces IWorkspaceAware.cs    
Workspaces MockView.cs    
Workspaces NavProps.cs    
Workspaces PopupResolver.cs    
Workspaces ViewnameToViewLookupKeyMetadataAttribute.cs    
Workspaces ViewResolver.cs    
Workspaces WorkspaceData.cs    

既然我已经向您展示了哪些类可以与 WPF/SL 兼容,那么让我们继续阅读本文的其余部分,好吗?但在此之前,这里是旧的 Cinch V1 文章的链接。

如果您错过了 Cinch V1,并且对 MVVM 感兴趣,我强烈建议您先阅读所有 Cinch V1 文章,这将使您更深入地理解这些 Cinch V2 文章中将要介绍的内容。

CinchV1 文章链接

有些人可能从未见过旧的 Cinch V1 文章,所以我也会在这里列出它们,因为 Cinch V2 仍然使用与 Cinch V1 相同的功能,我将把人们重定向到这些文章。

CinchV2 文章链接

这就是文章路线图的样子。我想现在是时候深入本文的核心内容了,那么我们开始吧。

服务/UI 服务

为了理解本文,您需要阅读第一篇文章,所以如果您还没有阅读,请使用此 URL 阅读:CinchV2:简介和 MEFedMVVM 以及 ViewModel/Service 解析

核心服务

Cinch 中的核心服务是共享的,可以在整个应用程序、多个 ViewModel 中使用,并且通常是单例实例。

Cinch V1 一直支持许多服务,例如 MessageBox/SaveFile/OpenFile/UIVisualiser,这些服务在 Cinch V2 中基本没有改变,不同之处在于不再需要处理 IOC 容器,因为这些服务会在所需的consuming ViewModel 构造函数中通过 MEF 进行注入。

以下部分将向您展示如何让您自己的 ViewModel 使用这些 MEF 启用的 Cinch V2 服务。

将服务导入 ViewModel

MEF 启用的 Cinch V2 服务导入 ViewModel 非常简单,可以使用标准的 MEF 属性完成。以下是如何导入各种 MEF 启用的 Cinch V2 服务的示例。需要注意的是,由于这些 MEF 启用的 Cinch V2 服务已使用 [PartCreationPolicy(CreationPolicy.Shared)] 标记,因此它们实际上是所有使用它们的 ViewModel 之间共享的单例实例。

这是一个典型的使用 ViewModel 外观。注意:这是一个 WPF ViewModel,但同样的原则也适用于开发 Cinch V2 Silverlight ViewModel。

[ExportViewModel("ImageLoaderViewModel")]
public class ImageLoaderViewModel : ViewModelBase
{
    private IViewAwareStatus viewAwareStatusService;
    private IMessageBoxService messageBoxService;
    private IOpenFileService openFileService;
    private ISaveFileService saveFileService;
    private IUIVisualizerService uiVisualizerService;

    [ImportingConstructor]
    public ImageLoaderViewModel(
        IMessageBoxService messageBoxService,
        IOpenFileService openFileService,
        ISaveFileService saveFileService,
        IUIVisualizerService uiVisualizerService,
        IViewAwareStatus viewAwareStatusService)
    {
        //setup services
        this.messageBoxService = messageBoxService;<
        this.openFileService = openFileService;
        this.saveFileService = saveFileService;
        this.uiVisualizerService = uiVisualizerService;
        this.viewAwareStatusService = viewAwareStatusService;
    }
}

这种方法的优点是

  1. 没有 ServiceResolver<T>,因此我们不会破坏单一职责原则。
  2. 我们允许 ViewModel 轻松接收模拟或测试替身服务。
  3. 如果我们要添加更多服务,我们只需创建一个新的服务契约,实现它,使用我在上一篇文章中展示的 MefedMVVM 属性对其进行标记,然后像上面那样使用它。它非常可扩展。

服务矩阵

在我们深入研究服务之前,有些人可能会发现此表很有帮助。

契约 服务实现 测试替身实现 是否感知视图 WPF SL4(或更高版本) (全部)
IMessageBox Service.csCinch.WPF DLL) WPFMessage BoxService.cs
Cinch.WPF DLL)
TestMessageBox Service.cs
Cinch.WPF DLL)
 
IOpenFileService.cs WPFOpenFileService.cs TestOpenFileService.cs
ISaveFileService.cs WPFSaveFileService.cs TestSaveFileService.cs
IUIVisualizerService.cs WPFUIVisualizer Service.cs TestUIVisualizerService.cs
IViewAwareStatus.cs ViewAwareStatus.cs TestViewAwareStatus.cs
IViewAware StatusWindow.cs ViewAware StatusWindow.cs TestViewAware StatusWindow.cs
IVSM.cs VSMService.cs TestVSMService.cs
IMessageBox Service.csCinch.SL DLL) SLMessageBox Service.cs
Cinch.SL DLL)
TestMessageBox Service.cs
Cinch.SL DLL)
   
IChildWindowService.cs ChildWindowService.cs TestChildWindowService.cs

可以看出,除了服务契约和实现之外,CinchV2 还提供了一个测试替身实现,您可以在单元测试中使用它。

通用服务

CinchV2 中,有两种通用服务可以在 WPF 或 SL 中使用。这两种服务将在下面讨论,但在深入讨论它们之前,我想稍微回顾一下我在第一篇文章中提到的内容。

IContextAware

MefedMVVM 中,因此在 CinchV2 中,有一个名为 IContextAware 的接口,可以由服务契约实现。当您在服务上实现此接口时,MefedMVVM 解析实现 IContextAware 的服务的过程还将一些上下文注入到实现 IContextAware 的服务中。上下文就是当前视图。因此,任何实现 IContextAware 的服务显然需要如下标记:

[PartCreationPolicy(CreationPolicy.NonShared)]

由于每个 IContextAware 实现的服务都应该只了解一个视图,因此这类服务不能共享。

如上所述,CinchV2 提供了两种通用的(WPF 和 SL)实现 IContextAware 的服务,我们将在下面详细介绍。

IViewAwareStatus

有一个服务契约,看起来像这样:

public interface IViewAwareStatus : IContextAware
{
    event Action ViewLoaded;
    event Action ViewUnloaded;
 
#if !SILVERLIGHT
 
    event Action ViewActivated;
    event Action ViewDeactivated;
 
#endif
 
    Dispatcher ViewsDispatcher { get; }
    Object View { get; }
}

您可以看到该服务对于 WPF 和 SL 来说确实是通用的。在 WPF 中,您基本上会获得更多事件。

那么,如何在 ViewModel 中使用和调用其中一种服务呢?这很简单。以下示例显示了如何操作:

[ExportViewModel("ImageLoaderViewModel")]
public class ImageLoaderViewModel : ViewModelBase
{
    private IViewAwareStatus viewAwareStatusService;


    [ImportingConstructor]
    public ImageLoaderViewModel(
        IViewAwareStatus viewAwareStatusService)
    {
        //setup services
        this.viewAwareStatusService = viewAwareStatusService;
        this.viewAwareStatusService.ViewLoaded += ViewAwareStatusService_ViewLoaded;
    }


    private void ViewAwareStatusService_ViewLoaded()
    {
        //the view is loaded...do something
    }
}

您可以使用 IViewAwareStatus 服务提供的其他公开事件做类似的事情。但是其他属性呢?好吧,让我们看看其中一些。

  • ViewsDispatcher:简单地返回与视图关联的 Dispatcher 对象,这可能允许您将线程化代码分派到 UI 线程。在测试替身中,使用的是当前 Dispatcher。
  • View:简单地返回视图。如果您的视图实现了一个您想在 ViewModel 中使用的特定接口,这可能会很有用,只需获取视图并将其转换为您的特定接口即可完成工作。您知道有时需要直接与视图通信。在测试替身中,您可以为此对象提供一个值(也许是某种 IView 接口)。

CinchV2 中的所有服务一样,我已经提供了一个测试替身供您在单元测试中使用。这是 TestViewAwareStatus 类的完整代码列表。

public class TestViewAwareStatus : IViewAwareStatus
{
    #region Data
    //This should more than likely be some IView type of object
    private object simulatedViewObject;
 
    #endregion
 
    #region Ctor
    public TestViewAwareStatus()
    {
#if SILVERLIGHT
        ViewsDispatcher = System.Windows.Deployment.Current.Dispatcher;
#else
        ViewsDispatcher = Dispatcher.CurrentDispatcher;
#endif
    }
    #endregion
 
    #region IViewAwareStatus Members
 
    public event Action ViewLoaded;
    public event Action ViewUnloaded;
 
#if !SILVERLIGHT
    public event Action ViewActivated;
    public event Action ViewDeactivated;
#endif
 
    public Dispatcher ViewsDispatcher { get; private set; }
 
    public Object View
    {
        get
        {
            return simulatedViewObject;
        }
        set
        {
            simulatedViewObject = value;
        }
    }
 
    #endregion
 
    #region IViewAware Members
 
    public void InjectContext(object view)
    {
        //nothing to do here, we should not be creating a FrameworkElement
        //in a unit test anyway, so we should expect "view" to be null
        //from a unit test case
    }
    #endregion
 
    #region Helpers
 
    /// <summary>
    /// can be called from unit test to simulate view Loaded
    /// </summary>
    public void SimulateViewIsLoadedEvent()
    {
        if (ViewLoaded != null)
            ViewLoaded();
    }
 
    /// <summary>
    /// can be called from unit test to simulate view Unloaded
    /// </summary>
    public void SimulateViewIsUnloadedEvent()
    {
        if (ViewUnloaded != null)
            ViewUnloaded();
    }
 
#if !SILVERLIGHT
 
    /// <summary>
    /// Can be called from unit test to simulate view Activated
    /// </summary>
    public void SimulateViewIsActivatedEvent()
    {
        if (ViewActivated != null)
            ViewActivated();
    }
 
    /// <summary>
    /// Can be called from unit test to simulate view Deactivated
    /// </summary>
    public void SimulateViewIsDeactivatedEvent()
    {
        if (ViewDeactivated != null)
            ViewDeactivated();
    }
 
#endif
 
    #endregion
}

我希望您能从中看出,可以从单元测试中模拟所有事件和数据。对于事件,您可以使用 SimulateXXXEvent,对于视图(假设您的 ViewModel 需要通过某个接口与视图通信),您的测试可以提供一个模拟视图(IView 或其他),这将使您的 ViewModel 认为视图确实存在。

我现在还包含了一个仅适用于 WPF 的服务,它的工作方式与上面显示的 IViewAwareStatus 服务非常相似,只是它针对的目标视图类型是 Window,并且公开了更多 Window 类型的事件。这是服务接口:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Threading;
using System.Windows;
using MEFedMVVM.Services.Contracts;
using System.ComponentModel;

namespace Cinch
{
    public interface IViewAwareStatusWindow : IContextAware
    {
        event Action ViewLoaded;
        event Action ViewUnloaded;
        event Action ViewActivated;
        event Action ViewDeactivated;

        event Action ViewWindowClosed;
        event Action ViewWindowContentRendered;
        event Action ViewWindowLocationChanged;
        event Action ViewWindowStateChanged;
        event EventHandler<CancelEventArgs> ViewWindowClosing;


        Dispatcher ViewsDispatcher { get; }
        Object View { get; }
    }
}

正如您所见,它与 IViewAwareStatus 相同,但提供了更多事件。一个有趣的事情是,您可以使用 ViewWindowClosing 事件的 CancelEventArgs 来取消 Window 的关闭,如果您不希望它因任何原因关闭的话。

重要提示:由于此服务预期与 Window 类型视图配合使用,因此此服务只能在 Window 的 ViewModel 上使用。Cinch 本身并不强制执行此操作,但它期望您能有所理解,希望没问题。

IVSM

另一个 IContextAware 服务是 VisualStateManager 服务,它允许您告诉关联视图进入特定状态。现在我不能声称我编写了这段代码,因为我没有,我只是从 MefedMVVM 复制的,并且也提供了一个测试版本,因为我想调试它。谢谢 Marlon。

这是服务契约:

public interface IVSM : IContextAware
{
    string LastStateExecuted { get; }
    void GoToState(string stateName);
}

如您所见,它非常简单。那么我们如何使用和调用其中一项服务呢?和以前一样,这非常简单。以下是一个示例:

using System;
using System.Collections.Generic;
using System.Windows.Input;
using System.ComponentModel.Composition;
using System.ComponentModel;
using System.Linq;
 
using Cinch;
using MEFedMVVM.ViewModelLocator;
 
namespace CinchV2DemoSL
{
    [ExportViewModel("GameViewModel")]
    public class GameViewModel : ViewModelBase
    {
        private IVSM visualStateManagerService;
        [ImportingConstructor]
        public GameViewModel(            
            IVSM visualStateManagerService)
        {
 
        }
  
        private void CheckForCompleted()
        {
            if (IsCompleted)
            {
                StoreGameState();
                visualStateManagerService.GoToState("WinOrCompletedState");
            }
        }
    }
}

看这有多容易?一如既往,有一个此服务的测试替身可供您在单元测试中使用,它称为 TestVVSMService。正如您所能想象的,如果没有实际的视图来显示 VisualStates,那么您实际上做不了太多事情,因此 TestVVSMService 在调用其 InjectContext(object view) / GoToState(string stateName) 时什么也不做。虽然这听起来很奇怪,但这是完全有效的;毕竟,它能让您的 ViewModel 保持正常工作并允许对其进行测试,所以一切都很好,宝贝。

重要提示:我应该提到的一点是,VisualStateManagerServiceIVSM 实现)期望在注入的上下文的第一个子级中找到 VisualStateGroups,因为它利用了标准的 VisualStateManager GotoStateAction,而后者只能与第一个子级配合使用,因为它使用标准的 VisualStateManager

如果您曾经真正使用过 Expression Blend,您会知道大多数情况下这都没问题,但有时您的 VisualStateGroups 可能不是主上下文容器的第一个子级。例如,在带有 TabItemTabControl 中,您可以轻松想象每个 TabItem 都有自己的 VisualStateGroupsVisualStates。如果出现这种情况,使用 VisualStateManagerServiceIVSM 实现)将不起作用,因为它依赖于使用标准的 VisualStateManager,而您真正需要使用的是 ExtendedVisualStateManager,因为它可以在任何 FrameworkElement 上工作。

在后续文章中,我将展示 Cinch V2 如何处理这个问题,但正如我所说,在 90% 的情况下,您应该没问题。

WPF 服务

Cinch V2 中的 WPF 服务与 Cinch V1 中的服务几乎相同,除了它们现在经过了 MEF 属性化,并且它们提供给 ViewModel 的方式使用了 MEF 而不是 ServiceResolver<T> 和 IOC 容器。因此,如果您认为您已经从阅读 Cinch V1 文章或使用 Cinch V1 中了解了这些服务的工作原理,您可能想跳过本节。

WPFMessageBoxService

这是一个共享服务,旨在跨所有 ViewModel 工作。自 Cinch V1 以来,服务契约几乎没有变化,唯一不同的是该服务被 MEFed 到 ViewModel 中。服务契约如下所示:

using System;
 
namespace Cinch
{
    /// <summary>
    /// Available Button options. 
    /// Abstracted to allow some level of UI Agnosticness
    /// </summary>
    public enum CustomDialogButtons
    {
        OK,
        OKCancel,
        YesNo,
        YesNoCancel
    }
 
    /// <summary>
    /// Available Icon options.
    /// Abstracted to allow some level of UI Agnosticness
    /// </summary>
    public enum CustomDialogIcons
    {
        None,
        Information,
        Question,
        Exclamation,
        Stop,
        Warning
    }

    /// <summary>
    /// Available DialogResults options.
    /// Abstracted to allow some level of UI Agnosticness
    /// </summary>
    public enum CustomDialogResults
    {
        None,
        OK,
        Cancel,
        Yes,
        No
    }
 
    /// <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);
    }
}

这是实际服务类的轮廓:

[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Both, typeof(IMessageBoxService))]
public class WPFMessageBoxService : IMessageBoxService
{
 
}

这是您如何在自己的 ViewModel 中导入它:

namespace CinchV2DemoWPF
{
    [ExportViewModel("ImageLoaderViewModel")]
    public class ImageLoaderViewModel : ViewModelBase
    {
        private IMessageBoxService messageBoxService;
        [ImportingConstructor]
        public ImageLoaderViewModel(
            IMessageBoxService messageBoxService)
        {
            //setup services
            this.messageBoxService = messageBoxService;
        }
 
        private void ExecuteSaveToFileCommand(Object args)
        {
 
            ......
            ......
           messageBoxService.ShowError(
               string.Format("An error occurred saving images to file\r\n{0}",ex.Message));
            ......
            ......
        }
    }
}

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

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 的行为不同,因为用户可以自由使用实际的消息框,并且可能会随机选择 Yes/No/Cancel。所以显然,模拟是不够的。我们需要一个更好的方法。

因此,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 V2 提供了一个测试替身供您在单元测试中使用,它允许您创建上述测试代码。测试服务称为 TestMessageBoxService。上面代码中的示例演示了这一点。

如果您想了解更多关于使用此服务进行测试的信息,您可以查看 Cinch V1 文章:CinchV.aspx,特别是这一部分:CinchV.aspx#Messager

WPFOpenFileService

这是一个共享服务,旨在跨所有 ViewModel 工作。自 Cinch V1 以来,服务契约几乎没有变化,唯一不同的是该服务被 MEFed 到 ViewModel 中。服务契约如下所示:

/// <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);
}

这是实际服务类的轮廓:

/// <summary> 
/// This class implements the IMessageBoxService for WPF purposes.
/// </summary> 
[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Both, typeof(IOpenFileService))]
public class WPFOpenFileService : IOpenFileService
{
}

这是您如何在自己的 ViewModel 中导入它:

namespace CinchV2DemoWPF
{
    [ExportViewModel("ImageLoaderViewModel")]
    public class ImageLoaderViewModel : ViewModelBase
    {
        private IOpenFileService openFileService;
        [ImportingConstructor]
        public ImageLoaderViewModel(
            IOpenFileService openFileService)
        {
            //setup services
            this.openFileService = openFileService;
 
        }
 
        /// <summary>
        /// Create a new List<ImageViewModel> by reading a XML file using XLINQ
        /// </summary>
        private void ExecuteOpenExistingFileCommand(Object args)
        {
            openFileService.InitialDirectory = @"C:\";
            openFileService.Filter = ".xml | XML Files";
 
            var result = openFileService.ShowDialog(null);
            if (result.HasValue && result.Value == true)
            {
                string fileName = openFileService.FileName
            }
        }
    }
}

这与上面为 Cinch.IMessageBoxService 服务概述的差不多,但这次,排队的 are Queue<Func<bool?>>。这意味着您可以模拟从单元测试中打开一个文件,通过根据当前正在测试的 ViewModel 代码的需要排队所需的 Func<bool?> 值。

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
 
using Cinch;
 
namespace MVVM.Test
{
    [TestFixture]
    public class Tests
    {
        [Test]
        public void OpenSomeFile_Tests()
        {
           //Setup some hyperthetical ViewModel
           TestOpenFileService testOpenFileService = new TestOpenFileService()
           SomeViewModel x = new SomeViewModel(testOpenFileService);
 
 
           //Queue up the responses we expect for our given TestOpenFileService 
           //for a given ICommand/Method call within the test ViewModel
           testOpenFileService.ShowDialogResponders.Enqueue
                (() =>
                    {
                      testOpenFileService.FileName = @"c:\test.txt";
                      return true
                    }
                );
    
           //Do some testing based on the File requested
           .....
           .....
           .....
           .....
        }
    }
}

一如既往,Cinch V2 提供了一个测试替身供您在单元测试中使用,它允许您创建上述测试代码。测试服务称为 TestOpenFileService,上面代码中的示例演示了这一点。

如果您想了解更多关于使用此服务进行测试的信息,您可以查看 Cinch V1 文章:CinchV.aspx,特别是这一部分:CinchV.aspx#OpenFile

WPFSaveFileService

这是一个共享服务,旨在跨所有 ViewModel 工作。自 Cinch V1 以来,服务契约几乎没有变化,唯一不同的是该服务被 MEFed 到 ViewModel 中。服务契约如下所示:

/// <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);
}

这是实际服务类的轮廓:

/// <summary> 
/// This class implements the ISaveFileService for WPF purposes.
/// </summary> 
[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Both, typeof(ISaveFileService))]
public class WPFSaveFileService : ISaveFileService
{
}

这是您如何在自己的 ViewModel 中导入它:

namespace CinchV2DemoWPF
{
    [ExportViewModel("ImageLoaderViewModel")]
    public class ImageLoaderViewModel : ViewModelBase
    {
        private ISaveFileService saveFileService;
        [ImportingConstructor]
        public ImageLoaderViewModel(
               ISaveFileService saveFileService)
        {
            //setup services
            this.saveFileService = saveFileService;
 
        }
 
        private void ExecuteSaveToFileCommand(Object args)
        {
            saveFileService.InitialDirectory = @"C:\";
            saveFileService.OverwritePrompt = true;
            saveFileService.Filter = ".xml | XML Files";
    
            var result = saveFileService.ShowDialog(null);
            if (result.HasValue && result.Value == true)
            {
                string savedFileName = saveFileService.FileName;
            }
        }
    }
}

这与上面为 Cinch.IMessageBoxService 服务概述的差不多,但这次,排队的 values are Queue<Func<bool?>>。这意味着您可以模拟从单元测试中打开一个文件,通过根据当前正在测试的 ViewModel 代码的需要排队所需的 Func<bool?> 值。

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
 
using Cinch;
 
namespace MVVM.Test
{
    [TestFixture]
    public class Tests
    {
        [Test]
        public void SaveSomeFile_Tests()
        {
            TestSaveFileService testSaveFileService = new TestSaveFileService();
 
            //Setup some hyperthetical ViewModel
            SomeViewModel x = new SomeViewModel(testSaveFileService);
 
            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;
           });
 
    
             //Do some testing based on the File saved here
            .....
            .....
            .....
            .....
        }
    }
}

一如既往,Cinch V2 提供了一个测试替身供您在单元测试中使用,它允许您创建上述测试代码。测试服务称为 TestSaveFileService,上面代码中的示例演示了这一点。

如果您想了解更多关于使用此服务进行测试的信息,您可以查看 Cinch V1 文章:CinchV.aspx,特别是这一部分:CinchV.aspx#SaveFile

WPFUIVisualizerService

我不知道你们其他人怎么样,但我们在公司正在做一个非常大的 WPF 项目,虽然我不喜欢弹出窗口,但我们确实有一些。弹窗在大多数人做 MVVM 的常规方式下不太好用。大多数人会把 View 做成一个带有 ViewModel 作为 DataContext 的 UserControl。这很酷。但有时,我们需要显示一个弹窗,并且它会编辑当前 ViewModel 中的某个对象,或者允许用户取消编辑。

Cinch 中,我通过使用一个可以处理显示弹窗的服务来解决这个问题。这是一个共享服务,旨在跨所有 ViewModel 工作。自 Cinch V1 以来,服务契约几乎没有变化,唯一不同的是该服务被 MEFed 到 ViewModel 中。服务契约如下所示:

/// <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);
}

这是实际服务类的完整代码,我包含了它,因为它可能有助于一些人理解这些服务的内部工作原理。

using System;
using System.Collections.Generic;
using System.Windows;
using System.ComponentModel.Composition;
using MEFedMVVM.ViewModelLocator;

namespace Cinch
{
    /// <summary>
    /// This class implements the IUIVisualizerService for WPF purposes.
    /// If you have attributed up your views
    /// using the ViewnameToViewLookupKeyMetadataAttribute
    /// Registration of Views with the IUIVisualizerService service is automatic.
    /// However you can still register views manually,
    /// to do this simply put some lines like this in you App.Xaml.cs
    /// ViewModelRepository.Instance.Resolver.Container.
    ///    GetExport<IUIVisualizerService>().
    ///    Value.Register("MainWindow", typeof(MainWindow));
    /// </summary>
    [PartCreationPolicy(CreationPolicy.Shared)]
    [ExportService(ServiceType.Both, typeof(IUIVisualizerService))]
    public class WPFUIVisualizerService : IUIVisualizerService
    {
        #region Data
        private readonly Dictionary<string, Type> _registeredWindows;
        #endregion

        #region Ctor
        public WPFUIVisualizerService()
        {
            _registeredWindows = new Dictionary<string, Type>();
        }
        #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);

            if (dataContext is IViewStatusAwareInjectionAware)
            {
                IViewAwareStatus viewAwareStatus = 
                    ViewModelRepository.Instance.Resolver.Container.
                    GetExport<IViewAwareStatus>().Value;
                viewAwareStatus.InjectContext((FrameworkElement)win);
                ((IViewStatusAwareInjectionAware)dataContext).
                  InitialiseViewAwareService(viewAwareStatus);
            }

            win.DataContext = dataContext;


            if (setOwner)
                win.Owner = Application.Current.MainWindow;

            if (dataContext != null)
            {
                var bvm = dataContext as ViewModelBase;
                if (bvm != null)
                {
                    if (isModal)
                    {
                        bvm.CloseRequest += ((EventHandler<CloseRequestEventArgs>)((s, e) =>
                        {
                            try
                            {
                                win.DialogResult = e.Result;
                            }
                            catch (InvalidOperationException)
                            {
                                win.Close();
                            }
                        })).MakeWeak(eh => bvm.CloseRequest -= eh);


                    }
                    else
                    {
                        bvm.CloseRequest += 
                          ((EventHandler<CloseRequestEventArgs>)((s, e) => win.Close()))
                                .MakeWeak(eh => bvm.CloseRequest -= eh); 
                    }
                    bvm.ActivateRequest += 
                       ((EventHandler<EventArgs>)((s, e) => win.Activate()))
                        .MakeWeak(eh => bvm.ActivateRequest -= eh); 
                }
            }

            win.Closed += (s, e) =>
            {
                if (completedProc != null)
                {
                    completedProc(this, new UICompletedEventArgs()
                    {
                        State = dataContext,
                        Result = (isModal) ? win.DialogResult : null
                    });
                }
            };


            return win;
        }
        #endregion
    }
}

它的作用是设置新请求的弹出窗口的 DataContext,并监听来自启动 ViewModel (Cinch.ViewModelBase) 的关闭命令,该命令指示弹出窗口关闭。

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

namespace CinchV2DemoWPF
{
    [ExportViewModel("ImageLoaderViewModel")]
    public class ImageLoaderViewModel : ViewModelBase
    {
        private IUIVisualizerService uiVisualizerService;
        [ImportingConstructor]
        public ImageLoaderViewModel(
            IUIVisualizerService uiVisualizerService)
        {
            //setup services
            this.uiVisualizerService = uiVisualizerService;
        }
 
        /// <summary>
        /// Show the AddImageRatingPopup using the IUIVisualizerService, passing
        /// it a ValidatingViewModel that should validate that a valid rating between
        /// 1-5 is entered by the user. If we get a valid rating then apply it to the
        /// currently selected ImageViewModel
        /// </summary>
        private void ExecuteAddImageRatingCommand(Object args)
        {
            ImageRatingViewModel imageRatingViewModel = 
                       new ImageRatingViewModel(messageBoxService);
            imageRatingViewModel.ImageRating.DataValue = 
                      ((ImageViewModel)loadedImagesCV.CurrentItem).Rating;
 
 
            bool? result = uiVisualizerService.ShowDialog(
                    "AddImageRatingPopup", imageRatingViewModel);
            if (result.HasValue && result.Value)
            {
                ((ImageViewModel)loadedImagesCV.CurrentItem).Rating = 
                    imageRatingViewModel.ImageRating.DataValue;
            }
        }
    }
}

可以看出,这段 ViewModel 代码使用了已注册弹窗的名称之一,然后以模态方式显示弹窗,并等待 DialogResult (bool?),如果 DialogResult 为 true,ViewModel 将关闭弹窗。

更细心的人可能会想,好吧,服务是如何知道这些视图字符串的呢?这里有几种选择。

您可以使用 Cinch V2 PopupNameToViewLookupKeyMetadata 属性标记您的弹窗,如下所示,并在 App.xaml.cs 中使用 Cinch V2 引导程序。这将遍历您指定的所有程序集,并查找具有此 PopupNameToViewLookupKeyMetadata 属性的类型,并将其添加到服务中。

[PopupNameToViewLookupKeyMetadata("AddImageRatingPopup",typeof(AddImageRatingPopup))]
public partial class AddImageRatingPopup : Window

所以您将在 App.xaml.cs 中看到这个:

CinchBootStrapper.Initialise(new List<Assembly> { typeof(App).Assembly });

这种方法的唯一问题是,如果您使用动态加载的程序集或使用 MEF 进行动态组合,您将不一定有所有程序集传递给 Cinch V2 引导程序。那么在这种情况下我们该怎么办?答案很简单,我们只是手动将其直接添加到服务中。请记住,这是一个共享服务,所以每当我们向其中添加内容时,所有使用此服务的 ViewModel 都将看到其效果。那么,让我们看看如何手动将视图添加到 WPFUIVisualizerService

IUIVisualizerService uiVisualizerService = 
     ViewModelRepository.Instance.Resolver.Container.GetExport<IUIVisualizerService>().Value;
uiVisualizerService .Register("SomeString",typeof(SomeChildWindow));

看,我们是如何直接将东西添加到 WPFUIVisualizerService 的?有一点需要注意,您显然需要在知道 ChildWindowType 的地方执行此操作。

以编程方式关闭弹出窗口

请注意,我们还可以使用 ViewModel 中的以下逻辑以编程方式关闭活动的弹出窗口:

CloseActivePopUpCommand.Execute(true);

这将设置我们希望在执行 ViewModelBase.CloseActivePopupCommand 时返回的 DialogResult 值,如果您想以编程方式关闭活动弹出窗口,而不是让用户使用与 CloseActivePopUpCommand 关联的按钮,这可能会很方便。此代码将在弹出窗口关闭时返回 true。

CloseActivePopupCommand.Execute 的实际 ViewModelBase 代码如下所示:

/// <summary>
/// Raises RaiseCloseRequest event, passing back correct DialogResult
/// </summary>
private void ExecuteCloseActivePopupCommand(Object param)
{
    if (param is Boolean)
    {
        // Close the dialog using DialogResult requested
        RaiseCloseRequest((bool)param);
        return;
    }
 
    //param is not a bool so try and parse it to a Bool
    Boolean popupAction = true;
    Boolean result = Boolean.TryParse(param.ToString(), out popupAction);
    if (result)
    {
        // Close the dialog using DialogResult requested
        RaiseCloseRequest(popupAction);
    }
    else
    {
        // Close the dialog passing back true as default
        RaiseCloseRequest(true);
    }
}

您可能还记得,在实际的 WPFUIVisualizerService 实现中,WPFUIVisualizerService 正在监听 ViewModelBase.CloseRequest 事件的引发。当 IUIVisualizerService 看到此事件被引发时,它将关闭活动弹出窗口,并使用 ViewModelBase.CloseRequest EventArgs 来确定弹出窗口应该以什么结果退出。

所以,为了再次说明,我们有一个 WPFUIVisualizerService 可以显示弹出窗口。WPFUIVisualizerService 监听 ViewModelBase.CloseRequest。我们还有一个 ViewModelBase.CloseActivePopupCommand,我们可以运行它,并向其传递一个 bool 参数,然后该命令将执行,ViewModelBase 将引发 ViewModelBase.CloseRequest 事件,并将一些 EventArgs 传递给弹出窗口的请求的编程退出值。然后 WPFUIVisualizerService 实现将使用请求的值作为弹出窗口的退出值,并继续关闭弹出窗口。

非常简单。

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

使用弹出窗口和 Cinch 时的技巧

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

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

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

有关此服务的更多背景阅读,我建议您阅读 Cinch V1 文章部分:CinchIII.aspx#PopServ

一如既往,Cinch V2 提供了一个测试替身供您在单元测试中使用,它允许您创建上述测试代码。测试服务称为 TestUIVisualizerService;上面代码中的示例演示了这一点。

如果您想了解更多关于使用此服务进行测试的信息,您可以查看 Cinch V1 文章:CinchV.aspx,特别是这一部分:CinchV.aspx#UIVisualizer

SL 服务

对于 Silverlight,显然您可以做的事情更少,因为您实际上是在沙箱中运行。因此,Cinch V2 中的可用服务较少。不过,现有的服务应该能满足大多数情况。那么,让我们来看看 Cinch V2 中可用的 Silverlight 服务。

IMessageBoxService

这是所有使用此服务的 ViewModel 之间的共享服务。

它的工作方式与其功能更全的 WPF 版本差不多。但是,由于 Silverlight 中目前可用的 MessageBox API 不如 WPF 中丰富,因此您可以使用的服务功能显然也较少。目前,在 Silverlight 中,您只能使用 MessageBox.Show,带有单个字符串或字符串/标题和确定/取消按钮集。

以下是完整的 IMessageBoxService Silverlight 合约,它正如我所说,其工作方式与上面讨论的 WPF MessageBoxService 差不多。

/// 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 OK/Cancel dialog and returns the user input.
    /// </summary>
    /// <param name="message">The message to be displayed.</param>
    /// <returns>User selection.</returns>
    CustomDialogResults ShowOkCancel(string message);
 
}

同样,我提供了一个 TestMessageBoxService,您可以在测试中使用它。同样,它的工作方式也与功能更全的 WPF 版本差不多。

IChildWindowService

这是所有使用此服务的 ViewModel 之间的共享服务。

在 Silverlight 3(我记得的话)中,有一个名为 ChildWindow 的新对象。通过使用其中一个,您可以显示一个弹出窗口,该窗口就像一个模态对话框,您可以在使用 WPF/WinForms 等其他框架以模态方式显示标准窗口时使用它。

它的行为方式略有不同,但一旦掌握了,也没那么糟糕。无论如何,Cinch V2 也为此提供了一项服务。理论上,它与 Cinch V2 for WPF 中的 WPFUIVisualizerService 没有太大区别。让我们看看服务契约,好吗?

/// <summary>
/// This interface defines a UI controller which can be used to ChildWindows
/// from a ViewModel
/// </summary>
public interface IChildWindowService
{
    /// <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 ChildWindow associated with the given key
    /// calling code is not blocked, and will not wait on the ChildWindow being
    /// closed. So this should only be used when there is no code dependant on
    /// the ChildWindows DialogResult. If you want to use the result of the ChildWindow
    /// being shown you can should create a callback delegate for the completedProc
    /// </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="completedProc">Callback used when UI closes (may be null)</param>
    void Show(string key, object state, EventHandler<UICompletedEventArgs> completedProc);
}

在该服务的实际 Silverlight 实现内部有一个 Dictionary<string,Type>,ViewModel 可以要求此服务使用字符串键创建一个特定的 ChildWindow,Silverlight 实现该服务将创建并显示该类型的一个实例,并将状态参数值作为 DataContext 传递给新构造的匹配请求字符串的 ChildWindow

以下是如何在 Silverlight ViewModel 中使用此服务的示例。请注意,虽然 ChildWindow 看起来是模态的,但它不会阻止调用代码继续执行,即它是非阻塞的,所以不要如果您依赖 ChildWindow 的结果,请做任何其他事情。请在 ChildWindow 关闭时调用的回调函数中完成所有这些工作。这在下面的示例中有所显示。

bool? dialogResult = null;
ChildWindowService.Show("PlayedGameChildWindow", 
      new PlayedGameViewModel(GameText), (s, e) =>
        {
            dialogResult = e.Result;
            string result = dialogResult.HasValue && 
                            dialogResult.Value ? "ok" : "Cancel";
            //you can do what you like with dialogResult
        });
//NOTE : You should not do anymore here, as the ChildWindow, although it appears
//modal, it does not block parent code.

更细心的人可能会想,好吧,服务是如何知道这些视图字符串的呢?这里有几种选择。

您可以使用 Cinch V2 PopupNameToViewLookupKeyMetadata 属性标记您的自定义 ChildWindows,如下所示,并在 App.xaml.cs 中使用 Cinch V2 引导程序。这将遍历您指定的所有程序集,并查找具有此 PopupNameToViewLookupKeyMetadata 属性的类型,并将其添加到服务中。

[PopupNameToViewLookupKeyMetadata("PlayedGameChildWindow",typeof(PlayedGameChildWindow))]
public partial class PlayedGameChildWindow : ChildWindow

所以您将在 App.xaml.cs 中看到这个:

CinchBootStrapper.Initialise(new List<Assembly> { typeof(App).Assembly });

这种方法的唯一问题是,如果您使用多个 XAP,而您正在使用 MEF DeploymentCatalogs 下载它们,您将不一定有所有程序集传递给 Cinch V2 引导程序。那么在这种情况下我们该怎么办?答案很简单,我们只是手动将其直接添加到服务中。请记住,这是一个共享服务,所以每当我们向其中添加内容时,所有使用此服务的 ViewModel 都将看到其效果。那么,让我们看看如何手动将视图添加到 ChildWindowService

IChildWindowService childWindowService = 
     ViewModelRepository.Instance.Resolver.Container.GetExport<IChildWindowService>().Value;
childWindowService.Register("SomeString",typeof(SomeChildWindow));

看,我们是如何直接将东西添加到 ChildWindowService 的?有一点需要注意,您显然需要在知道 ChildWindowType 的地方执行此操作。

还提供了一个测试版本的服务,您可以在测试中使用,它的工作方式与 Cinch V2 for WPF 中的 IUIVisualizerService 差不多。这是测试代码可能看起来的快速片段:

testChildWindowService.ShowResultResponders.Enqueue
    (() =>
    {
        return new UICompletedEventArgs()
                    {
                        State = WHATEVER STATE YOU LIKE,
                        Result = true
                    } ;
    }
    );

其思想与大多数 Cinch V2 测试服务一样,我们通过在测试用例中使用模拟用户可能进行的实际操作的 Func 委托来排队。总之,这与 Cinch V2 中的 IUIVisualizerService 类似。

UI 服务

UI 服务实际上是非共享服务,可用于为特定 ViewModel 提供设计时/运行时数据。Cinch V2 的编写考虑了服务的使用,为 ViewModel 提供数据。这有很多好处,例如:

  • 所有服务都使用接口,因此便于模拟,甚至替换(假设您需要支持两个不同的 WCF 服务,或两个不同的数据库,只需切换相应的服务)。
  • 它们允许设计时服务和运行时服务。
  • 它们允许在测试代码中进行同步获取,同时允许在实际运行时服务中进行异步获取。

我不想在这篇文章中过多地深入这个领域,但我会向您展示一个如何配置同步服务和异步服务的示例。我们将在讨论演示应用程序时更详细地研究这一点。

同步服务示例

假设我们有一个 ViewModel,它需要使用一个 UI 服务(这个来自 Cinch V2 代码库中包含的 WPF 演示应用程序),该服务需要执行一些磁盘操作来保存和加载一些 XML 数据,服务契约如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace CinchV2DemoWPF
{
    /// <summary>
    /// Data service used by the <c>ImageLoaderViewModel</c> to carry out Save/open
    /// operations
    /// </summary>
    public interface IImageDiskOperations
    {
        /// <summary>
        /// Saves viewModelsToSave to a XML file, this demonstrates the use of
        /// the <c>SaveFileService</c> from the <c>ImageLoaderViewModel</c>
        /// </summary>
        bool Save(string fileName, IEnumerable<ImageViewModel> viewModelsToSave);
 
        /// <summary>
        /// retusn a  List<ImageViewModel> from an XML file, this demonstrates the use of
        /// the <c>OpenFileService</c> from the <c>ImageLoaderViewModel</c>
        /// </summary>
        List<ImageViewModel> Open(string fileName);
    }
}

我们有一些 ViewModel 代码,看起来像这样:

[ExportViewModel("ImageLoaderViewModel")]
public class ImageLoaderViewModel : ViewModelBase
{
    private IImageDiskOperations imageDiskOperations;
 
     [ImportingConstructor]
    public ImageLoaderViewModel(
        IImageDiskOperations imageDiskOperations)
    {
        //setup services
        this.imageDiskOperations = imageDiskOperations;
    }
 
 
    /// <summary>
    /// Create a new List<ImageViewModel> by reading a XML file using XLINQ
    /// </summary>
    private void ExecuteOpenExistingFileCommand(Object args)
    {
        openFileService.InitialDirectory = @"C:\";
        openFileService.Filter = ".xml | XML Files";

        var result = openFileService.ShowDialog(null);
        if (result.HasValue && result.Value == true)
        {
            try
            {
        //use the service to load some XML data of disk
                List<ImageViewModel> xmlReadViewModels = 
        imageDiskOperations.Open(openFileService.FileName);

        ....
        ....
            }
            catch (Exception ex)
            {
                messageBoxService.ShowError(
                    string.Format("An error occurred opening file\r\n{0}", ex.Message));
            }
        }
    }
}

我们可以构建一个设计时服务,但没什么意义,因为这段代码是基于用户单击按钮触发 ICommand 的,所以对于这个特定的服务,我们不需要提供设计时服务,但我们显然需要创建一个运行时服务,但我们可以使用 MEFedMVVM 的强大功能将其标记为在设计时/运行时之间共享。这是同步演示应用程序服务实现的外观:

注意:为简洁起见,我仅显示 IImageDiskOperations.Open(string fileName) 方法。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using System.Xml.Linq;
using System.Xml;
 
using MEFedMVVM.ViewModelLocator;
 
namespace CinchV2DemoWPF
{
    //Extension method helper class
    public static class CustomXElementExtensions
    {
        public static string SafeValue(this XElement input)
        {
            return (input == null) ? string.Empty : (string)input.Value;
        }
    }
 
    //The actual service, see how its shared between design time/runtime
    [PartCreationPolicy(CreationPolicy.Shared)]
    [ExportService(ServiceType.Both, typeof(IImageDiskOperations))]
    public class ImageDiskOperations : IImageDiskOperations
    {
 
        public bool Save(string fileName, 
        IEnumerable<ImageViewModel> viewModelsToSave)
        {
        //skipped for brevity  
        }
 
        public List<ImageViewModel> Open(string fileName)
        {
            var xmlImageViewModelResults =
                from imageVM in StreamElements(fileName, "ImageVM")
                select new ImageViewModel
                {
                    ImagePath = imageVM.Element("ImagePath").SafeValue(),
                    FileName = imageVM.Element("FileName").SafeValue(),
                    FileDate = DateTime.Parse(imageVM.Element("FileDate").SafeValue()),
                    FileExtension = imageVM.Element("FileExtension").SafeValue(),
                    FileSize = int.Parse(imageVM.Element("FileSize").SafeValue()),
                    Rating = int.Parse(imageVM.Element("Rating").SafeValue())
                };
 
            return xmlImageViewModelResults.ToList();
        }
 
        public static IEnumerable<XElement> StreamElements(string uri, string name)
        {
            using (XmlReader reader = XmlReader.Create(uri))
            {
                reader.MoveToContent();
                while (reader.Read())
                {
                    if ((reader.NodeType == XmlNodeType.Element) &&
                      (reader.Name == name))
                    {
                        XElement element = (XElement)XElement.ReadFrom(reader);
                        yield return element;
                    }
                }
                reader.Close();
            }
        }
    }
}

我希望您能看出,由于此服务实际上只是实现了服务契约接口 IImageDiskOperations,因此可以使用标准模拟库(如 Moq/RhinoMocks)轻松模拟它。事实上,我们甚至可以创建一个测试替身(手动模拟),它是一个类,可以用于测试,并且它简单地实现了服务契约接口 IImageDiskOperations

所以这展示了如何使用同步服务。我还没有讨论设计时与运行时,但我将在下一部分以及后续文章中讨论。

异步服务示例

同步服务固然好,但偶尔(甚至更常见)我们需要执行耗时操作,这可能会导致 UI 变得无响应,因此我们需要对这些调用进行线程化。我们如何创建 ViewModel 可以使用的 UI 服务,这些服务可能耗时很长?嗯,事实证明,这并没有太大区别。让我们来检查其中一个。

假设我们有一个服务契约接口(再次,来自 Cinch V2 WPF 演示应用程序),它需要在后台获取数据,使用您喜欢的任何后台获取技术(BackgroundWorkerThreadPoolThreadTask、Cinch BackgroundTaskManager...您选择)。

/// <summary>
/// Data service used by the <c>ImageLoaderViewModel</c> to obtain data
/// </summary>
public interface IImageProvider
{
    void FetchImages(string imagePath, Action<List<ImageData>> callback);
}

假设我们有一个 ViewModel 需要使用此服务,如下所示:

[ExportViewModel("ImageLoaderViewModel")]
public class ImageLoaderViewModel : ViewModelBase
{
    private IImageProvider imageProvider;
 
     [ImportingConstructor]
    public ImageLoaderViewModel(
        IImageProvider imageProvider)
    {
        //setup services
        this.imageProvider= imageProvider;
    }

    //left out for brevity
    //left out for brevity
    //left out for brevity
    //left out for brevity
    

    private void LoadImages(string imagePath)
    {
        imageProvider.FetchImages(imagePath, LoadImagesFromRetrievedData);
    }


    private void LoadImagesFromRetrievedData(List<ImageData> data)
    {
        //left out for brevity
        //left out for brevity
        //left out for brevity
        //left out for brevity
    }
}

我们可以看到服务被调用了,但我们向它提供了一个回调 Action<List<ImageData> 委托,所以当它完成时,它能够使用期望结果的回调委托回调所需的 ViewModel 方法。

让我们来看看这个服务契约实现的运行时版本,好吗?这是 Cinch V2 WPF 演示应用程序中的一个。请注意,我使用了 Cinch BackGroundTaskManager 来执行后台工作,但您可以使用任何您喜欢的东西。哦,我还显示了完整的服务实现,因为我认为最好能看到这个完整的示例。

/// <summary>
/// Runtime implementation of the 
/// Data service used by the <c>ImageLoaderViewModel</c> to obtain data
/// </summary>
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.Runtime, typeof(IImageProvider))]
public class RunTimeImageProvider : IImageProvider
{
    #region Data
    private BackgroundTaskManager<string, List<ImageData>> bgWorker = 
        new BackgroundTaskManager<string, List<ImageData>>();
    #endregion
 
    #region Public Methods/Properties
 
    public void FetchImages(string imagePath, Action<List<ImageData>> callback)
    {
        bgWorker.TaskFunc = (argument) =>
            {
                return FetchImagesInternal(argument);
            };
 
        bgWorker.CompletionAction = (result) =>
            {
                callback(result);
            };
 
        bgWorker.WorkerArgument = imagePath;
        bgWorker.RunBackgroundTask();
 
    }
 
    /// <summary>
    /// To allow this class to be unit tested stand alone
    /// See CinchV1 articles about Unit Testing for this
    /// Or comments in Cinch BackgroundTaskManager<T> class
    /// </summary>
    public BackgroundTaskManager<string,List<ImageData>> BgWorker
    {
        get { return bgWorker; }
    }
 
    #endregion
 
    #region Private Methods
    private List<ImageData> FetchImagesInternal(string imagePath)
    {
        List<string> imageFiles = new List<string>();
 
        string strFilter = "*.jpg;*.png;*.gif";
        string[] filters = strFilter.Split(';');
        foreach (string filter in filters)
        {
            imageFiles.AddRange(Directory.GetFiles(imagePath, filter));
        }
 
        List<ImageData> images = new List<ImageData>();
 
        if (imageFiles.Count > 0)
        {
            int maxImages = imageFiles.Count > 20 ? 20 : imageFiles.Count;
 
            for (int i = 0; i < maxImages; i++)
            {
                FileInfo fi = new FileInfo(imageFiles[i]);
                ImageData id = new ImageData();
                id.ImagePath = imageFiles[i];
                id.FileDate = fi.LastWriteTime;
                id.FileExtension = fi.Extension;
                id.FileName = fi.Name;
                id.FileSize = (int)fi.Length / 1024;
                images.Add(id);
            }
        }
 
        return images;
 
    }
    #endregion
}

这就是运行时服务的工作方式,那么设计时服务呢?好吧,这是完整的代码:

[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.DesignTime, typeof(IImageProvider))]
public class DesigntimeImageProvider : IImageProvider
{
    public void FetchImages(string imagePath, Action<List<ImageData>> callback)
    {
        List<ImageData> fakeImages = new List<ImageData>();
        for (int i = 0; i < 10; i++)
        {
            ImageData id = new ImageData();
            id.ImagePath = @"C:\Users\Public\Pictures\Sample Pictures\Desert.jpg";
            id.FileDate = DateTime.Now;
            id.FileExtension = "*.jpg";
            id.FileName = "Desert.jpg";
            id.FileSize = 223;
            fakeImages.Add(id);
        }
        callback(fakeImages);
    }
}

如何测试呢?嗯,您有几种选择。Moq/RhinoMocks 提供了调用回调委托的功能。或者您可以创建一个测试替身,它很可能看起来与上面显示的设计时服务非常相似。如果您正在进行完整的 UI-数据库测试(集成测试),您很可能也想测试线程化,但我将把它留给读者练习。我将要说的是,Cinch V1 确实支持测试其自己的 BackGroundTaskManager,您可以通过此链接了解更多信息:CinchV.aspx

正如我刚才讨论的,Moq/RhinoMocks 提供了一种处理回调委托的方法;事实上,这里有一个使用 Moq 测试回调委托的小示例:

Mock<IImageProvider> mockImageProvider = new Mock<IImageProvider>();
Action<List<ImageData> action = null;
 
mockImageProvider.Setup(b => b.GetData(It.IsAny<Action<List<ImageData>>()))
.Callback<Action<List<ImageData>>(a => action = a);
 
ImageLoaderViewModel vm = new ImageLoaderViewModel(mockImageProvider.Object);
 
action.Invoke(GetFakeListImageData());

但正如我所说,如果您不喜欢模拟框架,您可以使用类似于设计时服务或测试替身的东西。

暂时就到这里

如果您喜欢这篇文章,并且觉得它对您有帮助,能否请您通过留下投票/评论来表示支持?

和以前一样,如果您有任何深入的 MEF 相关问题,您应该通过他的博客 C# Disciples 或使用 MEFedMVVM CodePlex 站点 联系 Marlon Grech。其他任何 Cinch V2 问题都将在下一期 CinchV2 文章中得到解答。

© . All rights reserved.