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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (49投票s)

2010 年 8 月 8 日

CPOL

20分钟阅读

viewsIcon

289807

如果杰克丹尼尔斯制作 MVVM 框架。

目录

引言

上次我们讨论了 Cinch V2 WPF 演示应用程序。在本文中,我们将介绍 Cinch V2 Silverlight 演示应用程序,该应用程序随 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 站点链接到这两个应用程序。

总之,这不重要。正如我所说,我决定做一些不同的事情。那么,话不多说,Silverlight 演示应用程序有什么作用?

好吧,我认为这可以通过以下几点来概括

  • 有一个主 TabControl,允许您输入用户名来玩井字棋游戏(与计算机对战)
  • 还有一个井字棋控制
  • 有一个以前玩过的游戏列表,用户可以调出代表所玩游戏的 ChildWindow

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

它长什么样

既然我已经谈论了它的功能,那么我们来看看它长什么样,好吗?

当您启动应用程序时,它应该看起来像这样

MainWindow 正在等待您输入用户名来玩游戏

当您输入名称并按 OK 时,您可以使用翻转按钮(右上角)显示新的 GameViewGameStatView

GameView 允许您通过单击网格按钮来玩游戏

GameStatView 显示所有以前玩过的游戏列表,您可以使用提供的按钮选择查看。

如果单击 GameStatView 的其中一个按钮,将显示一个 PlayedGameChildWindow ChildWindow,它将显示以前的游戏状态。

整体结构

下图说明了 Silverlight 演示应用程序的视图/视图模型和弹出 ChildWindow 的整体结构。有许多帮助类和服务,但我将在我们遇到它们时讨论它们。目前,请注意 Silverlight 演示应用程序的整体结构,如下所示

它是如何工作的

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

子窗口

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

确保 ChildWindow 可用

有一个服务专门用于显示 ChildWindow,名为 IChildWindowService,它包含一个 Dictionary<string, Type>,这样 IChildWindowService 服务的消费者就可以简单地通过名称(字符串)从内部 Dictionary<string, Type> 请求 ChildWindow,然后 IChildWindowService 将在 Dictionary<string, Type> 中找到该条目并创建该 Type 的新实例并显示它。

为了清楚起见,以下是 SL 的完整 IChildWindowService 服务实现

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

namespace Cinch
{
    /// <summary>
    /// This class implements the IChildWindowService for SL purposes.
    /// If you have attributed up your views using the PopupNameToViewLookupKeyMetadata
    /// Registration of Views with the IChildWindowService service is automatic.
    /// However you can still register views manually, to do this simply put some lines like this in you App.Xaml.cs
    /// MefLocator.Container.GetExport<IChildWindowService>().Value.Register("ChildWindow1", typeof(ChildWindow1));
    /// </summary>
    /// </summary>
    /// <example>
    /// <![CDATA[
    /// 
    ///    //ui is an instance of the IChildWindowService which was MEF injected (say) into your ViewModel
    ///    bool? dialogResult = null;
    ///    ui.Show(popName, this, (s, e) =>
    ///    {
    ///        dialogResult = e.Result;
    ///        //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.
    /// ]]>
    /// </example>
    [PartCreationPolicy(CreationPolicy.Shared)]
    [ExportService(ServiceType.Both, typeof(IChildWindowService))]
    public class ChildWindowService : IChildWindowService
    {
        #region Data
        private readonly Dictionary<string, Type> _registeredWindows;
        #endregion

        #region Ctor
        public ChildWindowService()
        {
            _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(ChildWindow).IsAssignableFrom(winType))
                throw new ArgumentException("winType must be of type ChildWindow");

            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 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>
        public void Show(string key, object state, EventHandler<UICompletedEventArgs> completedProc)
        {
            ChildWindow win = CreateChildWindow(key, state, completedProc);
            if (win != null)
            {
                win.Show();
            }
        }
 
        #endregion

        #region Private Methods

        /// <summary>
        /// This creates the SL ChildWindow from a key.
        /// </summary>
        /// <param name="key">Key</param>
        /// <param name="dataContext">DataContext (state) object</param>
        /// <param name="completedProc">Callback used when UI closes (may be null)</param>
        /// <returns>ChildWindow</returns>
        private ChildWindow CreateChildWindow(string key, object dataContext, 
            EventHandler<UICompletedEventArgs> completedProc)
        {
            if (string.IsNullOrEmpty(key))
                throw new ArgumentNullException("key");

            Type winType;
            lock (_registeredWindows)
            {
                if (!_registeredWindows.TryGetValue(key, out winType))
                    return null;
            }

            var win = (ChildWindow)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;
            ViewModelBase bvm=null;

            EventHandler<CloseRequestEventArgs> handler = ((s, e) =>
                    {
                        try
                        {
                            win.DialogResult = e.Result;
                        }
                        catch (InvalidOperationException)
                        {
                            win.Close();
                        }
                    });

            if (dataContext != null)
            {
                bvm = dataContext as ViewModelBase;
                if (bvm != null)
                {
                    bvm.CloseRequest += handler;

                }
            }

            win.Closed += (s, e) =>
            {
                bvm.CloseRequest -= handler;

                if (completedProc != null)
                {
                    completedProc(this, new UICompletedEventArgs()
                    {
                        State = dataContext,
                        Result = win.DialogResult
                    });

                    GC.Collect();
                }
            };

            return win;
        }
        #endregion
    }
}

如需进一步阅读,请查看此链接 CinchV2_2.aspx#CoreServices 并阅读 IChildWindowService 部分。

因此,您可能想知道这个 IChildWindowServiceDictionary<string, Type> 是如何及时填充的,以确保当请求 ChildWindow 时,它存在于 Dictionary<string, Type> 中。这可以通过两种不同的方式发生。

手动向字典添加项目

您可以在适当的时间(例如在应用程序构建或甚至启动时)手动将弹出项添加到 IChildWindowService Dictionary<string, Type> 中。因此,您可能会有如下内容

public partial class App: Application
{
    public App()
    {
        ViewModelRepository.Instance.Resolver.Container.
            GetExport<IChildWindowService >().Value.Register("ChildWindow1", 
                typeof(ChildWindow1));
        InitializeComponent();
    }
}

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

自动查找属于 ChildWindow 的类型

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

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

因此,我们现在有一个带有属性的弹出窗口,但这只是一半的故事,我们需要确保有东西检查这些 PopupNameToViewLookupKeyMetadata 属性。这就是 CinchBootStrapper 的工作。基本上,CinchBootStrapper 接受一个 IEnumerable<Assembly> 来检查传入的 IEnumerable<Assembly> 中是否有任何具有 PopupNameToViewLookupKeyMetadata 属性的 Type,如果有,它们将被添加到 IChildWindowService 中以供以后使用。您所要做的就是确保调用 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
}

显示特定的 ChildWindow

一旦您的 IChildWindowService Dictionary<string, Type> 中有 ChildWindowKeyValuePair 条目,从 ViewModel 显示 ChildWindow 真是小菜一碟(请原谅这个双关语)。您只需这样做

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel;

using Cinch;
using MEFedMVVM.Common;
using MEFedMVVM.ViewModelLocator;

namespace CinchV2DemoSL
{
    public class GameStatViewModel : ViewModelBase
    {
        private ICommand viewGameCommand;
        private IChildWindowService ChildWindowService { get; set; }

        public GameStatViewModel(string winnerName, string gameText)
        {
            ViewGameCommand = new SimpleCommand<Object, Object>(ExecuteViewGameCommand);
        }

        public SimpleCommand<Object, Object> ViewGameCommand { get; private set; }

        private void ExecuteViewGameCommand(object o)
        {
            bool? dialogResult = null;
            ChildWindowService.Show("PlayedGameChildWindow", 
                new PlayedGameViewModel(GameText), (s, e) =>
            {
                dialogResult = e.Result;
                string result = dialogResult.HasValue && dialogResult.Value ? "ok" : "Cancel";
                MessageBoxService.ShowInformation("You clicked " + result);
                //you can do what you like with dialogResult
            });
        }
    }
}

重要提示:Silverlight 中的 ChildWindow 与 WPF 中的模态对话框不同,它们看起来是模态的,行为也像模态的,但显示 ChildWindow 后的任何代码都将继续运行。因此,当我们显示 ChildWindow 时,我们需要确保不再运行任何代码。您可能会问,我们如何处理 ChildWindow 可能已操作的修改状态?很简单,我们使用回调,如上面的代码片段所示。我们可以在那里简单地挂接一个 lambda,并在 ChildWindow 关闭时运行一些代码。

Cinch V2 还提供了 IChildWindowService 的测试替身,您可以用于测试。

因此,如果您想使用服务的测试版本,您只需从单元测试代码中注入测试版本(上面示例中的 TestChildWindowService),而不是实际版本。

应用程序管理

要使演示应用程序工作,实际上只有一件事是必需的,如下所示

应用程序构建

正如我在上面的 ChildWindows 部分提到的,Cinch V2 支持通过使用属性进行弹出查找和各种其他查找,其中在启动时找到具有这些属性的 Type。但为了使其正常工作,需要告诉 Cinch 要查看哪些程序集。对于演示应用程序,所有视图/弹出窗口都在与演示相同的 Assembly 中定义,因此我需要告诉 Cinch 在该 Assembly 中查找 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 Silverlight 演示应用程序中有许多视图模型。因此,我们将依次检查它们中的每一个,看看视图/视图模型如何协同工作。

UserEntryView / UserEntryViewModel

此视图是 Silverlight 演示应用程序启动时显示的第一个视图。其目的是简单的,从用户那里获取一个名称,该名称可用于存储用户使用 Silverlight 应用程序其余部分玩的井字棋游戏。

这个视图真的没有太多东西,我们先看看视图,好吗?这是 UserEntryView XAML 中的相关代码

<UserControl x:Class="CinchV2DemoSL.UserEntryView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400"
    Width="650" Height="370"
    xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.SL"
    xmlns:meffed="http:\\www.codeplex.com\MEFedMVVM"
    meffed:ViewModelLocator.ViewModel="UserEntryViewModel">

    <Border BorderBrush="#ff656565" BorderThickness="2" 
            HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="VisualStateGroup">
                <VisualStateGroup.Transitions>
                    <VisualTransition GeneratedDuration="0" To="InValidState"/>
                </VisualStateGroup.Transitions>
                <VisualState x:Name="InValidState">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames 
                              Storyboard.TargetProperty="(UIElement.Visibility)" 
                              Storyboard.TargetName="image">
                            <DiscreteObjectKeyFrame KeyTime="0">
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Visible</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="ValidState"/>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>

        <Grid x:Name="grid">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Grid Grid.Row="0" Background="{StaticResource ViewBackGrounds}">
                .......
                .......
                
                <TextBox Height="26" Margin="10,79,0,0" TextWrapping="Wrap" 
                    VerticalAlignment="Top" Width="300"
                    Text="{Binding UserName.DataValue, Mode=TwoWay, 
                        ValidatesOnDataErrors=True, 
                        ValidatesOnExceptions=True,
                        ValidatesOnNotifyDataErrors=True}" HorizontalAlignment="Left"/>

                <Image x:Name="image" Margin="0,-29,23,152" 
                   HorizontalAlignment="Right" Width="134" 
                   Source="/CinchV2DemoSL;component/Images/error.png" 
                   Visibility="Collapsed">

                    <Image.Effect>
                        <DropShadowEffect Color="#7F979797" 
                           ShadowDepth="12" Opacity="0.5"/>
                    </Image.Effect>
                </Image>
            </Grid>
               ......
               ......
               ......
                <Button Content="Ok" Height="25" Margin="5" 
                   Width="70" Foreground="Black"
                   Command="{Binding SaveUserNameCommand}" 
                   HorizontalAlignment="Right"/>
               ......
               ......
               ......

        </Grid>
    </Border>
</UserControl>

除了一个触发 SimpleCommandButton 和一个绑定到 UserEntryViewModel.UserName 属性的 TextBox 之外,没有太多内容,但也有一些 VisualState 用于在用户未输入有效用户名(基本上不能为空)时显示错误图标。

另一点要注意的是,标准视图模型解析是使用 MeffedMVVM 完成的。

现在,UserEntryViewModel 怎么样?我们简单回顾一下它的功能

  • 允许用户输入姓名。
  • 使用验证规则验证用户名。
  • 当用户单击“确定”按钮时,如果用户输入了有效名称,则控制视图模型被视为有效,用户名使用非核心 UI 服务保存到 IsolatedStorage 中的文件中,并且视图转换为“ValidState” VisualState
  • 但是,如果用户未输入用户名并单击“确定”按钮,则视图模型被视为无效,并且视图转换为“InvalidState” VisualState

我们来检查一下这些元素,好吗?

输入有效用户名/验证

这很容易实现,只需从 ValidatingViewModelBase 继承,并向 DataWrapper<T> 添加 Cinch SimpleRule 验证规则。

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel.Composition;
using System.Threading;
using System.Collections.Generic;
using System.ComponentModel;

using Cinch;
using MEFedMVVM.ViewModelLocator;

namespace CinchV2DemoSL
{
    /// <summary>
    /// Game Stat List ViewModel
    /// Demonstrates CinchV2 IDataErrorInfo support/DataWrappers/IVSM service
    /// MeffedMVVM service injection
    /// </summary>
    [ExportViewModel("UserEntryViewModel")]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class UserEntryViewModel : ValidatingViewModelBase
    {
        private DataWrapper<String> userName;
        private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;
        private static SimpleRule userNameRule;

        [ImportingConstructor]
        public UserEntryViewModel(
        .....
        .....
        .....)
        {
            UserName = new DataWrapper<String>(this, userNameChangeArgs);
            UserName.IsEditable = true;

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

            userName.AddRule(userNameRule);
        }

        static UserEntryViewModel()
        {
            userNameRule = new SimpleRule("DataValue", "UserName can not be empty",
                    (Object domainObject) =>
                    {
                        DataWrapper<String> obj = (DataWrapper<String>)domainObject;
                        return String.IsNullOrEmpty(obj.DataValue);
                    });
        }
   
        /// <summary>
        /// UserName
        /// </summary>
        static PropertyChangedEventArgs userNameChangeArgs =
            ObservableHelper.CreateArgs<UserEntryViewModel>(x => x.UserName);

        public DataWrapper<String> UserName
        {
            get { return userName; }
            private set
            {
                userName = value;
                NotifyPropertyChanged(userNameChangeArgs);
            }
        }

        /// <summary>
        /// Is the ViewModel Valid
        /// </summary>
        static PropertyChangedEventArgs isValidChangeArgs =
            ObservableHelper.CreateArgs<UserEntryViewModel>(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);
            }
        }
    }
}

我们在 XAML 中声明了以下内容

<TextBox Height="26" Margin="10,79,0,0" TextWrapping="Wrap" 
        VerticalAlignment="Top" Width="300"
        Text="{Binding UserName.DataValue, Mode=TwoWay, 
            ValidatesOnDataErrors=True, 
            ValidatesOnExceptions=True,
            ValidatesOnNotifyDataErrors=True}" HorizontalAlignment="Left"/>

检查用户条目是否有效

这非常简单。所有需要做的就是为“确定”按钮使用 SimpleCommand<T1,T2> 并检查 IsValid 属性,如上所示。如果用户输入了有效名称,则使用 IVSM 服务将视图转换为“ValidState” VisualState,并使用用户名保存 IsolatedStorage 文件。如果用户输入了无效数据(由 Cinch SimpleRule 验证规则定义),则只需使用 IVSM 服务将视图转换为“InvalidState” VisualState

UserEntryViewModel 的重要部分如下所示

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel.Composition;
using System.Threading;
using System.Collections.Generic;
using System.ComponentModel;

using Cinch;
using MEFedMVVM.ViewModelLocator;

namespace CinchV2DemoSL
{
    /// <summary>
    /// Game Stat List ViewModel
    /// Demonstrates CinchV2 IDataErrorInfo support/DataWrappers/IVSM service
    /// MeffedMVVM service injection
    /// </summary>
    [ExportViewModel("UserEntryViewModel")]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class UserEntryViewModel : ValidatingViewModelBase
    {
        private IMessageBoxService messageBoxService;
        private IViewAwareStatus viewAwareStatusService;
        private IChildWindowService childWidowService;
        private IGameStorerProvider gameStorerProvider;
        private IVSM visualStateManagerService;
    
        [ImportingConstructor]
        public UserEntryViewModel(
            IMessageBoxService messageBoxService,
            IViewAwareStatus viewAwareStatusService,
            IChildWindowService childWidowService,
            IGameStorerProvider gameStorerProvider,
            IVSM visualStateManagerService)
        {
            this.messageBoxService = messageBoxService;
            this.viewAwareStatusService = viewAwareStatusService;
            this.childWidowService = childWidowService;
            this.gameStorerProvider = gameStorerProvider;
            this.visualStateManagerService = visualStateManagerService;

            //Commands
            SaveUserNameCommand = new SimpleCommand<Object, Object>(ExecuteSaveUserNameCommand);
        }

        public SimpleCommand<Object, Object> SaveUserNameCommand { get; private set; }

        private void ExecuteSaveUserNameCommand(Object args)
        {
            if (IsValid)
            {
                visualStateManagerService.GoToState("ValidState");
                gameStorerProvider.WriteUserNameToFile(UserName.DataValue);
                messageBoxService.ShowError("Successfully saved username, flip to play a game!");
            }
            else
            {
                visualStateManagerService.GoToState("InValidState");
                messageBoxService.ShowError("The UserName entered is invalid it must contain a value");
            }
        }
    ......
    ......
    }
}

你们当中眼尖的人可能会想,等等,如果我正在对我的 ViewModel 进行单元测试,我将不在 Web 上下文中,甚至可能不希望我的 ViewModel 使用 IsolatedStorage 文件。没问题。这种将用户名保存到文件的操作是通过使用一个非核心服务 IGameStorerProvider 完成的,它看起来像这样

/// <summary>
/// Data service used to store/retrieve game data
/// </summary>
public interface IGameStorerProvider
{
    void StoreGameResults(string winnerName, String completeGameText);
    void WriteUserNameToFile(string username);
    string ReadUserNameFromFile();
    void FetchGameResults(Action<List<Tuple<string, string>>> callback);
}

我提供了一个运行时版本,它看起来像这样

/// <summary>
/// Runtime Data service used to store/retrieve game data
/// </summary>
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.Runtime, typeof(IGameStorerProvider))]
public class RunTimeGameStorerProvider : IGameStorerProvider
{

#region Data
    private BackgroundTaskManager<object,List<Tuple<string, string>>> bgWorker = 
        new BackgroundTaskManager<object, List<Tuple<string, string>>>();
#endregion

#region IGameStorerProvider Members

    public void StoreGameResults(string winnerName, string completeGameText)
    {
        IsolatedStorageHelper.StoreGameResults(winnerName, completeGameText);
    }

    public void WriteUserNameToFile(string username)
    {
        IsolatedStorageHelper.WriteUserNameToFile(username);
    }

    public string ReadUserNameFromFile()
    {
        return IsolatedStorageHelper.ReadUserNameFromFile();
    }

    public void FetchGameResults(Action<List<Tuple<string, string>>> callback)
    {
        bgWorker.TaskFunc = (argument) =>
        {
            return IsolatedStorageHelper.FetchGameResults();
        };

        bgWorker.CompletionAction = (result) =>
        {
            callback(result);
        };

        bgWorker.RunBackgroundTask();
}
#endregion

#region Public Properties

    /// <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<object, List<Tuple<string, string>>> BgWorker
    {
        get { return bgWorker; }
    }

#endregion
}

以及一个用于向 Blend 提供设计时数据的设计时版本,它看起来像这样

/// <summary>
/// Runtime Data service used to store/retrieve game data
/// </summary>
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.DesignTime, typeof(IGameStorerProvider))]
public class DesignTimeGameStorerProvider : IGameStorerProvider
{
    #region IGameStorerProvider Members

    public void StoreGameResults(string winnerName, string completeGameText)
    {
        //do nothing
    }

    public void WriteUserNameToFile(string username)
    {
        //do nothing
    }

    public string ReadUserNameFromFile()
    {
        return "Sacha";
    }

    public void FetchGameResults(Action<List<Tuple<string, string>>> callback)
    {
        List<Tuple<string, string>> results = 
                new List<Tuple<string, string>>();
        results.Add(new Tuple<string,string>("Sacha won","O:X:X:O:O:X:X:O:O:"));
        results.Add(new Tuple<string,string>("Computer won","O:O:O:X:X:X:X:O:O:"));
        results.Add(new Tuple<string,string>("Sacha won","O:X:X:O:O:X:X:O:O:"));
        callback(results);
    }

    #endregion
}

现在在您的单元测试中,您可以简单地传入 IGameStorerProvider 的 Mock 或测试替身,以执行您在受测 ViewModel 中想要执行的操作。

GameView / GameViewModel

在最基本的层面上,GameView 只是允许用户在井字棋游戏中与电脑对战。当游戏结束时,赢家(或平局)将被存储到 IsolatedStorage。用户可以使用“再玩一次” Image 选择再次玩游戏。找到赢家后,GameView 会显示一个新的 VisualState,它会对赢家文本进行一点动画,使用赢家的名字(可以是电脑,也可以是在第一个 UserNameEntryView 中输入的用户名)。

GameViewModel 中的很多代码都与确定谁是赢家以及管理游戏状态的逻辑有关,这与 Cinch 严格无关,所以我不会写关于这方面的内容,我只会关注 Cinch 框架中那些好用的部分。

让我们从“再玩一次” Image 开始。如果我们检查 XAML,我们可以看到它使用了 Cinch EventToCommandTrigger 来在 Image 上发生 MouseDown 事件时触发 Cinch SimpleCommand。

<Image Height="30" Width="30" Margin="5,2,5,2"
            Source="/CinchV2DemoSL;component/Images/repeat.png" 
            VerticalAlignment="Center">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseLeftButtonDown">
            <CinchV2:EventToCommandTrigger 
                Command="{Binding RestartCommand}" 
                CommandParameter="5"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Image>

以下是 GameViewModel 支持此 SimpleCommand 的相关部分

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")]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class GameViewModel : ViewModelBase
    {
        private IVSM visualStateManagerService;
        private IViewAwareStatus viewAwareStatusService;
        private IGameStorerProvider gameStorerProvider;

        [ImportingConstructor]
        public GameViewModel(            
            IVSM visualStateManagerService,
            IGameStorerProvider gameStorerProvider)
        {
            this.visualStateManagerService = visualStateManagerService;
            this.gameStorerProvider = gameStorerProvider;

            RestartCommand = new SimpleCommand<Object,Object>(ExecuteRestartCommand);
        }

        public SimpleCommand<Object, Object> RestartCommand { get; private set; }

        private void ExecuteRestartCommand(object o)
        {
            ....
             ....
            visualStateManagerService.GoToState("RestartedState");
        }
    }
}

可以看出,SimpleCommandExecute 委托也使用了 IVSM 服务,以便在执行 RestartCommand 时进入“RestartedState” VisualState。那么这个 ViewModel 还做了什么呢?

好吧,当游戏完成时,它需要被保存,我们可以在 GameViewModel 的此代码片段中看到

private void SetWinState(string winnerText)
{
    HaveWinner = true;
    WinnerName = winnerText + " Wins";
    DisableRemainingCells();
    StoreGameState();
    visualStateManagerService.GoToState("WinOrCompletedState");
}

private void StoreGameState()
{
    string actualWinnerName = WinnerName.StartsWith(GameCellViewModel.PlayersText) ?
           IsolatedStorageHelper.ReadUserNameFromFile() + " won" : "Computer won";
    string winner = HaveWinner ? actualWinnerName : "no winner";

    string gameText = "";
    foreach (GameCellViewModel cell in gameCells)
    {
        gameText += cell.CellText + ":";
    }
    gameText.TrimEnd(":".ToCharArray());

    gameStorerProvider.StoreGameResults(actualWinnerName, gameText);

    Mediator.Instance.NotifyColleagues<bool>("RefreshGameStats", true);
}

其中有几点值得注意。首先,在 StoreGameState() 方法中,我们正在使用我们之前讨论的 IGameStorerProvider 非核心服务,将结果存储到 IsolatedStorageFile 中。我们还在使用 IVSM 服务将 GameView 推入“WinOrCompletedState” VisualState,当游戏被认为是完成时。

还有一点值得注意,那就是在运行时,GameViewListGameStatView 都位于一个 TabControl 中,因此每次在 GameViewModel 中完成新游戏时,都会向 ListGameStatViewModel 发送一条 Mediator 消息,以便使用运行时 IGameStorerProvider 更新其赢家数据,这通过 ListGameStatView 上的绑定显示。

ListGameStatView / ListGameStatViewModel

ListGameStatView 实际上只是显示一个 List<GameStateViewModel>,其中包含谁赢得了特定的井字棋游戏。关于这个 ViewModel 没有太多可说的,除了它使用 IGameStorerProvider 来获取数据以创建 List<GameStateViewModel>。以下是完整的 ListGameStateViewModel。请原谅其中提到图像的方法是从 WPF 演示中复制的,我忘记重命名它,但意图应该仍然清晰。

using System;
using System.Collections.Generic;
using System.Windows.Input;
using System.ComponentModel.Composition;
using System.ComponentModel;
using System.Linq;
using System.Collections.ObjectModel;

using Cinch;
using MEFedMVVM.ViewModelLocator;

namespace CinchV2DemoSL
{
    /// <summary>
    /// Game Stat List ViewModel
    /// Demonstrates CinchV2 CIViewAwareStatus service/Mediator
    /// MeffedMVVM service injection, along with a utility
    /// service that is used at both design/runtime
    /// </summary>
    [ExportViewModel("ListGameStatViewModel")]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class ListGameStatViewModel : ViewModelBase
    {
        #region Data
        private ObservableCollection<GameStatViewModel> gameStats=
            new ObservableCollection<GameStatViewModel>();
        private IGameStorerProvider gameStorerProvider;
        private IViewAwareStatus viewAwareStatusService;
        #endregion

        #region Ctor
        [ImportingConstructor]
        public ListGameStatViewModel(
            IGameStorerProvider gameStorerProvider,
            IViewAwareStatus viewAwareStatusService)
        {
            this.gameStorerProvider = gameStorerProvider;
            this.viewAwareStatusService = viewAwareStatusService;
            this.viewAwareStatusService.ViewLoaded += viewAwareStatusService_ViewLoaded;
            Mediator.Instance.Register(this);
        }
        #endregion

        #region Mediator Message Sinks
        [MediatorMessageSinkAttribute("RefreshGameStats")]
        public void RefreshGameStatsMessageSink(bool dummy)
        {
            FetchData();
        }
        #endregion

        #region Public Methods/Properties
        /// <summary>
        /// GameStats
        /// </summary>
        static PropertyChangedEventArgs gameStatsChangeArgs =
            ObservableHelper.CreateArgs<ListGameStatViewModel>(x => x.GameStats);

        public ObservableCollection<GameStatViewModel> GameStats
        {
            get { return gameStats; }
            set
            {
                gameStats = value;
                NotifyPropertyChanged(gameStatsChangeArgs);
            }
        }
        #endregion

        #region Private Methods
        private void viewAwareStatusService_ViewLoaded()
        {
            FetchData();
        }

        private void FetchData()
        {
            gameStorerProvider.FetchGameResults(LoadImagesFromRetrievedData);
        }


        private void LoadImagesFromRetrievedData(List<Tuple<string, string>> data)
        {
            ObservableCollection<GameStatViewModel> newStats = 
                 new ObservableCollection<GameStatViewModel>();
            foreach (Tuple<String, String> result in data)
            {
                newStats.Add(new GameStatViewModel(result.Item1, result.Item2));
            }
            GameStats = newStats;
        }

        #endregion

        #region Overrides
        protected override void OnDispose()
        {
            base.OnDispose();
            Mediator.Instance.Unregister(this);
        }
        #endregion

    }
}

需要注意的一点是,因为此 ViewModel 使用 IGameStorerProvider,所以可以提供设计时数据。我们快速看一下。回想一下,我有一个这样的设计时 IGameStorerProvider 服务实现

/// <summary>
/// Runtime Data service used to store/retrieve game data
/// </summary>
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.DesignTime, typeof(IGameStorerProvider))]
public class DesignTimeGameStorerProvider : IGameStorerProvider
{
    #region IGameStorerProvider Members

    public void StoreGameResults(string winnerName, string completeGameText)
    {
        //do nothing
    }

    public void WriteUserNameToFile(string username)
    {
        //do nothing
    }

    public string ReadUserNameFromFile()
    {
        return "Sacha";
    }

    public void FetchGameResults(Action<List<Tuple<string, string>>> callback)
    {
        List<Tuple<string, string>> results = 
                  new List<Tuple<string, string>>();
        results.Add(new Tuple<string,string>("Sacha won","O:X:X:O:O:X:X:O:O:"));
        results.Add(new Tuple<string,string>("Computer won","O:O:O:X:X:X:X:O:O:"));
        results.Add(new Tuple<string,string>("Sacha won","O:X:X:O:O:X:X:O:O:"));
        callback(results);
    }

    #endregion
}

这在 Blend 中产生了以下结果

其中使用了在 UserNameEntryView / UserNameEntryViewModel 中输入的用户名。

还有一点值得注意,那就是在运行时,GameViewListGameStatView 都位于一个 TabControl 中,因此每次在 GameViewModel 中完成新游戏时,都会向 ListGameStatViewModel 发送一条 Mediator 消息,以便使用运行时 IGameStorerProvider 更新其赢家数据,这通过 ListGameStatView 上的绑定显示。

ListGameStatViewModel 包含一个由单个 GameStatViewModel 组成的列表,每个 GameStatViewModel 都可以用来显示 PlayedGameChildWindow ChildWindow。关于 GameStatView/GameStatViewModel 没有太多可说的,视图实际上只包含一个 DataTemplate,其中包含一个按钮,该按钮触发一个 Cinch SimpleCommand<T1,T2> 来显示以前存储在 IsolatedStorage 中的游戏状态(使用 IGameStorerProvider 服务)。所以我们来看看用于显示 PlayedGameChildWindow ChildWindowSimpleCommand<T1,T2>,因为这是 GameStatView/GameStatViewModel 中唯一真正重要的功能;它就是这样

private void ExecuteViewGameCommand(object o)
{
    bool? dialogResult = null;
    ChildWindowService.Show("PlayedGameChildWindow", 
        new PlayedGameViewModel(GameText), (s, e) =>
    {
        dialogResult = e.Result;
        string result = dialogResult.HasValue && dialogResult.Value ? "ok" : "Cancel";
        MessageBoxService.ShowInformation("You clicked " + result);
        //you can do what you like with dialogResult
    });
}

重要的是要记住,Silverlight 中的 ChildWindow 与 WPF 中的模态对话框不同,它们看起来是模态的,并且有点像模态的,但显示 ChildWindow 之后的任何代码都将继续运行。因此,当我们显示 ChildWindow 时,我们需要确保不再运行任何代码。您可能会问,我们如何处理 ChildWindow 可能已操纵的修改状态?很简单,我们使用回调,如上面的代码片段所示。我们可以在那里简单地挂接一个 lambda,并在 ChildWindow 关闭时运行一些代码。

并且 PlayedGameChildWindow ChildWindow 代码隐藏看起来像这样

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

using Cinch;

namespace CinchV2DemoSL.ChildWindows
{
    [PopupNameToViewLookupKeyMetadata("PlayedGameChildWindow",
                                      typeof(PlayedGameChildWindow))]
    public partial class PlayedGameChildWindow : ChildWindow
    {
        public PlayedGameChildWindow()
        {
            InitializeComponent();
        }

        private void OKButton_Click(object sender, RoutedEventArgs e)
        {
            this.DialogResult = true;
        }

        private void CancelButton_Click(object sender, RoutedEventArgs e)
        {
            this.DialogResult = false;
        }
    }
}

注意:如果使用 WPF,我们可以在 XAML 中使用 IsDefault/IsCancel 属性,但由于 Silverlight 不支持这些属性,我们必须有一些代码隐藏处理程序来关闭 ChildWindow

正如我们刚刚看到的,GameStatViewModel 负责使用 IChildWindowService 显示 PlayedGameChildWindow ChildWindow。现在,如果我们要检查 PlayedGameChildWindow ChildWindow 的 XAML,我们将看不到任何 MeffedMVVM 附加 DP 来解析 ViewModel,这与以前不同。

<controls:ChildWindow x:Class="CinchV2DemoSL.ChildWindows.PlayedGameChildWindow"
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
           xmlns:controls="clr-namespace:System.Windows.Controls;
                           assembly=System.Windows.Controls"
           xmlns:local="clr-namespace:CinchV2DemoSL"                      
           Width="400" Height="400" 
           Title="Played Game" 
           Background="{StaticResource verticalTabHeaderBackground}">
    
    <Grid x:Name="LayoutRoot" Margin="2">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <ItemsControl  Grid.Row="0"
                    HorizontalAlignment="Center" 
                    VerticalAlignment="Stretch" 
                    ItemsSource="{Binding GameCells}"
                    Background="Transparent"
                    ItemTemplate="{StaticResource cellDataTemplate}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <local:UniformGrid Background="Transparent"
                            Columns="3" Rows="3" 
                            Width="300" Height="300"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>

        <Button x:Name="CancelButton" Content="Cancel" 
                Click="CancelButton_Click" 
                Width="75" Height="23" 
                HorizontalAlignment="Right" Margin="0,12,0,0" 
                Grid.Row="1" />
        <Button x:Name="OKButton" Content="OK" 
           Click="OKButton_Click" Width="75" 
           Height="23" HorizontalAlignment="Right" 
           Margin="0,12,79,0" Grid.Row="1" />
    </Grid>
</controls:ChildWindow>

主要原因是 Cinch 中的 ChildWindow 期望调用者将一些状态 (ViewModel) 推送到它们。弹出窗口操纵推送到它的 ViewModel,然后很可能会关闭,但由于调用者是最初创建 ViewModel 以推送到 ChildWindow 的那个人,因此调用者(父 ViewModel)在推送到弹出窗口的 ViewModel 中拥有所有在 ChildWindow 中所做的更改。

所以这就是为什么您看不到任何 MeffedMVVM 附加 DP,基本上,弹出 ViewModel 预计由其他 ViewModel 创建。

我通常的做法是将预期的服务从父 ViewModel 推送到 ChildWindow ViewModel 中,然后使用 IChildWindowService 将新创建的 ViewModel 推送到 ChildWindow 中。这种方法确实意味着父 ViewModel 需要引用它打算推送到子 ViewModel 的服务,但没关系,我对此很满意。

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

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

特别说明

这是我在 CodeProject 上的第 100 篇文章,所以我只是想问一下,如果您喜欢我到目前为止带给您的文章,那么一些好的投票将非常受欢迎。无论如何,这取决于您,但它们将非常受欢迎,这是纪念我的第 100 篇文章的好方法。

暂时就到这里

暂时就这些了,我现在可以回去处理我为自己标记的许多其他事情了。我可能会做一些增强(有三个人提出了要求),但它们是增强,不是错误,所以不重要,我可能会做,也可能不会做。

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

和以前一样,如果您有任何与 MEF 相关的深入问题,您应该直接向 Marlon Grech 提问,可以通过他的博客 C# Disciples,或者通过 MefedMVVM CodePlex 站点;任何其他 Cinch V2 问题都将在接下来的 Cinch V2 文章中得到解答。

© . All rights reserved.