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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (22投票s)

2010 年 8 月 3 日

CPOL

24分钟阅读

viewsIcon

87383

如果 Jack Daniels 制造 MVVM 框架。

目录

引言

上次我们讨论了 Cinch V2 的新功能和保持不变的功能。在本文中,我们将介绍 Cinch V2 WPF 演示应用程序,该应用程序随 Cinch V2 代码库一起在 Cinch 的 CodePlex 网站上提供。

正如我所承诺的,在每篇文章中,我都会展示 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 文章链接

好的,这就是文章路线图的样子。我想现在是时候深入研究本文的实质内容了,所以我们开始吧

它有什么功能

对于 Cinch V1,我创建了一个 LOB(业务线)应用程序,但在工作中,我正在开发一个大型 LOB 应用程序,说实话,我只是厌倦了创建另一个 LOB 应用程序,而且 Cinch V1 和 V2 之间的共同点在旧的 Cinch V1 演示中可以非常清楚地看到。真正改变的是 UI 服务,并且附加属性现在已成为 Blend 行为。

所以这次我决定做一些更有创意的事情,这让一些读者感到沮丧。然而,一些读者可能会很高兴地知道,我已经被另外两个使用 Cinch 的 CodeProject 用户联系,其中一个将编写一篇 Cinch V2 LOB 文章,另一个将编写一个 VB.NET Cinch V2 应用程序,当这些 CodeProject 用户告诉我他们完成文章编写后,我将从 Cinch CodePlex 网站链接到这两个应用程序。

无论如何,那都不是重点。正如我所说,我决定做一些不同的事情。那么,WPF 演示应用程序有什么功能呢?

嗯,我想这可以用以下几点来概括

  • 创建一个带选项卡的主界面,允许显示 n 个可关闭的选项卡,其中每个选项卡可以是“关于”选项卡或“图像查看器”选项卡。
  • 创建一个图像查看器视图,显示来自特定文件夹(在 App.Config 中指定)的图像,并允许用户对每个图像进行评分,以及保存和加载每个图像收到的评分。
  • 创建一个“关于”视图,允许用户打开一个弹出窗口以查看各种网站。

现在,这看起来可能不多,但相信我,这足以展示 Cinch 的大部分功能。

它长什么样

现在我已经谈论了它的功能,让我们来看看它长什么样,好吗?

当您启动应用程序时,它应该看起来像这样(请记住更改 App.Config 以指向您有一些图像的位置)。

从上图中可以看出,它是一个单窗口应用程序。主窗口名为 MainWindow,它有一个 TabControl,其中托管着许多视图。此 TabControl 通过 MainWindowViewModel 中的 ObservableCollection<WorkSpaceData> 填充。

您在下面看到的第一个视图名为 ImageLoaderView,它只是显示您 PC 中的许多图像。使用的路径在 App.Config 中配置。

ImageLoaderView 中,可以使用“添加评分”按钮启动 AddImageRatingPopup。显然,弹出窗口的显示实际上是在名为 ImageLoaderViewModel 的 ViewModel 中完成的。

MainWindow 中显示的下一个视图名为 AboutView,它使用了 AboutViewModel

AboutView 中,还可以启动 AboutViewLinkRequestedPopup。此弹出窗口的显示是在 AboutViewModel 中完成的。

整体结构

下图说明了 WPF 演示应用程序的视图/视图模型和弹出窗口的整体结构。还有许多辅助类和服务,但我将在遇到它们时讨论。现在,只需注意下面所示的 WPF 演示应用程序的整体结构

它是如何工作的

接下来的三个部分将尝试概述 WPF 演示应用程序中视图/视图模型和弹出窗口执行的所有功能。

弹出窗口

在本节中,我们将讨论如何从您的视图模型中显示弹出窗口。

确保弹出窗口可供显示

你们中的一些人可能熟悉早期的 Cinch 文章,甚至因为您使用过 Cinch V1 而熟悉所有这些工作原理,但是你们中的一些人可能不知道,所以对于那些新手来说,基本思想如下

有一个处理显示弹出窗口的服务,名为 IUIVisualizerService,它包含一个 Dictionary<string, Type>,这样 IUIVisualizerService 的使用者可以简单地通过名称 (string) 从内部 Dictionary<string, Type> 请求一个弹出窗口,然后 IUIVisualizerService 将在 Dictionary<string, Type> 中找到该条目并创建该 Type 的新实例并显示它。

为了清楚起见,这是 WPF 的完整 IUIVisualizerService 服务实现

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

欲了解更多信息,请参阅链接:CinchV2_2.aspx#CoreServices 并阅读 WPFUIVisualizerService 部分。

您可能想知道 IUIVisualizerService Dictionary<string, Type> 是如何及时填充的,以确保当请求弹出窗口时,它存在于 Dictionary<string, Type> 中。嗯,这可以通过两种不同的方式发生。

手动向字典中添加项目

您可以在适当的时候手动将弹出窗口项目添加到 IUIVisualizerService Dictionary<string, Type> 中,例如在应用程序构建或甚至启动时。因此,您可能会有这样的代码

public partial class App : Application
{
    public App()
    {
        ViewModelRepository.Instance.Resolver.Container.
            GetExport<IUIVisualizerService>().Value.Register(
                "AddImageRatingPopup", 
                typeof(AddImageRatingPopup));

        InitializeComponent();
    }
}

该行将确保 IUIVisualizerService Dictionary<string, Type> 使用正确的 KeyValuePair 进行填充。

自动查找作为弹出窗口的类型

手动添加东西固然很好,但 Cinch V2 提供了一种更好的方法,通过使用属性和在启动时运行的引导程序。因此,如果我们有一个我们知道将与 IUIVisualizerService 一起使用的弹出窗口,我们只需在其代码隐藏中按如下方式对其进行属性化

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

因此,我们现在有了一个带有属性的弹出窗口,但这只是一半的故事,我们需要确保有东西检查这些 PopupNameToViewLookupKeyMetadata 属性。这就是 CinchBootStrapper 的工作。基本上,CinchBootStrapper 接受一个 IEnumerable<Assembly> 来检查传入的 IEnumerable<Assembly> 中带有 PopupNameToViewLookupKeyMetadata 属性的 Type,如果它们有,则将它们添加到 IUIVisualizerService 中,以备后用。您所要做的就是确保在应用程序构建或应用程序启动时调用 CinchBootStrapper

这是一个来自 Cinch V2 WPF 演示应用程序的示例

/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
    #region Initialisation
    /// <summary>
    /// Initiliase Cinch using the CinchBootStrapper. 
    /// </summary>
    public App()
    {
        CinchBootStrapper.Initialise(new List<Assembly> { typeof(App).Assembly });
        InitializeComponent();
    }
    #endregion
}

显示特定弹出窗口

因此,一旦您在 IUIVisualizerService Dictionary<string, Type> 中有一个带有 KeyValuePair 条目的弹出窗口,从 ViewModel 显示弹出窗口就非常简单了。您只需这样做

namespace CinchV2DemoWPF
{
    [ExportViewModel("AboutViewModel")]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class AboutViewModel : ViewModelBase
    {
        public IUIVisualizerService uiVisualizer;

        [ImportingConstructor]
        public AboutViewModel(IUIVisualizerService uiVisualizer)
        {

            this.uiVisualizer = uiVisualizer;
            AboutViewEventToVMFiredCommand = 
              new SimpleCommand<Object, EventToCommandArgs>(
              ExecuteAboutViewEventToVMFiredCommand);
        }
        #endregion

        private void ExecuteAboutViewEventToVMFiredCommand(EventToCommandArgs args)
        {
            //Create popups ViewModel
            AboutViewLinkRequestedPopupViewModel aboutViewLinkRequestedPopupViewModel =
                new AboutViewLinkRequestedPopupViewModel();

            switch ((String)args.CommandParameter)
            {
                case "Home":
                    aboutViewLinkRequestedPopupViewModel.NavigateTo = 
                        @"http://cinch.codeplex.com/";
                    break;
                case "Source":
                    aboutViewLinkRequestedPopupViewModel.NavigateTo =
                        @"http://cinch.codeplex.com/SourceControl/list/changesets";
                    break;
            }
            //show popup
            uiVisualizer.ShowDialog("AboutViewLinkRequestedPopup", 
                                    aboutViewLinkRequestedPopupViewModel);
        }
    }
}

Cinch V2 还提供了 IUIVisualizerService 的测试替身,您可以用于测试,它的工作原理与此处描述的非常相似:CinchV.aspx#UIVisualizer。唯一的区别是您不再需要为 IUIVisualizerService 解析任何东西,您只需将 TestUIVisualizerService 注入到您正在测试的 ViewModel 中。

这种差异是由于 Cinch V1 处理服务的方式,使用 DI/IOC 和常见的 ServiceLocator 模式。而 Cinch V2 只依赖于通过构造函数参数或属性设置器注入的所有内容。因此,如果您想使用服务的测试版本,您只需从单元测试代码中注入测试版本(上面示例中的 TestUIVisualizerService),而不是真实版本。

应用程序管理

要使演示应用程序正常工作,实际上只需要两件事,它们如下所示

App.Config

必须App.Config 中指定一个有效的图像位置,以便应用程序正常工作。这是我的 App.Config 文件在我运行家庭演示应用程序时的样子

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="YourImagePath" 
      value="C:\Users\Public\Pictures\Sample Pictures"/>
  </appSettings>
</configuration>

应用程序构建

正如我在上面的弹出窗口部分中提到的,Cinch V2 支持通过使用属性进行弹出窗口查找和各种其他查找,其中在启动时找到具有这些属性的 Type。但为了使其正常工作,需要告诉 Cinch 要查看哪些程序集。对于演示应用程序,所有视图/弹出窗口都在与演示相同的程序集中定义,因此我需要告诉 Cinch 在该程序集中查找 Cinch 属性的 Type。这是通过应用程序构造函数中的以下代码完成的

/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
    #region Initialisation
    /// <summary>
    /// Tell Cinch what Assemblies to look in for Cinch attributed types that
    /// can be cached, to prevent the user from manually having to add things
    /// to lookup Dictionaries later
    /// </summary>
    public App()
    {
        CinchBootStrapper.Initialise(
          new List<Assembly> { typeof(App).Assembly });
        InitializeComponent();
    }
    #endregion
}

Cinch BootStrapper 接受 IEnumerable<Assembly>,因此如果将弹出窗口拆分为不同的程序集,您可以传入其他 DLL。

视图/视图模型

Cinch V2 WPF 演示应用程序中有许多视图模型。因此,我们将依次检查它们,并了解视图/视图模型如何协同工作。

主窗口 / 主窗口视图模型

MainWindow 仅充当容器,用于在我在另一篇 Cinch V2 文章中提到的专用 TabControl 中托管许多其他视图:CinchV2_3.aspx#Workspaces

首先阅读该部分,然后您会更好地理解本部分。正如我所说,MainWindow 只是在专用的 TabControl 中托管其他视图,所以让我们看看 MainWindow 的 XAML。

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.WPF"
    xmlns:meffed="http:\\www.codeplex.com\MEFedMVVM"
    xmlns:local="clr-namespace:CinchV2DemoWPF;assembly="
    xmlns:Microsoft_Windows_Themes=
      "clr-namespace:Microsoft.Windows.Themes;
       assembly=PresentationFramework.Aero"
    x:Class="CinchV2DemoWPF.MainWindow"
    Icon="/CinchV2DemoWPF;component/Images/CinchIcon.png"
    Title="CinchV2 : WPF Demo app"
    MinHeight="600" 
    MinWidth="800"
    WindowState="Maximized"
    WindowStartupLocation="CenterScreen"
    meffed:ViewModelLocator.ViewModel="MainWindowViewModel">

    <Window.Resources>
        <DataTemplate DataType="{x:Type CinchV2:WorkspaceData}">
            <AdornerDecorator>
                <Border HorizontalAlignment="Stretch" 
                        VerticalAlignment="Stretch" 
                        CinchV2:NavProps.ViewCreator="{Binding}"/>
            </AdornerDecorator>
        </DataTemplate>
    </Window.Resources>

    <Grid>
         ......
         ......
         ......
         ......
        <local:TabControlEx Grid.Row="1" x:Name="tab1"
            ItemsSource="{Binding Views}"  TabStripPlacement="Left"
                CinchV2:NavProps.ShouldHideHostWhenNoItems="true"
                ItemContainerStyle="{StaticResource TabItemStyleVerticalTabs}"
                Style="{DynamicResource TabControlStyleVerticalTabs}" 
                IsSynchronizedWithCurrentItem="True" 
                DisplayMemberPath="DisplayText">

            <local:TabControlEx.ContextMenu>
                <ContextMenu IsOpen="{Binding ShowContextMenu, Mode=OneWay}">
                    <Menu x:Name="menu" Margin="0,0,0,0" 
                        Height="Auto" Foreground="Black"
                        ItemContainerStyle="{StaticResource ContextMenuItemStyle}"
                        ItemsSource="{Binding MainWindowOptions}" 
                        BorderBrush="Transparent"
                        VerticalAlignment="Top" 
                        Background="Transparent" />
                </ContextMenu>
            </local:TabControlEx.ContextMenu>
        </local:TabControlEx>
    </Grid>
</Window>

可以看出,有一个 TabControlEx 和一个 ContextMenu,以及 MeffedMVVM ViewModelLocator.ViewModel 附加 DP 来解析 ViewModel。现在让我们将注意力转向 MainWindowViewModel,看看它长什么样,我们期望它提供以下功能

  • ContextMenu 支持。
  • TabControlEx 项目的一些初始 WorkSpace

这是 MainWindowViewModel 中相关代码,我们确实可以看到我们刚才提到的两个功能都已满足。此 MainWindowViewModel 提供 List<CinchMenuItem> MainWindowOptions 属性,该属性在 MainWindow 中用作 ContextMenu,它还将 WorkspaceData 项目添加到 Views 属性中,该属性在 MainWindow 中用作 TabControlItemsSource

/// <summary>
/// This ViewModel demonstrates how to use WorkSpaces and Menus. You will
/// need to look in the MainWindow.xaml and also the AppStyles.xaml ResourceDictionary
/// to see how the Styles are used to tie up with this ViewModel
/// </summary>
[ExportViewModel("MainWindowViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class MainWindowViewModel : ViewModelBase
{
    #region Data
    private bool showContextMenu = false;
    private IViewAwareStatus viewAwareStatusService;
    #endregion

    #region Ctor
    [ImportingConstructor]
    public MainWindowViewModel(IViewAwareStatus viewAwareStatusService)
    {
        this.viewAwareStatusService = viewAwareStatusService;
        this.viewAwareStatusService.ViewLoaded += ViewAwareStatusService_ViewLoaded;
    }
    #endregion

    #region Private Methods
    /// <summary>
    /// Creates and returns the menu items
    /// </summary>
    private List<CinchMenuItem> CreateMenus()
    {

        List<CinchMenuItem> menu = new List<CinchMenuItem>();

        CinchMenuItem menuActions = new CinchMenuItem("Actions");
        menu.Add(menuActions);

        CinchMenuItem menuAbout = new CinchMenuItem("About CinchV2");
        menuAbout.Command = new SimpleCommand<object, object>((x) =>
        {
            WorkspaceData workspace2 = 
                new WorkspaceData(@"/CinchV2DemoWPF;component/Images/About.png",
                "AboutView", null, "About Cinch V2", true);
            Views.Add(workspace2);
            ShowContextMenu = false;
        });
        menuActions.Children.Add(menuAbout);


        CinchMenuItem menuImages = new CinchMenuItem("ImageLoaderView");
        menuImages.Command = new SimpleCommand<object, object>((x) =>
        {
            String imagePath = 
              ConfigurationManager.AppSettings["YourImagePath"].ToString();

            WorkspaceData workspaceImages = 
                new WorkspaceData(@"/CinchV2DemoWPF;component/Images/imageIcon.png",
                "ImageLoaderView", imagePath, "Image View", true);
            Views.Add(workspaceImages);
            ShowContextMenu = false;
        });
        menuActions.Children.Add(menuImages);

        return menu;
    }

    private void ViewAwareStatusService_ViewLoaded()
    {

        if (Designer.IsInDesignMode)
            return;

        String imagePath = 
          ConfigurationManager.AppSettings["YourImagePath"].ToString();

        WorkspaceData workspace1 = 
            new WorkspaceData(@"/CinchV2DemoWPF;component/Images/imageIcon.png",
            "ImageLoaderView", imagePath, "Image View", true);

        WorkspaceData workspace2 = 
           new WorkspaceData(@"/CinchV2DemoWPF;component/Images/About.png",
           "AboutView", null, "About Cinch V2", true);

        Views.Add(workspace1);
        Views.Add(workspace2);
        SetActiveWorkspace(workspace1);
    }
    #endregion

    #region Public Properties
    /// <summary>
    /// Returns the bindbable Main Window options
    /// </summary>
    public List<CinchMenuItem> MainWindowOptions
    {
        get
        {
            return CreateMenus();
        }
    }

    /// <summary>
    /// ShowContextMenu
    /// </summary>
    static PropertyChangedEventArgs showContextMenuArgs =
        ObservableHelper.CreateArgs<MainWindowViewModel>(x => x.ShowContextMenu);

    public bool ShowContextMenu
    {
        get { return showContextMenu; }
        private set
        {
            showContextMenu = value;
            NotifyPropertyChanged(showContextMenuArgs);
        }
    }
    #endregion

}

ImageLoaderView / ImageLoaderViewModel

ImageLoaderViewModelCinch V2 WPF 演示应用程序中最复杂的一个,它执行以下功能

  • 加载一组图像(文件夹在 App.Config 中指定),这些图像使用非核心服务加载,还提供了设计时版本。
  • 允许使用反向的 SimpleCommandCompletedAwareCommandTrigger 来显示/隐藏操作区域。
  • 允许打开评级弹出窗口(下面描述的 AddImageRatingPopup)。
  • 使用各种其他标准服务,如 MessageBoxService/SaveFileService/OpenFileService

我现在将解释这些部分的每一个是如何在 Cinch V2 WPF 演示应用程序中实现的。

使用非核心服务加载一组图像

正如我在之前的一篇 Cinch V2 文章中提到的,Cinch V2 具有核心服务概念,例如 IMessageBoxServiceISaveFileServiceIOpenFileService 等,但它也使用了非核心(应用程序特定)服务。这些应用程序特定服务是额外的接口和实现,它们也用 MeffedMVVM 属性标记,以便可以将它们导入到 ViewModel 中。

这种思维背后的基本原理是它非常可测试。想象一下,您的 ViewModel 正在从外部源(例如 Web 服务或 WCF 服务)获取数据,并且 Web/WCF 服务正在并行开发。为了测试您的 ViewModel,通过契约接口与外部代码通信是一个好主意。这不仅使客户端应用程序和 Web 服务/WCF 服务之间的契约众所周知,而且还促进了测试。如果 ViewModel 接受 ISomeInterface 服务并期望从某个地方获取数据,您可以使用真实的服务(它将调用 Web 服务/WCF 服务),或者您可以注入一个测试替身,并简单地测试您的 ViewModel,而无需依赖任何 Web 服务/WCF 服务(可能甚至还没有准备好进行测试)。这一切都与可测试性有关。

无论如何,Cinch WPF 演示代码使用了两个这样的服务,下面将介绍它们

IImageDiskOperations

ImageLoaderViewModel 使用 IImageDiskOperations 服务将图像评分保存到/从用户选择的 XML 文件中。IImageDiskOperations 服务契约如下所示

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

而真实的 IImageDiskOperations(设计时也使用)实现如下所示

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
{
    public static class CustomXElementExtensions
    {
        public static string SafeValue(this XElement input)
        {
            return (input == null) ? string.Empty : (string)input.Value;
        }
    }

    /// <summary>
    /// Runtime/Deigntime implementation of the 
    /// ImageDiskOperations service used by
    /// the <c>ImageLoaderViewModel</c> to save/open data
    /// </summary>
    [PartCreationPolicy(CreationPolicy.Shared)]
    [ExportService(ServiceType.Both, typeof(IImageDiskOperations))]
    public class ImageDiskOperations : IImageDiskOperations
    {
        #region IImageDiskOperations Members

        /// <summary>
        /// Saves viewModelsToSave to a XML file, this demonstrates the use of
        /// the <c>SaveFileService</c>
        /// from the <c>ImageLoaderViewModel</c>
        /// </summary>
        public bool Save(string fileName, 
               IEnumerable<ImageViewModel> viewModelsToSave)
        {

            CreateInitialFile(fileName, viewModelsToSave.First());
            IQueryable<ImageViewModel> allButFirst = 
                viewModelsToSave.Skip(1).AsQueryable<ImageViewModel>();

            foreach (ImageViewModel imageVM in allButFirst)
            {
                AppendToFile(fileName, imageVM);
            }
            return true;
        }

        /// <summary>
        /// retusn a  List<ImageViewModel> from an XML file,
        /// this demonstrates the use of
        /// the <c>OpenFileService</c>
        /// from the <c>ImageLoaderViewModel</c>
        /// </summary>
        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();
        }

        #endregion
        #region Private Methods

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

        private static void AppendToFile(string fullXmlPath, ImageViewModel imageVM)
        {
            XElement imagesVM_XMLDocument = XElement.Load(fullXmlPath);
            imagesVM_XMLDocument.Add(new XElement("ImageVM",
                        new XElement("ImagePath", imageVM.ImagePath),
                        new XElement("FileName", imageVM.FileName),
                        new XElement("FileDate", imageVM.FileDate),
                        new XElement("FileExtension", imageVM.FileExtension),
                        new XElement("FileSize", imageVM.FileSize),
                        new XElement("Rating", imageVM.Rating)));

            imagesVM_XMLDocument.Save(fullXmlPath);
        }

        private static void CreateInitialFile(string fullXmlPath, ImageViewModel imageVM)
        {
            XElement imagesVM_XMLDocument =
                new XElement("AllImageViewModels",
                    new XElement("ImageVM",
                        new XElement("ImagePath", imageVM.ImagePath),
                        new XElement("FileName", imageVM.FileName),
                        new XElement("FileDate", imageVM.FileDate),
                        new XElement("FileExtension", imageVM.FileExtension),
                        new XElement("FileSize", imageVM.FileSize),
                        new XElement("Rating", imageVM.Rating))
                );
            imagesVM_XMLDocument.Save(fullXmlPath);
        }
        #endregion
    }
}

IImageDiskOperations 服务按如下方式导入到 ImageLoaderViewModel

[ExportViewModel("ImageLoaderViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ImageLoaderViewModel : ViewModelBase
{

private IImageProvider imageProvider;
private IImageDiskOperations imageDiskOperations;

[ImportingConstructor]
public ImageLoaderViewModel(
    IImageProvider imageProvider,
    IImageDiskOperations imageDiskOperations)
{
    //setup services
    this.imageProvider = imageProvider;
    this.imageDiskOperations = imageDiskOperations;
}

由于 ImageLoaderViewModel 只是期望一个 IImageDiskOperations,因此在进行单元测试时,您可以轻松地向其中注入一个测试替身或模拟 IImageDiskOperations。看看这种方法的美妙之处,我认为这是 MeffedMVVM 的真正优点。

IImageProvider

IImageProvider 服务只是向 ImageLoaderViewModel 提供图像。IImageProvider 服务契约如下所示。

请注意,此服务应为异步服务,完成时会调用回调委托。

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

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

    /// <summary>
    /// Data class used by <c>IImageProvider</c>
    /// </summary>
    public class ImageData
    {
        public string ImagePath { get; set; }
        public string FileName { get; set; }
        public DateTime FileDate { get; set; }
        public string FileExtension { get; set; }
        public int FileSize { get; set; }
    }
}

真实的 IImageProvider 实现如下所示,请注意它如何使用 Cinch 多线程助手 BackgroundTaskManager<T> 来完成工作。

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

而设计时的 IImageProvider 服务如下所示。请注意,我们只是立即回调,我们根本不使用任何多线程。您也可以在单元测试中这样做。在单元测试中测试线程操作并不容易,通常涉及 WaitHandle 等。我留给您决定,但只是让您知道,您可以这样做,没问题。

/// <summary>
/// Designtime implementation of the 
/// Data service used by the <c>ImageLoaderViewModel</c> to obtain data
/// </summary>
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.DesignTime, typeof(IImageProvider))]
public class DesigntimeImageProvider : IImageProvider
{
    #region Public Methods

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

    #endregion
}

IImageProvider 服务按如下方式导入到 ImageLoaderViewModel

[ExportViewModel("ImageLoaderViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ImageLoaderViewModel : ViewModelBase
{

private IImageProvider imageProvider;
private IImageDiskOperations imageDiskOperations;

[ImportingConstructor]
public ImageLoaderViewModel(
    IImageProvider imageProvider,
    IImageDiskOperations imageDiskOperations)
{
    //setup services
    this.imageProvider = imageProvider;
    this.imageDiskOperations = imageDiskOperations;
}

由于 ImageLoaderViewModel 只是期望一个 IImageProvider,因此您可以轻松地向其中注入一个测试替身或模拟 IImageProvider

使用 SimpleCommand / CompletedAwareCommandTrigger 显示/隐藏操作区域

演示应用程序在 ImageLoaderView 上有一个不总是可见的小区域。它仅在请求了所需的 VisualState(默认是“HideActionsState”)时才可见。

我所说的区域看起来像这样,我们使用顶部的两个 Label 控件来显示/隐藏操作区域

其中有两个 Label 控件使用 EventToCommandTriggerImageLoaderViewModel 中触发 SimpleCommand

<Label FontFamily="Wingdings" Foreground="Black" 
    VerticalAlignment="Center" Margin="10,5,5,5"
    VerticalContentAlignment="Center"
    FontSize="20" FontWeight="Normal" 
    Content="þ">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseLeftButtonUp">
            <CinchV2:EventToCommandTrigger 
                    Command="{Binding ShowActionsCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Label>

<Label FontFamily="Wingdings" Foreground="Black" 
    VerticalAlignment="Center" Margin="5"
    VerticalContentAlignment="Center"
    FontSize="20" FontWeight="Normal" 
    Content="ý">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseLeftButtonUp">
            <CinchV2:EventToCommandTrigger 
                    Command="{Binding HideActionsCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Label>

其中 ImageLoaderViewModel 中的 SimpleCommand 只是触发一个空委托。

//EventToCommand triggered, see the View
ShowActionsCommand = new SimpleCommand<Object, Object>(ExecuteShowActionsCommand);
HideActionsCommand = new SimpleCommand<Object, Object>(ExecuteHideActionsCommand);

//some reverse commands, that the VM fires, and the View uses as CompletedAwareCommandTriggers
//to carry out some actions. In this case GoToStateActions are used in the View
ShowActionsCommandReversed = new SimpleCommand<Object, Object>((input) => { });
HideActionsCommandReversed = new SimpleCommand<Object, Object>((input) => { });

....
....

/// <summary>
/// Goto "ShowActionsState", Using CompletedAwareCommandTrigger
/// </summary>
private void ExecuteShowActionsCommand(Object args)
{
    ShowActionsCommandReversed.Execute(null);
}

/// <summary>
/// Goto "HideActionsState", Using CompletedAwareCommandTrigger
/// </summary>
private void ExecuteHideActionsCommand(Object args)
{
    HideActionsCommandReversed.Execute(null);
}

然后,在 ImageLoaderView 的 XAML 中,整个 UserControl 有一些 Blend 交互,它们通过使用 CompletedAwareCommandTrigger 监听这些反向 SimpleCommand,并将 UserControl 置于新的 VisualState,这取决于从 ViewModel 触发的哪个反向 SimpleCommand 导致 CompletedAwareCommandTrigger 做出反应并更改为新的 VisualState

<i:Interaction.Triggers>
    <CinchV2:CompletedAwareCommandTrigger 
                    Command="{Binding ShowActionsCommandReversed}">
        <ei:GoToStateAction StateName="ShowActionsState"/>
    </CinchV2:CompletedAwareCommandTrigger>

    <CinchV2:CompletedAwareCommandTrigger 
                    Command="{Binding HideActionsCommandReversed}">
        <ei:GoToStateAction StateName="HideActionsState"/>
    </CinchV2:CompletedAwareCommandTrigger>
</i:Interaction.Triggers>

<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="RectangleStates">
        <VisualStateGroup.Transitions>
            <VisualTransition GeneratedDuration="0:0:0.5">
                <VisualTransition.GeneratedEasingFunction>
                    <ElasticEase EasingMode="EaseInOut" 
                          Oscillations="5" Springiness="6"/>
                </VisualTransition.GeneratedEasingFunction>
            </VisualTransition>
        </VisualStateGroup.Transitions>
        <VisualState x:Name="ShowActionsState">
            <Storyboard>
                <DoubleAnimation Duration="0" To="1" 
                    Storyboard.TargetProperty=
                       "(UIElement.RenderTransform).(ScaleTransform.ScaleX)" 
                    Storyboard.TargetName="bordActions" 
                    d:IsOptimized="True"/>
                <DoubleAnimation Duration="0" To="1" 
                    Storyboard.TargetProperty=
                      "(UIElement.RenderTransform).(ScaleTransform.ScaleY)" 
                    Storyboard.TargetName="bordActions" 
                    d:IsOptimized="True"/>
            </Storyboard>
        </VisualState>
        <VisualState x:Name="HideActionsState"/>
        <VisualState x:Name="NullState"/>
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>
允许打开弹出窗口

正如你们中的一些人可能知道的,Cinch 也提供了一种显示弹出窗口的方法(我们之前也介绍过),因此将 SimpleCommand 连接到 UI 中的按钮以显示弹出窗口几乎是微不足道的。以下是您要做的

在您的 XAML 中,有一个 Button 连接到 SimpleCommand

<Button Grid.Row="0"
        Template="{StaticResource GlassButton}" 
        Margin="10" 
        HorizontalAlignment="Stretch" 
        Command="{Binding AddImageRatingCommand}">
    <StackPanel Orientation="Horizontal">
        <Label Style="{StaticResource selectedImageLabelStyle}" 
                    Content="Add Rating"/>
        <Label Style="{StaticResource selectedImageLabelStyle}" 
                    FontFamily="Wingdings 2" Content="êêêêê"/>
    </StackPanel>
</Button>

其中 ImageLoaderViewModel 声明 SimpleCommandSimpleCommand.Execute 处理程序,如下所示

AddImageRatingCommand = new SimpleCommand<Object, Object>(ExecuteAddImageRatingCommand);

....

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

这显然依赖于 IUIVisualizerServiceImageLoaderViewModel 使用 MeffedMVVM 导入,如下所示

[ExportViewModel("ImageLoaderViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ImageLoaderViewModel : ViewModelBase
{

private IMessageBoxService messageBoxService;
[ImportingConstructor]
public ImageLoaderViewModel(
    IMessageBoxService messageBoxService,
    IUIVisualizerService uiVisualizerService)
{
    //setup services
    this.messageBoxService = messageBoxService;
    this.uiVisualizerService = uiVisualizerService;
}

使用各种其他服务

WPF 演示应用程序还展示了如何使用其他几个核心 Cinch 服务,例如 IOpenFileServiceISaveFileService。让我们快速了解一下它们的实际应用

SaveFileService

由于 Cinch 提供 ISaveFileService,因此使用它几乎是微不足道的,我们只需在 ViewModel 中这样做。也可以看到此代码使用了我们之前讨论过的 IImageDiskOperations

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)
    {
        try
        {
            if (imageDiskOperations.Save(saveFileService.FileName, 
                loadedImages.AsEnumerable()))
            {
                messageBoxService.ShowInformation(string.Format(
                    "Successfully saved images to file\r\n{0}",
                    saveFileService.FileName));
            }
        }
        catch (Exception ex)
        {
            messageBoxService.ShowError(string.Format(
              "An error occurred saving images to file\r\n{0}",
              ex.Message));
        }
    }
}

注意Cinch 还提供 TestSaveFileService,您可以在 Cinch V1 文章中阅读更多相关内容。

OpenFileService

由于 Cinch 提供 IOpenFileService,因此使用它几乎是微不足道的,我们只需在 ViewModel 中这样做。也可以看到此代码使用了我们之前讨论过的 IImageDiskOperations

/// <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
        {
            List<ImageViewModel> xmlReadViewModels = 
                  imageDiskOperations.Open(openFileService.FileName);
            if (xmlReadViewModels != null)
            {
                loadedImages = xmlReadViewModels;
                LoadedImagesCV = CollectionViewSource.GetDefaultView(loadedImages);
                if (loadedImages != null)
                    LoadedImagesCV.MoveCurrentTo(loadedImages.First());

                messageBoxService.ShowInformation(string.Format(
                    "Successfully retreived images from file\r\n{0}",
                    saveFileService.FileName));
            }
            else
            {
                messageBoxService.ShowError(string.Format(
                    "Couldn't load any images from file\r\n{0}",
                    saveFileService.FileName));
            }
        }
        catch (Exception ex)
        {
            messageBoxService.ShowError(
                string.Format("An error occurred opening file\r\n{0}", 
                              ex.Message));
        }
    }
}

注意Cinch 还提供 TestOpenFileService,您可以在 Cinch V1 文章中阅读更多相关内容。

AddImageRatingPopup / ImageRatingViewModel

AddImageRatingPopup 只是用于为 ImageLoaderViewModel 中选定的 ImageViewModel 添加 1-5 之间的评级。因此,ImageLoaderViewModel 打开 AddImageRatingPopup 并向其推送一个新创建的 ImageRatingViewModel。以下是单击“添加评级”按钮时运行的 SimpleCommand Execute 代码。

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

可以看出,创建了一个新的 ImageRatingViewModel 实例,并且其中表示当前评级的 ImageRatingViewModel ImageRating DataWrapper<T> 设置为与 ImageLoaderViewModel 中选定的 ImageViewModel 相关联的当前评级。

之后,使用 IUIVisualizerService 显示 AddImageRatingPopup 弹出窗口,其中 IUIVisualizerService 将创建弹出窗口并将其 DataContext 设置为新实例化的 ImageRatingViewModel

所以我们现在创建了一个 AddImageRatingPopup 弹出窗口,它使用 ImageRatingViewModel 作为其 DataContext,但是 ImageRatingViewModel 有什么作用呢?嗯,让我们看看它的代码。这里是它的全部内容

/// <summary>
/// A simple ViewModel that is the ViewModel for the 
/// <c>AddImageRatingPopup.xaml</c> popup window.
/// 
/// This example shows you you to show popups from
/// we can use a Validating ViewModel, and also how
/// to use the control focus from the ViewModel using the
/// <c>TextBoxFocusBehavior</c>. 
/// It also shows how to the <c>NumericTextBoxBehaviour</c> for
/// the Rating TextBox.
/// </summary>
public class ImageRatingViewModel : ValidatingViewModelBase
{
    #region Data
    private DataWrapper<Int32> imageRating;
    private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;
    private static SimpleRule imageRatingRule;
    private IMessageBoxService messageBoxService;
    #endregion

    #region Ctor

    public ImageRatingViewModel(IMessageBoxService messageBoxService)
    {
        //setup services
        this.messageBoxService = messageBoxService;
            
        //Commands
        SaveImageRatingCommand = 
          new SimpleCommand<Object, Object>(ExecuteSaveImageRatingCommand);

        #region Create DataWrappers

        ImageRating = new DataWrapper<Int32>(this, imageRatingChangeArgs);
        ImageRating.IsEditable = true;

        //fetch list of all DataWrappers, so they can be used again later without the
        //need for reflection
        cachedListOfDataWrappers =
          DataWrapperHelper.GetWrapperProperties<ImageRatingViewModel>(this);
        #endregion

        #region Create Validation Rules

        imageRating.AddRule(imageRatingRule);
        #endregion
    }

    static ImageRatingViewModel()
    {
        imageRatingRule = new SimpleRule("DataValue", 
                     "ImageRating must be between 1-5",
                    (Object domainObject)=>
                    {
                        DataWrapper<Int32> obj = 
                           (DataWrapper<Int32>)domainObject;
                        return obj.DataValue < 0 || obj.DataValue > 5;
                    });
    }
    #endregion

    #region Public Properties

    //commands
    public SimpleCommand<Object, Object> SaveImageRatingCommand { get; private set; }

    /// <summary>
    /// CustomerId
    /// </summary>
    static PropertyChangedEventArgs imageRatingChangeArgs =
        ObservableHelper.CreateArgs<ImageRatingViewModel>(x => x.ImageRating);

    public DataWrapper<Int32> ImageRating
    {
        get { return imageRating; }
        private set
        {
            imageRating = value;
            NotifyPropertyChanged(imageRatingChangeArgs);
        }
    }
    #endregion

    #region Private Methods

    private void ExecuteSaveImageRatingCommand(Object args)
    {
            
        if (IsValid)
        {
            CloseActivePopUpCommand.Execute(true);
        }
        else
        {
            NotifyPropertyChanged(isValidChangeArgs);
            RaiseFocusEvent("ImageRating");
            messageBoxService.ShowError(
              "The Rating entered is invalid it must be between 1-5");
        }
    }

    #endregion

    #region Overrides
    /// <summary>
    /// Is the ViewModel Valid
    /// </summary>

    static PropertyChangedEventArgs isValidChangeArgs =
        ObservableHelper.CreateArgs<ImageRatingViewModel>(x => x.IsValid);

    public override bool IsValid
    {
        get
        {
            //return base.IsValid and use DataWrapperHelper, if you are
            //using DataWrappers
            return base.IsValid &&
                DataWrapperHelper.AllValid(cachedListOfDataWrappers);
        }
    }
    #endregion
}

此代码中有几点需要注意

  1. 它继承自 ValidatingViewModelBase,因此需要提供验证规则。
  2. 它使用 DataWrapper<T> 作为其图像评级数据。
  3. 它可以使用 SetFocus 事件将焦点设置到特定的 TextBox,我们在此前的文章 CinchV2_3.aspx 中讨论过。
  4. 当弹出窗口被认为是有效时,它将使用 ViewModelBase.CloseActivePopupCommand 关闭自身,这将把控制权返回给 ImageLoaderViewModel,后者以模态方式显示了弹出窗口,现在可以使用从 ImageLoaderViewModel 传递给 AddImageRatingPopupImageRatingViewModel 中可能已修改的值。

大部分内容可以直接在上面的 ImageRatingViewModel 代码中看到,唯一看不到的是 TextBox 验证以及它如何使用焦点行为。

这是相关的 XAML

<TextBox Text="{Binding ImageRating.DataValue, UpdateSourceTrigger=LostFocus, 
            ValidatesOnDataErrors=True, ValidatesOnExceptions=True}"
            Style="{StaticResource ValidatingTextBox}"
                IsEnabled="{Binding ImageRating.IsEditable}">
    <i:Interaction.Behaviors>
        <CinchV2:TextBoxFocusBehavior IsUsingDataWrappers="true" />
        <CinchV2:NumericTextBoxBehaviour/>
    </i:Interaction.Behaviors>
</TextBox>

其中名为 ValidatingTextBoxStyle 看起来像这样

<Style x:Key="ValidatingTextBox" TargetType="{x:Type TextBoxBase}">
    <Setter Property="SnapsToDevicePixels" Value="True"/>
    <Setter Property="OverridesDefaultStyle" Value="True"/>
    <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
    <Setter Property="MinWidth" Value="120"/>
    <Setter Property="MinHeight" Value="20"/>
    <Setter Property="AllowDrop" Value="true"/>
    <Setter Property="Validation.ErrorTemplate" Value="{x:Null}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBoxBase}">
                <Border 
                        Name="Border"
                        CornerRadius="5" 
                        Padding="2"
                        Background="White"
                        BorderBrush="Black"
                        BorderThickness="2" >
                    <ScrollViewer Margin="0" x:Name="PART_ContentHost"/>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter TargetName="Border" 
                          Property="Background" Value="LightGray"/>
                        <Setter TargetName="Border" 
                          Property="BorderBrush" Value="Black"/>
                        <Setter Property="Foreground" Value="Gray"/>
                    </Trigger>
                    <Trigger Property="Validation.HasError" Value="true">
                        <Setter TargetName="Border" Property="BorderBrush" 
                                Value="Red"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="true">
            <Setter Property="ToolTip"
                        Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                        Path=(Validation.Errors).CurrentItem.ErrorContent}"/>
        </Trigger>
    </Style.Triggers>
</Style>

这显然会导致在 ImageRatingViewModel 中定义的验证规则被违反时显示以下内容

AboutView / AboutViewModel

AboutView 相当简单,它只包含一个 FlowDocument,以及使用我在此处讨论过的 EventToCommandTrigger Blend 触发器的链接按钮:CinchV2_3.aspx#Interactivity

以下是 AboutView 的相关 XAML;和以前一样,请注意 MeffedMVVM ViewModelLocator.ViewModel 附加 DP 来解析 ViewModel

<UserControl x:Class="CinchV2DemoWPF.AboutView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.WPF"
             xmlns:meffed="http:\\www.codeplex.com\MEFedMVVM"
             xmlns:local="clr-namespace:CinchV2DemoWPF;assembly="
             xmlns:i="clr-namespace:System.Windows.Interactivity;
                      assembly=System.Windows.Interactivity"
             mc:Ignorable="d" 
             d:DesignHeight="371" d:DesignWidth="533"
             meffed:ViewModelLocator.ViewModel="AboutViewModel">

    <Grid>
         ......
         ......
         ......
         ......
        <Grid Grid.Column="1" Background="{StaticResource mainGridBrush}">
            <StackPanel Orientation="Vertical" 
                  VerticalAlignment="Top" Margin="30">
                <Label Style="{StaticResource aboutLabelStyle}"  
                         Content="Check Out Cinch:"/>
                <StackPanel Orientation="Vertical">
                    <TextBlock Style="{StaticResource aboutTextBlockStyleLinks}" 
                                   Text="Home Page [At Codeplex]">
                        <i:Interaction.Triggers>
                            <i:EventTrigger EventName="MouseLeftButtonDown">
                                <CinchV2:EventToCommandTrigger 
                                     Command="{Binding AboutViewEventToVMFiredCommand}" 
                                     CommandParameter="Home"/>
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                    </TextBlock>
                    <TextBlock Style="{StaticResource aboutTextBlockStyleLinks}" 
                                   Text="Source Code [At Codeplex]">
                        <i:Interaction.Triggers>
                            <i:EventTrigger EventName="MouseLeftButtonDown">
                                <CinchV2:EventToCommandTrigger 
                                     Command="{Binding AboutViewEventToVMFiredCommand}" 
                                     CommandParameter="Source"/>
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                    </TextBlock>
                </StackPanel>
            </StackPanel>
        </Grid>
    </Grid>
</UserControl>

除了使用 MeffedMVVM 进行 ViewModel 解析以及 2 个 EventToCommandTrigger 之外,并没有太多内容。所以为了完整性,让我们看看 AboutViewModel

[ExportViewModel("AboutViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class AboutViewModel : ViewModelBase
{
    public IUIVisualizerService uiVisualizer;

    [ImportingConstructor]
    public AboutViewModel(IUIVisualizerService uiVisualizer)
    {
        this.uiVisualizer = uiVisualizer;
        AboutViewEventToVMFiredCommand = 
          new SimpleCommand<Object, EventToCommandArgs>(
          ExecuteAboutViewEventToVMFiredCommand);
    }

    /// <summary>
    /// An event to command fired command, have a look at the AboutView, and look for
    /// where this command is used to see how the View can fire Commands in the ViewModel
    /// passing in Parameters 
    /// </summary>
    public SimpleCommand<Object, EventToCommandArgs> 
           AboutViewEventToVMFiredCommand { get; private set; }

    private void ExecuteAboutViewEventToVMFiredCommand(EventToCommandArgs args)
    {
        AboutViewLinkRequestedPopupViewModel aboutViewLinkRequestedPopupViewModel =
            new AboutViewLinkRequestedPopupViewModel();
        switch ((String)args.CommandParameter)
        {
            case "Home":
                aboutViewLinkRequestedPopupViewModel.NavigateTo = 
                    @"http://cinch.codeplex.com/";
                break;
            case "Source":
                aboutViewLinkRequestedPopupViewModel.NavigateTo =
                    @"http://cinch.codeplex.com/SourceControl/list/changesets";
                break;
        }
        uiVisualizer.ShowDialog("AboutViewLinkRequestedPopup", 
                                aboutViewLinkRequestedPopupViewModel);
    }
    #endregion
}

那么,这两个 XAML 声明的 EventToCommandTrigger Blend 触发器到底做了什么?嗯,它们都调用 AboutViewModelAboutViewEventToVMFiredCommand SimpleCommand,并传入不同的 CommandParameter 值。然后 AboutViewModel 使用通过 MeffedMVVM 注入的 IUIVisualizerService 来显示一个名为 AboutViewLinkRequestedPopup 的弹出窗口。您可以通过阅读文章后面的 弹出窗口 部分来了解弹出窗口的工作原理。

AboutViewLinkRequestedPopup / AboutViewLinkRequestedPopupViewModel

AboutViewLinkRequestedPopup 只是在一个嵌入式 WebBrowser 中导航到请求的网页。

正如我们刚刚看到的,AboutViewModel 负责显示一个名为 AboutViewLinkRequestedPopup 的弹出窗口,这是使用 IUIVisualizerService 完成的。现在,如果我们要检查弹出窗口的 XAML,我们将看不到其中有任何 MeffedMVVM 附加 DP 来解析 ViewModel,这与之前不同。

<Window x:Class="CinchV2DemoWPF.AboutViewLinkRequestedPopup"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="CinchV2 : WPF Demo app"  
        Icon="/CinchV2DemoWPF;component/Images/CinchIcon.png"
        Height="700" 
        Width="700"
        WindowStartupLocation="CenterOwner">
    <Grid>
        <WebBrowser x:Name="browser"  Margin="0"/>
    </Grid>
</Window>

主要原因是 Cinch 中的弹出窗口需要调用者将某些状态 (ViewModel) 推送到它们。弹出窗口操作推送到它的 ViewModel,然后很可能会关闭,但由于调用者是创建初始 ViewModel 以推送到弹出窗口的一方,因此调用者(父 ViewModel)拥有弹出窗口中在推送到弹出窗口的 ViewModel 中所做的所有更改。这就是您看不到任何 MeffedMVVM 附加 DP 的原因;基本上,弹出窗口 ViewModel 应该由其他 ViewModel 创建。

我通常的做法是将预期的服务从父 ViewModel 推送到弹出窗口的 ViewModel 中,然后使用 IUIVisualizerService 将新创建的 ViewModel 推送到弹出窗口中。这种方法确实意味着父 ViewModel 需要引用它打算推送到子 ViewModel 的服务,但嘿,我对此没意见。

实际上,有一种方法可以仍然使用 MeffedMVVM 附加 DP/属性来简单地让 MeffedMVVM 为您的预期服务注入属性设置器,但这有点高级,您可能不需要这样做。但是,如果您确实需要让 MeffedMVVM 注入属性设置器(例如用于服务),则此 Cinch 论坛帖子值得一读

https://codeproject.org.cn/Messages/3533572/Question-about-ViewModel-constructors-with-MEF.aspx

但是无论如何,我们现在暂时离题;让我们专注于正常用法,即父 ViewModel 创建一个新的弹出 ViewModel;我们在 AboutViewModel 的代码中看到了这一点,让我们快速回顾一下

private void ExecuteAboutViewEventToVMFiredCommand(EventToCommandArgs args)
{
    AboutViewLinkRequestedPopupViewModel aboutViewLinkRequestedPopupViewModel =
        new AboutViewLinkRequestedPopupViewModel();
    switch ((String)args.CommandParameter)
    {
        case "Home":
            aboutViewLinkRequestedPopupViewModel.NavigateTo = 
                @"http://cinch.codeplex.com/";
            break;
        case "Source":
            aboutViewLinkRequestedPopupViewModel.NavigateTo =
                @"http://cinch.codeplex.com/SourceControl/list/changesets";
                
            break;
    }
    uiVisualizer.ShowDialog("AboutViewLinkRequestedPopup", 
                            aboutViewLinkRequestedPopupViewModel);
}

看它是如何创建 AboutViewLinkRequestedPopupViewModel 并将其作为状态传递给 IUIVisualizerService 以用于新的弹出实例?让我们将注意力转向这个 AboutViewLinkRequestedPopupViewModel,它的全部内容如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Cinch;
using System.ComponentModel;
using System.ComponentModel.Composition;

namespace CinchV2DemoWPF
{
    ///NOTE : As this is a popup we should not be manually setting the Views 
    ///ViewModel to a pre populated ViewModel, by using the 
    ///<c>WPFUIVisualizerService</c> service, as we would typically pass the
    ///popup and object to alter the state with
    public class AboutViewLinkRequestedPopupViewModel : 
           ViewModelBase, IViewStatusAwareInjectionAware
    {
        #region Data
        private string navigateTo;
        #endregion

        #region Public Properties

        private IViewAwareStatus ViewAwareStatusService { get; set; }

        /// <summary>
        /// NavigateTo
        /// </summary>
        static PropertyChangedEventArgs navigateToArgs =
            ObservableHelper.CreateArgs<AboutViewLinkRequestedPopupViewModel>(
            x => x.NavigateTo);

        public string NavigateTo
        {
            get { return navigateTo; }
            set
            {
                navigateTo = value;
                NotifyPropertyChanged(navigateToArgs);
            }
        }
        #endregion

        #region IViewStatusAwareInjectionAware Members

        public void InitialiseViewAwareService(IViewAwareStatus viewAwareStatusService)
        {
            this.ViewAwareStatusService = viewAwareStatusService;
            this.ViewAwareStatusService.ViewLoaded += ViewAwareStatusService_ViewLoaded;
        }
        #endregion

        #region Private Methods

        private void ViewAwareStatusService_ViewLoaded()
        {
            //Get the View from the ViewAwareStatusService as a specific interface
            //and ask it to navigate its internal WebBrowser to the requested Url
            //Sometimes a tiny bit of code behind in the view is the correct thing
            //to do, we could abstract ourselves to insanity, but the thing is, if
            //it truly is a UI type operation and is not really something that requires
            //a lot of testing, I see nothing wrong with a tiny bit of code behind in the
            //view and that is what this is showing you
            IWebBrowserNavigatable webBrowserNavigatable = 
                this.ViewAwareStatusService.View as IWebBrowserNavigatable;
            if (webBrowserNavigatable != null)
            {
                ((IWebBrowserNavigatable)webBrowserNavigatable).NavigateTo(NavigateTo);
            }
        }
        #endregion
    }
}

实际上,这里面有一些微妙之处,其中第一个是 IViewStatusAwareInjectionAware 接口的使用。它的作用是,当 IUIVisualizerService 创建一个弹出窗口时,它会检查它传入的 ViewModel,并看到它想要了解 IViewStatusAware 服务(IViewStatusAwareInjectionAware 接口告诉它),如果它想要,则将一个新的 IViewStatusAware 注入到 Viewmodel 中。

这是 IUIVisualizerService 中处理此问题的相关代码

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;

    ......
    ......
    ......
}

AboutViewLinkRequestedPopupViewModel 代码中的另一个微妙之处在于,它在 IViewAwareStatus.Loaded 处理程序中做了一些奇怪的事情

private void ViewAwareStatusService_ViewLoaded()
{
    //Get the View from the ViewAwareStatusService as a specific interface
    //and ask it to navigate its internal WebBrowser to the requested Url
    //Sometimes a tiny bit of code behind in the view is the correct thing
    //to do, we could abstract ourselves to insanity, but the thing is, if
    //it truly is a UI type operation and is not really something that requires
    //a lot of testing, I see nothing wrong with a tiny bit of code behind in the
    //view and that is what this is showing you
    IWebBrowserNavigatable webBrowserNavigatable = 
        this.ViewAwareStatusService.View as IWebBrowserNavigatable;
    if (webBrowserNavigatable != null)
    {
        ((IWebBrowserNavigatable)webBrowserNavigatable).NavigateTo(NavigateTo);
    }
}

看到它如何从 IViewAwareStatus 实例获取 View,并期望它是 IWebBrowserNavigatable 吗?这怎么可能?

嗯,IViewAwareStatus 服务公开了 View(使用 WeakReference),因此您可以将 View 转换为 View 可能实现的任何接口并在您的 ViewModel 中使用它。在这种情况下,AboutViewLinkRequestedPopup View 实现了 IWebBrowserNavigatable 接口,如下所示。

[PopupNameToViewLookupKeyMetadata("AboutViewLinkRequestedPopup", 
        typeof(AboutViewLinkRequestedPopup))]
public partial class AboutViewLinkRequestedPopup : 
    Window, 
    IWebBrowserNavigatable
    //Show that sometimes code behind is the right thing to do
{

    public void NavigateTo(string url)
    {
        browser.Navigate(url);
    }
}

ViewModel 现在可以使用此 IWebBrowserNavigatable 接口与 View 进行通信。

我通常不会在我的视图上使用任何接口,但有时这样做是正确的,所以只需让你的 ViewModel 使用一个众所周知的契约(即接口)与视图进行通信。

暂时就到这里

这就是我现在想说的一切。在这个新系列中我还有一篇文章,然后就完成了。下一篇是关于 Silverlight 演示应用程序的,顺便说一句,那将是我的第 100 篇 CodeProject 文章,这非常了不起,所以如果能在那篇文章上获得一些投票/评论,那就太好了。

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

一如既往,如果您有任何与 MEF 相关的深入问题,您应该直接向 Marlon Grech 提问,可以通过他的博客 C# Disciples,或者使用 MefedMVVM CodePlex 网站;任何其他 Cinch V2 问题将在下一篇 Cinch V2 文章中回答。

© . All rights reserved.