0613-prism-docs

https://docs.microsoft.com/en-us/previous-versions/msp-n-p/ff648465(v%3dpandp.10)

0613-prism-docs_第1张图片

 

依赖注入

使用Prism Library构建的应用程序依赖于容器提供的依赖注入。该库提供了与Unity应用程序块(Unity)或托管扩展性框架(MEF)一起使用的程序集,它允许您使用其他依赖项注入容器。引导过程的一部分是配置此容器并使用容器注册类型。

Prism库包括UnityBootstrapperMefBootstrapper类,它们实现了在应用程序中使用Unity或MEF作为依赖注入容器所需的大部分功能。

创建Shell

在传统的Windows Presentation Foundation(WPF)应用程序中,启动主窗口的App.xaml文件中指定了启动统一资源标识符(URI)。

在使用Prism Library创建的应用程序中,引导程序负责创建shell或主窗口。这是因为shell依赖于需要在显示shell之前注册的服务(例如Region Manager)

关键决定

在您决定在应用程序中使用Prism库之后,还需要做出一些额外的决定:

  • 您需要决定是否使用MEF,Unity或其他容器作为依赖注入容器。这将确定您应该使用哪个提供的引导程序类,以及是否需要为另一个容器创建引导程序。
  • 您应该考虑应用程序中所需的特定于应用程序的服务。这些将需要在容器中注册。
  • 确定内置日志记录服务是否足以满足您的需求,或者是否需要创建其他日志记录服务。
  • 确定应用程序如何发现模块:通过显式代码声明,通过目录扫描,配置或XAML发现的模块上的代码属性。

核心情景

创建启动序列是构建Prism应用程序的重要部分。本节介绍如何创建引导程序并对其进行自定义以创建shell,配置依赖项注入容器,注册应用程序级服务以及如何加载和初始化模块。

为您的应用程序创建引导程序

如果您选择使用Unity或MEF作为依赖注入容器,则可以轻松地为您的应用程序创建一个简单的引导程序。您需要创建一个派生自MefBootstrapperUnityBootstrapper的新类。然后,* 实现* CreateShell方法。(可选)您可以覆盖InitializeShell方法以进行特定于shell的初始化。

实现CreateShell方法

CreateShell方法允许开发者指定一个Prism应用顶层窗口。shell通常是MainWindowMainPage。通过返回应用程序的shell类的实例来实现此方法。在Prism应用程序中,您可以创建shell对象,或者根据应用程序的要求从容器中解析它。

以下代码示例中显示了使用ServiceLocator解析shell对象的示例。

protected override DependencyObject CreateShell()
{
    return ServiceLocator.Current.GetInstance();
}

实现InitializeShell方法

创建shell后,可能需要运行初始化步骤以确保准备好显示shell。对于WPF应用程序,您将创建shell应用程序对象并将其设置为应用程序的主窗口,如此处所示(来自WPF的Modularity QuickStarts)。

protected override void InitializeShell()
{
    Application.Current.MainWindow = Shell;
    Application.Current.MainWindow.Show();
}

InitializeShell的基本实现什么都不做。不调用基类实现是安全的。

创建和配置模块目录

如果要构建模块应用程序,则需要创建和配置模块目录。Prism使用具体的IModuleCatalog实例来跟踪应用程序可用的模块,可能需要下载的模块以及模块所在的位置。

引导程序提供了一个受保护的ModuleCatalog属性来引用目录以及一个基实现虚拟的CreateModuleCatalog方法。基础实现返回一个新的ModuleCatalog ; 但是,可以重写此方法以提供不同的IModuleCatalog实例,如下面的代码来自模块化中的QuickStartBootstrapper和MEF for WPF QuickStart。

protected override IModuleCatalog CreateModuleCatalog()
{
    // When using MEF, the existing Prism ModuleCatalog is still
    // the place to configure modules via configuration files.
    returnnewConfigurationModuleCatalog()
}

UnityBootstrapperMefBootstrapper类中,Run方法调用CreateModuleCatalog方法,然后使用返回的值设置类的ModuleCatalog属性。如果重写此方法,则无需调用基类的实现,因为您将替换提供的功能。

创建和配置容器

容器在使用Prism Library创建的应用程序中起着关键作用。Prism Library和构建在它上面的应用程序都依赖于一个容器来注入所需的依赖项和服务。在容器配置阶段,注册了几个核心服务。除了这些核心服务之外,您还可以使用特定于应用程序的服务,这些服务提供与组合相关的其他功能。

核心服务

下表列出了Prism库中的核心非应用程序特定服务。

服务界面

描述

IModuleManager

定义将检索和初始化应用程序模块的服务的接口。

IModuleCatalog

包含有关应用程序中模块的元数据。Prism Library提供了几种不同的目录。

IModuleInitializer

初始化模块。

IRegionManager

注册和检索区域,这些区域是布局的可视容器。

IEventAggregator

在发布者和订阅者之间松散耦合的事件集合。

ILoggerFacade

日志记录机制的包装器,因此您可以选择自己的日志记录机制。Stock Trader参考实施(Stock Trader RI)通过EnterpriseLibraryLoggerAdapter类使用Enterprise Library Logging Application Block 作为如何使用您自己的记录器的示例。使用CreateLogger方法返回的值,通过引导程序的Run方法向容器注册日志记录服务。向容器注册另一个记录器将不起作用; 而是覆盖引导程序上的CreateLogger方法。

IServiceLocator

允许Prism库访问容器。如果要自定义或扩展库,这可能很有用。

特定应用服务

下表列出了Stock Trader RI中使用的特定于应用程序的服务。这可以作为一个示例来了解您的应用程序可能提供的服务类型。

股票交易者RI中的服务

描述

IMarketFeedService

提供实时(模拟)市场数据。该PositionSummaryViewModel更新它从这项服务中接收基于通知的位置屏幕。

IMarketHistoryService

提供用于显示所选基金趋势线的历史市场数据。

IAccountPositionService

提供投资组合中的资金清单。

IOrdersService

坚持提交买/卖订单。

INewsFeedService

提供所选基金的新闻项目列表。

IWatchListService

将新的监视项目添加到监视列表时处理。

Prism,UnityBootstrapperMefBootstrapper中有两个Bootstrapper派生的类。创建和配置不同的容器涉及以不同方式实现的类似概念。

在UnityBootstrapper中创建和配置容器

UnityBootstrapper类的CreateContainer方法简单地创建并返回的新实例UnityContainer。在大多数情况下,您无需更改此功能; 但是,该方法是虚拟的,从而允许这种灵活性。

创建容器后,可能需要为您的应用程序配置容器。UnityBootstrapper中ConfigureContainer实现默认注册了许多核心Prism服务

// UnityBootstrapper.cs
protected virtual void ConfigureContainer()
{
    ...
    if (useDefaultConfiguration)
 {
    RegisterTypeIfMissing(typeof(IServiceLocator), typeof(UnityServiceLocatorAdapter), true);
    RegisterTypeIfMissing(typeof(IModuleInitializer), typeof(ModuleInitializer), true);
    RegisterTypeIfMissing(typeof(IModuleManager), typeof(ModuleManager), true);
    RegisterTypeIfMissing(typeof(RegionAdapterMappings), typeof(RegionAdapterMappings), true);
    RegisterTypeIfMissing(typeof(IRegionManager), typeof(RegionManager), true);
    RegisterTypeIfMissing(typeof(IEventAggregator), typeof(EventAggregator), true);
    RegisterTypeIfMissing(typeof(IRegionViewRegistry), typeof(RegionViewRegistry), true);
    RegisterTypeIfMissing(typeof(IRegionBehaviorFactory), typeof(RegionBehaviorFactory), true);
    RegisterTypeIfMissing(typeof(IRegionNavigationJournalEntry), typeof(RegionNavigationJournalEntry), false);RegisterTypeIfMissing(typeof(IRegionNavigationJournal), typeof(RegionNavigationJournal), false);RegisterTypeIfMissing(typeof(IRegionNavigationService), typeof(RegionNavigationService), false);RegisterTypeIfMissing(typeof(IRegionNavigationContentLoader), typeof(UnityRegionNavigationContentLoader), true);

  }
}

引导程序的RegisterTypeIfMissing方法确定服务是否已经注册 - 它不会注册两次。这允许您通过配置覆盖默认注册。您也可以默认关闭注册任何服务; 为此,请使用重载的Bootstrapper.Run方法传入false。您还可以覆盖ConfigureContainer方法并禁用您不想使用的服务,例如事件聚合器。

为了扩展的默认行为ConfigureContainer,只是一个覆盖添加到您的应用程序的引导程序和可选调用基实现,如从下面的代码QuickStartBootstrapper从模块化的WPF(使用Unity)快速入门。此实现调用基类的实现,注册ModuleTracker类型的具体实施IModuleTracker,并注册callbackLogger作为的单一实例CallbackLogger与统一。

protected override void ConfigureContainer()
{
    base.ConfigureContainer();

    this.RegisterTypeIfMissing(typeof(IModuleTracker), typeof(ModuleTracker), true);
    this.Container.RegisterInstance(this.callbackLogger);
}

在MefBootstrapper中创建和配置容器

MefBootstrapper类的CreateContainer方法做几件事情。首先,它创建一个AssemblyCatalog和一个CatalogExportProvider。该CatalogExportProvider允许MefExtensions组件提供默认出口一批Prism类型,并且还可以让你覆盖默认类型注册。然后,CreateContainer使用CatalogExportProvider创建并返回CompositionContainer的新实例。在大多数情况下,您无需更改此功能; 但是,该方法是虚拟的,从而允许这种灵活性。

创建容器后,需要为您的应用程序配置容器。默认情况下,MefBootstrapper中ConfigureContainer实现注册了许多核心Prism服务,如以下代码示例所示。如果重写此方法,请仔细考虑是否应调用基类的实现来注册核心Prism服务,或者是否在实现中提供这些服务。

protected virtual void ConfigureContainer()
{
    this.RegisterBootstrapperProvidedTypes();
}

protected virtual void RegisterBootstrapperProvidedTypes()
{
    this.Container.ComposeExportedValue(this.Logger);
    this.Container.ComposeExportedValue(this.ModuleCatalog);
    this.Container.ComposeExportedValue(new MefServiceLocatorAdapter(this.Container));
    this.Container.ComposeExportedValue(this.AggregateCatalog);
}

MefBootstrapper中,Prism的核心服务作为单例添加到容器中,因此可以在整个应用程序中通过容器定位它们。

除了提供CreateContainerConfigureContainer方法之外,MefBootstrapper还提供了两种方法来创建和配置MEF使用的AggregateCatalog。该CreateAggregateCatalog方法简单地创建并返回一个AggregateCatalog对象。与MefBootstrapper中的其他方法一样,CreateAggregateCatalog是虚拟的,如果需要可以覆盖。

ConfigureAggregateCatalog方法允许您注册类型添加到AggregateCatalog势在必行。例如,QuickStartBootstrapper从MEF快速入门模块性明确增加了ModuleA和ModuleC到AggregateCatalog,如下图所示。

protected override void ConfigureAggregateCatalog()
{
    base.ConfigureAggregateCatalog();
    // Add this assembly to export ModuleTracker
    this.AggregateCatalog.Catalogs.Add(
                 new AssemblyCatalog(typeof(QuickStartBootstrapper).Assembly));
    // Module A is referenced in in the project and directly in code.
    this.AggregateCatalog.Catalogs.Add(
                 new AssemblyCatalog(typeof(ModuleA.ModuleA).Assembly));
    this.AggregateCatalog.Catalogs.Add(
                 new AssemblyCatalog(typeof(ModuleC.ModuleC).Assembly));

// Module B and Module D are copied to a directory as part of a post-build step.
    // These modules are not referenced in the project and are discovered by inspecting a directory.
    // Both projects have a post-build step to copy themselves into that directory.
    DirectoryCatalog catalog = new DirectoryCatalog("DirectoryModules");    this.AggregateCatalog.Catalogs.Add(catalog);
}

基于Prism库的应用程序是复合应用程序,可能包含许多松散耦合的类型和服务。他们需要进行交互以提供内容并根据用户操作接收通知。因为它们是松散耦合的,所以它们需要一种相互交互和通信的方式来提供所需的业务功能。为了将这些不同的部分组合在一起,基于Prism库的应用依赖于依赖注入容器。

依赖注入容器通过提供实例化类实例的工具并根据容器的配置管理其生命周期来减少对象之间的依赖关系。在对象创建期间,容器会将对象所需的所有依赖项注入其中。如果尚未创建这些依赖项,则容器首先创建并解析它们的依赖项。在某些情况下,容器本身被解析为依赖项。例如,当使用Unity应用程序块(Unity)作为容器时,模块会注入容器,因此可以使用该容器注册其视图和服务。

使用容器有几个好处:

  • 容器不需要组件来定位其依赖项或管理它们的生命周期。
  • 容器允许交换已实现的依赖项而不影响组件。
  • 容器通过允许模拟依赖项来促进可测试性。
  • 容器通过允许将新组件轻松添加到系统中来提高可维护性。

在基于Prism库的应用程序的上下文中,容器具有特定的优点:

  • 容器在加载时将模块依赖项注入模块。
  • 容器用于注册和解析视图模型和视图。
  • 容器可以创建视图模型并注入视图。
  • 容器注入组合服务,例如区域管理器和事件聚合器。
  • 容器用于注册特定于模块的服务,这些服务是具有模块特定功能的服务。

注意: Prism指南中的某些示例依赖Unity应用程序块(Unity)作为容器。其他代码示例(例如Modularity QuickStarts)使用Managed Extensibility Framework(MEF)。Prism库本身不是特定于容器的,您可以将其服务和模式与其他容器一起使用,例如Castle Windsor,StructureMap和Spring.NET。

关键决策:选择依赖注入容器

Prism Library为依赖注入容器提供了两个选项:Unity或MEF。Prism是可扩展的,从而允许使用其他容器而不需要一点工作。Unity和MEF都为依赖注入提供了相同的基本功能,即使它们的工作方式非常不同。两个容器提供的一些功能包括:

  • 它们都使用容器注册类型。
  • 他们都用容器注册实例。
  • 它们都强制创建已注册类型的实例。
  • 它们都将注册类型的实例注入到构造函数中。
  • 它们都将已注册类型的实例注入属性。
  • 它们都具有用于标记需要管理的类型和依赖项的声明性属性。
  • 它们都解决了对象图中的依赖关系。

Unity提供了MEF不具备的几种功能:

  • 它解决了没有注册的具体类型。
  • 它解决了开放的泛型。
  • 它使用拦截来捕获对象的调用并向目标对象添加其他功能。

MEF提供了Unity不具备的几种功能:

  • 它发现目录中的程序集。
  • 它使用XAP文件下载和程序集发现。
  • 它会在发现新类型时重新组合属性和集合。
  • 它会自动导出派生类型。
  • 它与.NET Framework一起部署。

容器具有不同的功能和不同的工作方式,但Prism库将与容器一起使用并提供类似的功能。在考虑使用哪个容器时,请记住前面的功能并确定哪种容量更适合您的方案。

使用容器的注意事项

在使用容器之前,您应该考虑以下事项:

  • 考虑使用容器注册和解析组件是否合适:
    • 考虑在您的方案中是否可以接受向容器注册和从中解析实例的性能影响。例如,如果需要创建10,000个多边形以在渲染方法的局部范围内绘制曲面,则通过容器解析所有这些多边形实例的成本可能会产生显着的性能成本,因为容器使用反射来创建每个实体。
    • 如果存在许多或深度依赖性,则创建成本会显着增加。
    • 如果组件没有任何依赖关系或者不是其他类型的依赖关系,那么将它放在容器中可能没有意义。
    • 如果组件具有一组与该类型不可分割的依赖关系并且永远不会更改,则将其放入容器中可能没有意义。
  • 考虑组件的生命周期是否应该注册为单例或实例:
    • 如果组件是充当单个资源(例如日志记录服务)的资源管理器的全局服务,则可能需要将其注册为单例。
    • 如果组件为多个使用者提供共享状态,您可能希望将其注册为单例。
    • 如果正在注入的对象需要在每次依赖对象需要时注入一个新实例,请将其注册为非单例。例如,每个视图可能需要一个视图模型的新实例。
  • 考虑是否要通过代码或配置配置容器:
    • 如果要集中管理所有不同的服务,请通过配置配置容器。
    • 如果要有条件地注册特定服务,请通过代码配置容器。
    • 如果您有模块级服务,请考虑通过代码配置容器,以便仅在加载模块时注册这些服务。

 注意

某些容器(如MEF)无法通过配置文件进行配置,必须通过代码进行配置。

核心情景

容器用于两个主要目的,即注册和解析。

注册

在将依赖项注入对象之前,需要向容器注册依赖项的类型。注册类型通常涉及向容器传递接口和实现该接口的具体类型。注册类型和对象主要有两种方法:通过代码或通过配置。具体方式因容器而异。

通常,有两种方法可以通过代码在容器中注册类型和对象:

  • 您可以使用容器注册类型或映射。在适当的时候,容器将构建您指定的类型的实例。
  • 您可以将容器中的现有对象实例注册为单例。容器将返回对现有对象的引用。

使用Unity容器注册类型

在初始化期间,类型可以注册其他类型,例如视图和服务。注册允许通过容器提供其依赖项,并允许从其他类型访问它们。要做到这一点,类型将需要将容器注入模块构造函数。以下代码显示了命令QuickStart中的OrderModule类型如何注册类型。

C#复制

// OrderModule.cs
public class OrderModule : IModule
{
    public void Initialize()
    {
        this.container.RegisterType(new ContainerControlledLifetimeManager());
        ...
    }
    ...
}

根据您使用的容器,也可以通过配置在代码外部执行注册。有关此示例,请参阅在模块化应用程序开发中使用配置文件注册模块。

 注意

与配置相比,在代码中注册的优点是仅在模块加载时才进行注册。

使用MEF注册类型

MEF使用基于属性的系统来向容器注册类型。因此,向容器添加类型注册很简单:它需要在类型中添加[Export]属性,如下面的代码示例所示。

C#复制

[Export(typeof(ILoggerFacade))]
public class CallbackLogger: ILoggerFacade
{
}

使用MEF时的另一个选择是创建类的实例并使用容器注册该特定实例。带有MEF QuickStart的Modularity中的QuickStartBootstrapperConfigureContainer方法中显示了一个示例,如下所示。

C#复制

protected override void ConfigureContainer()
{
    base.ConfigureContainer();

    // Because we created the CallbackLogger and it needs to 
    // be used immediately, we compose it to satisfy any imports it has.
    this.Container.ComposeExportedValue(this.callbackLogger);
}

 注意

使用MEF作为容器时,建议您使用属性来注册类型。

解决

注册类型后,可以将其解析或注入为依赖项。在解析类型并且容器需要创建新实例时,它会将依赖项注入这些实例。

通常,在解析类型时,会发生以下三种情况之一:

  • 如果尚未注册该类型,则容器会引发异常。

     注意

    某些容器(包括Unity)允许您解析尚未注册的具体类型。

  • 如果类型已注册为单例,则容器将返回单例实例。如果这是第一次调用该类型,则容器会创建它并保留它以供将来调用。

  • 如果类型尚未注册为单例,则容器将返回新实例。

     注意

    默认情况下,使用MEF注册的类型是单例,容器包含对象的引用。在Unity中,默认情况下会返回新的对象实例,并且容器不会维护对该对象的引用。

使用Unity解析实例

命令快速入门中的以下代码示例显示了从容器中解析OrdersEditorViewOrdersToolBar视图的位置,以将它们与相应的区域相关联。

C#复制

// OrderModule.cs
public class OrderModule : IModule
{
    public void Initialize()
    {
        this.container.RegisterType(new ContainerControlledLifetimeManager());

        // Show the Orders Editor view in the shell's main region.
        this.regionManager.RegisterViewWithRegion("MainRegion",                            () => this.container.Resolve());

        // Show the Orders Toolbar view in the shell's toolbar region.
        this.regionManager.RegisterViewWithRegion("GlobalCommandsRegion",                            () => this.container.Resolve());
    }
    ...
}

OrdersEditorViewModel构造包含以下依赖(订单仓库和订单命令代理),当其解决注入。

C#复制

// OrdersEditorViewModel.cs
public OrdersEditorViewModel(IOrdersRepository ordersRepository, OrdersCommandProxy commandProxy)
{
    this.ordersRepository = ordersRepository;
    this.commandProxy     = commandProxy;

    // Create dummy order data.
    this.PopulateOrders();

    // Initialize a CollectionView for the underlying Orders collection.
    this.Orders = new ListCollectionView( _orders );
    // Track the current selection.
    this.Orders.CurrentChanged += SelectedOrderChanged;
    this.Orders.MoveCurrentTo(null);
}

除了前面代码中显示的构造函数注入之外,Unity还允许注入属性。应用[Dependency]属性的任何属性将在解析对象时自动解析并注入。

使用MEF解析实例

以下代码示例显示了使用MEF QuickStart的Modularity中的Bootstrapper如何获取shell的实例。代码可以请求接口的实例,而不是请求具体类型。

C#复制

protected override DependencyObject CreateShell()
{
    return this.Container.GetExportedValue();
}

在MEF解析的任何类中,您也可以使用构造函数注入,如下面的模块化与MEF QuickStart中的ModuleA中的代码示例所示,其中注入了ILoggerFacadeIModuleTracker

C#复制

[ImportingConstructor]
public ModuleA(ILoggerFacade logger, IModuleTracker moduleTracker)
{
    if (logger == null)
    {
        throw new ArgumentNullException("logger");
    }
    if (moduleTracker == null)
    {
        throw new ArgumentNullException("moduleTracker");
    }
    this.logger = logger;
    this.moduleTracker = moduleTracker;
    this.moduleTracker.RecordModuleConstructed(WellKnownModuleNames.ModuleA);
}

另一种选择是使用属性注入,如Modularity with MEF QuickStart 中的ModuleTracker类所示,其中注入了ILoggerFacade的实例。

C#复制

[Export(typeof(IModuleTracker))]
public class ModuleTracker : IModuleTracker
{
     [Import] private ILoggerFacade Logger;
}

在Prism中使用依赖注入容器和服务

依赖注入容器(通常称为“容器”)用于满足组件之间的依赖关系; 满足这些依赖性通常涉及注册和解决。Prism Library提供对Unity容器和MEF的支持,但它不是特定于容器的。因为库通过IServiceLocator接口访问容器,所以可以替换容器。为此,您的容器必须实现IServiceLocator接口。通常,如果要更换容器,则还需要提供自己的容器特定引导程序。该IServiceLocator接口在Common Service Locator Library中定义。这是一项开源工作,旨在提供IoC(控制反转)容器的抽象,例如依赖注入容器和服务定位器。使用此库的目的是利用IoC和服务位置,而不必与特定实现相关联。

Prism库提供UnityServiceLocatorAdapterMefServiceLocatorAdapter。两个适配器都通过扩展ServiceLocatorImplBase类型来实现ISeviceLocator接口。下图显示了类层次结构。

0613-prism-docs_第2张图片

Prism中的Common Service Locator实现

虽然Prism Library不引用或依赖于特定容器,但应用程序通常依赖于特定容器。这意味着特定应用程序引用容器是合理的,但Prism Library不直接引用容器。例如,Stock Trader RI和Prism附带的几个QuickStart依赖Unity作为容器。其他样品和快速入门依赖于MEF。

IServiceLocator

以下代码显示了IServiceLocator接口。

C#复制

public interface IServiceLocator : IServiceProvider
{
    object GetInstance(Type serviceType);
    object GetInstance(Type serviceType, string key);
    IEnumerable<object> GetAllInstances(Type serviceType);
    TService GetInstance();
    TService GetInstance(string key);
    IEnumerable GetAllInstances();
}

服务定位器在Prism库中扩展,扩展方法如下面的代码所示。您可以看到IServiceLocator仅用于解析,这意味着它用于获取实例; 它不用于注册。

C#复制

// ServiceLocatorExtensions
public static class ServiceLocatorExtensions
{
    public static object TryResolve(this IServiceLocator locator, Type type)
    {
        try
        {
            return locator.GetInstance(type);
        }
        catch (ActivationException)
        {
            return null;
        }
    }

    public static T TryResolve(this IServiceLocator locator) where T: class
    {
        return locator.TryResolve(typeof(T)) as T;
    }
}

Unity容器不支持的TryResolve扩展方法 - 如果已注册,则返回要解析的类型的实例; 否则,它返回null

所述ModuleInitializer使用IServiceLocator为加载模块期间解析模块,作为显示在下面的代码示例。

C#复制

// ModuleInitializer.cs - Initialize()
IModule moduleInstance = null;
try
{
    moduleInstance = this.CreateModule(moduleInfo);
    moduleInstance.Initialize();
}
...

C#复制

// ModuleInitializer.cs - CreateModule()
protected virtual IModule CreateModule(string typeName)
{
    Type moduleType = Type.GetType(typeName);
    if (moduleType == null)
    {
        throw new ModuleInitializeException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.FailedToGetType, typeName));
    }

    return (IModule)this.serviceLocator.GetInstance(moduleType);
}

使用IServiceLocator的注意事项

IServiceLocator并不是通用容器。容器具有不同的使用语义,这通常决定了为什么选择容器。考虑到这一点,Stock Trader RI直接使用依赖注入容器而不是使用IServiceLocator。这是您的应用程序开发的推荐方法。

在以下情况下,您可能适合使用IServiceLocator

  • 您是一家独立软件供应商(ISV),负责设计需要支持多个容器的第三方服务。
  • 您正在设计一个服务,以便在使用多个容器的组织中使用。

4、

模块化应用程序是一个应用程序,它被分成一组松散耦合的功能单元(命名模块),可以集成到更大的应用程序中。客户端模块封装了应用程序的整体功能的一部分,并且通常表示一组相关的问题。它可以包括一组相关组件,例如应用程序功能,包括用户界面和业务逻辑,或应用程序基础结构,例如用于记录或验证用户的应用程序级服务。模块彼此独立,但可以以松散耦合的方式彼此通信。使用模块化应用程序设计,您可以更轻松地开发,测试,部署和维护应用程序。

例如,考虑个人银行应用程序。用户可以访问各种功能,例如在账户之间转账,支付账单以及从单个用户界面(UI)更新个人信息。但是,在幕后,这些功能中的每一个都封装在一个离散模块中。这些模块相互通信,并与后端系统(如数据库服务器和Web服务)进行通信。应用服务集成了每个不同模块中的各种组件,并处理与用户的通信。用户看到的视图类似于单个应用程序的集成视图。

下图显示了具有多个模块的模块化应用程序的设计。

0613-prism-docs_第3张图片

模块组成

构建模块化应用程序的好处

您可能已经使用程序集,接口和类构建了一个架构良好的应用程序,并采用了良好的面向对象设计原则。即便如此,除非非常小心,否则您的应用程序设计可能仍然是“单一的”(所有功能都在应用程序内以紧密耦合的方式实现),这可能使应用程序难以开发,测试,扩展和维护。

另一方面,模块化应用程序方法可以帮助您识别应用程序的大规模功能区域,并允许您独立开发和测试该功能。这可以使开发和测试更容易,但它也可以使您的应用程序更灵活,更容易在未来扩展。模块化方法的好处是它可以使您的整体应用程序架构更加灵活和可维护,因为它允许您将应用程序分解为可管理的部分。每个部分都封装了特定的功能,每个部分都通过清晰但松散耦合的通信渠道进行集成。

Prism对模块化应用程序开发的支持

Prism为您的应用程序中的模块化应用程序开发和运行时模块管理提供支持。使用Prism的模块化开发功能可以节省您的时间,因为您不必实现和测试自己的模块化框架。Prism支持以下模块化应用程序开发功能:

  • 用于注册命名模块和每个模块位置的模块目录; 您可以通过以下方式创建模块目录:
    • 通过代码或可扩展应用程序标记语言(XAML)定义模块
    • 通过发现目录中的模块,您可以加载所有模块,而无需在集中目录中明确定义
    • 通过在配置文件中定义模块
  • 模块的声明性元数据属性,以支持初始化模式和依赖性
  • 与依赖注入容器集成以支持模块之间的松散耦合
  • 对于模块加载:
    • 依赖管理,包括重复和循环检测,以确保模块以正确的顺序加载,并且只加载和初始化一次
    • 模块的按需和后台下载,以最大限度地减少应用程序启动时间; 其余模块可以在后台加载和初始化,也可以在需要时加载和初始化

核心概念

本节介绍与Prism模块化相关的核心概念,包括IModule接口,模块加载过程,模块目录,模块之间的通信以及依赖注入容器。

IModule:模块化应用程序的构建块

模块是功能和资源的逻辑集合,以可以单独开发,测试,部署和集成到应用程序中的方式打包。包可以是一个或多个程序集。每个模块都有一个中心类,负责初始化模块并将其功能集成到应用程序中。该类实现了IModule接口。

注意:实现IModule接口的类的存在足以将包标识为模块。

IModule的接口只有一个方法,名为初始化,您可以在其中实现的任何逻辑需要初始化和模块的功能集成到应用程序。根据模块的用途,它可以将视图注册到组合用户界面,为应用程序提供其他服务,或扩展应用程序的功能。以下代码显示了模块的最低实现。

C#复制

public class MyModule : IModule
{
    public void Initialize()
    {
        // Do something here.
    }
}

 注意

Stock Trader RI使用声明的,基于属性的方法来注册视图,服务和类型,而不是使用IModule接口提供的初始化机制。

模块寿命

Prism中的模块加载过程包括以下内容:

  1. 注册/发现模块。在运行时为特定应用程序加载的模块在模块目录中定义。该目录包含有关要加载的模块,其位置以及加载顺序的信息。
  2. 加载模块。包含模块的程序集将加载到内存中。此阶段可能需要从某个远程位置或本地目录检索模块。
  3. 初始化模块。然后初始化模块。这意味着创建模块类的实例并通过IModule接口调用它们的Initialize方法。

下图显示了模块加载过程。

0613-prism-docs_第4张图片

模块加载过程

模块目录

所述ModuleCatalog保存关于能够由应用程序使用的模块的信息。目录本质上是ModuleInfo类的集合。ModuleInfo类中描述了每个模块,该类记录了模块的其他属性中的名称,类型和位置。使用ModuleInfo实例填充ModuleCatalog有几种典型方法:

  • 在代码中注册模块
  • 在XAML中注册模块
  • 在配置文件中注册模块
  • 在磁盘上的本地目录中发现模块

您应该使用的注册和发现机制取决于您的应用程序需要什么。使用配置文件或XAML文件允许您的应用程序不需要引用模块。使用目录可以允许应用程序发现模块,而无需在文件中指定它们。

控制何时加载模块

Prism应用程序可以尽快初始化模块,称为“可用时”,或者当应用程序需要它们时,称为“按需”。请考虑以下加载模块的准则:

  • 运行应用程序所需的模块必须与应用程序一起加载,并在应用程序运行时进行初始化。
  • 包含几乎总是在应用程序的典型使用中使用的功能的模块可以在后台加载并在可用时进行初始化。
  • 可以按需加载和初始化包含很少使用的功能(或其他模块可选择依赖的支持模块)的模块。

考虑如何对应用程序进行分区,常见使用方案,应用程序启动时间以及下载的数量和大小,以确定如何配置模块以进行下载和初始化。

将模块与应用程序集成

Prism提供以下类来引导您的应用程序:UnityBootstrapperMefBootstrapper。这些****类可用于创建和配置模块管理器以发现和加载模块。您可以覆盖配置方法,以在几行代码中注册XAML文件,配置文件或目录位置中指定的模块。

使用模块Initialize方法将模块与应用程序的其余部分集成。执行此操作的方式因应用程序的结构和模块的内容而异。以下是将模块集成到应用程序中的常见操作:

  • 将模块的视图添加到应用程序的导航结构中。在使用视图发现或视图注入构建复合UI应用程序时,这很常见。
  • 订阅应用程序级别的事件或服务。
  • 使用应用程序的依赖注入容器注册共享服务。

在模块之间进行通信

即使模块之间的耦合度较低,模块也可以相互通信。有几种松散耦合的通信模式,每种都有自己的优势。通常,这些模式的组合用于创建所得到的解决方案。以下是其中一些模式:

  • 松散耦合的事件。模块可以广播已发生的特定事件。其他模块可以订阅这些事件,以便在事件发生时通知他们。松耦合事件是在两个模块之间建立通信的轻量级方式; 因此,它们很容易实现。但是,过于依赖事件的设计可能变得难以维护,尤其是如果必须协调许多事件以完成单个任务。在这种情况下,考虑共享服务可能更好。
  • 共享服务。共享服务是可以通过公共接口访问的类。通常,共享服务位于共享程序集中,并提供系统范围的服务,例如身份验证,日志记录或配置。
  • 共享资源。如果您不希望模块直接相互通信,您还可以通过共享资源(如数据库或一组Web服务)间接进行通信。

依赖注入和模块化应用程序

Unity应用程序块(Unity)和托管可扩展性框架(MEF)等容器允许您轻松使用控制反转(IoC)和依赖注入,它们是强大的设计模式,有助于以松散耦合的方式组合组件。它允许组件获得对它们所依赖的其他组件的引用,而无需对这些引用进行硬编码,从而促进更好的代码重用和更高的灵活性。在构建松散耦合的模块化应用程序时,依赖注入非常有用。Prism旨在与用于组成应用程序中的组件的依赖注入容器无关。容器的选择取决于您,并且在很大程度上取决于您的应用要求和偏好。然而,

模式和实践Unity Application Block提供了一个功能齐全的依赖注入容器。它支持基于属性和基于构造函数的注入和策略注入,允许您透明地在组件之间注入行为和策略; 它还支持许多其他典型的依赖注入容器功能。

MEF(它是.NET Framework 4.5的一部分)通过支持基于依赖注入的组件组合提供对构建可扩展.NET应用程序的支持,并提供支持模块化应用程序开发的其他功能。它允许应用程序在运行时发现组件,然后以松散耦合的方式将这些组件集成到应用程序中。MEF是一个很好的可扩展性和组合框架。它包括程序集和类型发现,类型依赖性解析,依赖注入以及一些不错的程序集下载功能。Prism支持利用MEF功能,以及以下内容:

  • 通过XAML和代码属性进行模块注册
  • 通过配置文件和目录扫描进行模块注册
  • 加载模块时的状态跟踪
  • 使用MEF时模块的自定义声明性元数据

Unity和MEF依赖注入容器都可以与Prism无缝协作。

关键决定

您要做的第一个决定是您是否要开发模块化解决方案。如上一节所述,构建模块化应用程序有许多好处,但是您需要花费时间和精力来获得这些好处。如果您决定开发模块化解决方案,还有几个需要考虑的事项:

  • 确定您将使用的框架。您可以创建自己的模块化框架,使用Prism,MEF或其他框架。
  • 确定如何组织解决方案。通过定义每个模块的边界来处理模块化体系结构,包括哪些组件是每个模块的一部分。您可以决定使用模块化来简化开发,以及控制应用程序的部署方式或是否支持插件或可扩展体系结构。
  • 确定如何对模块进行分区。可以根据需求对模块进行不同的分区,例如,按功能区域,提供程序模块,开发团队和部署要求进行分区。
  • 确定应用程序将为所有模块提供的核心服务例如,核心服务可以是错误报告服务或身份验证和授权服务。
  • 如果您使用的是Prism,请确定在模块目录中注册模块时使用的方法。对于WPF,您可以在代码,XAML,配置文件中注册模块,或在磁盘上的本地目录中发现模块。确定您的模块通信和依赖策略。模块需要相互通信,您需要处理模块之间的依赖关系。
  • 确定您的依赖注入容器。通常,模块化系统需要依赖注入,控制反转或服务定位器,以允许松散耦合和动态加载和创建模块。Prism允许在使用Unity,MEF或其他容器之间进行选择,并为Unity或基于MEF的应用程序提供库。
  • 最小化应用程序启动时间。考虑模块的按需和后台下载,以最大限度地减少应用程序启动时间。
  • 确定部署要求。您需要考虑如何部署应用程序。

下一节提供了有关这些决策的详细信息。

将您的应用程序划分为模块

当您以模块化方式开发应用程序时,可以将应用程序组织到单独的客户端模块中,这些模块可以单独开发,测试和部署。每个模块都将封装应用程序的一部分整体功能。您必须做出的首要设计决策之一是决定如何将应用程序的功能划分为离散模块。

模块应该封装一组相关的问题,并具有一组独特的职责。模块可以表示应用程序的垂直切片或水平服务层。大型应用程序可能有两种类型的模块。

0613-prism-docs_第5张图片

围绕垂直切片组织模块的应用程序

0613-prism-docs_第6张图片

围绕水平层组织模块的应用程序

较大的应用程序可能具有使用垂直切片和水平层组织的模块。模块的一些示例包括以下内容:

  • 包含特定应用程序功能的模块,例如Stock Trader参考实现中的新闻模块(Stock Trader RI)
  • 包含特定子系统或功能的模块,用于一组相关用例,例如采购,发票或总帐
  • 包含基础结构服务的模块,例如日志记录,缓存和授权服务,或Web服务
  • 除了其他内部系统之外,包含调用业务线(LOB)系统(如Siebel CRM和SAP)的服务的模块

模块应该对其他模块具有最小的依赖关系。当模块依赖于另一个模块时,它应该通过使用共享库中定义的接口而不是具体类型来松散耦合,或者通过使用EventAggregator通过EventAggregator事件类型与其他模块进行通信。

模块化的目标是以一种即使在添加和删除功能和技术时仍保持灵活性,可维护性和稳定性的方式对应用程序进行分区。实现此目的的最佳方法是设计应用程序,使模块尽可能独立,具有良好定义的接口,并尽可能隔离。

确定项目与模块的比率

有几种方法可以创建和打包模块。建议的和最常见的方法是为每个模块创建一个组件。这有助于保持逻辑模块分离并促进适当的封装。它还使得更容易将组件作为模块边界以及如何部署模块的包装进行讨论。但是,没有什么可以阻止单个程序集包含多个模块,在某些情况下,这可能是首选,以最大限度地减少解决方案中的项目数量。对于大型应用程序,拥有10-50个模块并不罕见。将每个模块分离到自己的项目中会增加解决方案的复杂性,并会降低Visual Studio的性能。

使用依赖注入来实现松散耦合

模块可以依赖于主机应用程序或其他模块提供的组件和服务。Prism支持在模块之间注册依赖关系的能力,以便以正确的顺序加载和初始化它们。Prism还支持在将模块加载到应用程序时初始化模块。在模块初始化期间,模块可以检索对其所需的附加组件和服务的引用,和/或注册它包含的任何组件和服务,以使其可供其他模块使用。

模块应使用独立机制来获取外部接口的实例,而不是直接实例化具体类型,例如通过使用依赖注入容器或工厂服务。诸如Unity或MEF之类的依赖注入容器允许类型通过依赖注入自动获取所需的接口和类型的实例。Prism与Unity和MEF集成,允许模块轻松使用依赖注入。

下图显示了加载模块时需要获取或注册组件和服务引用的典型操作顺序。

0613-prism-docs_第7张图片

依赖注入的示例

在此示例中,OrdersModule程序集定义了OrdersRepository类(以及实现顺序功能的其他视图和类)。所述CustomerModule组件限定CustomersViewModel类依赖于OrdersRepository,通常基于由服务暴露的接口上。应用程序启动和引导过程包含以下步骤:

  1. 引导程序启动模块初始化过程,模块加载程序加载并初始化OrdersModule

  2. OrdersModule的初始化中,它将OrdersRepository注册到容器中。

  3. 然后,模块加载器加载CustomersModule。模块加载的顺序可以由模块元数据中的依赖项指定。

  4. CustomersModule构建的一个实例CustomerViewModel通过容器以解决该问题。该CustomerViewModel对一个依赖OrdersRepository(通常基于它的接口上),并指示它通过构造或财产注射。容器根据OrdersModule注册的类型在视图模型的构造中注入该依赖。最终结果是从CustomerViewModelOrderRepository的接口引用,而没有这些类之间的紧密耦合。

     注意

    用于公开OrderRespositoryIOrderRepository)的接口可以驻留在单独的“共享服务”程序集或“订单服务”程序集中,该程序集仅包含公开这些服务所需的服务接口和类型。这样,CustomersModuleOrdersModule之间就没有硬依赖关系。

    请注意,两个模块都依赖于依赖注入容器。在模块构建器中的模块构造期间注入该依赖性。

核心情景

本节介绍在应用程序中使用模块时将遇到的常见方案。这些方案包括定义模块,注册和发现模块,加载模块,初始化模块,指定模块依赖关系,按需加载模块,在后台下载远程模块以及检测模块何时已加载。您可以在代码,XAML或应用程序配置文件中注册和发现模块,也可以通过扫描本地目录来注册和发现模块。

定义模块

模块是功能和资源的逻辑集合,以可以单独开发,测试,部署和集成到应用程序中的方式打包。每个模块都有一个中心类,负责初始化模块并将其功能集成到应用程序中。该类实现了IModule接口,如下所示。

C#复制

public class MyModule : IModule
{
    public void Initialize()
    {
        // Initialize module
    }
}

实现Initialize方法的方式取决于应用程序的要求。模块目录中定义了模块类类型,初始化模式和任何模块依赖性。对于目录中的每个模块,模块加载器创建模块类的实例,然后调用Initialize方法。模块按模块目录中指定的顺序处理。运行时初始化顺序基于模块下载,可用和满足依赖性的时间。

根据应用程序使用的模块目录的类型,可以通过模块类本身的声明性属性或模块目录文件中的模块依赖性来设置模块依赖性。以下部分提供了更多详细信息。

注册和发现模块

应用程序可以加载的模块在模块目录中定义。Prism Module Loader使用模块目录来确定哪些模块可以加载到应用程序中,何时加载它们以及它们的加载顺序。

模块目录由实现IModuleCatalog接口的类表示。模块目录类由应用程序引导程序类在应用程序初始化期间创建。Prism提供了不同的模块目录实现供您选择。您还可以通过调用AddModule方法或从ModuleCatalog派生来创建具有自定义行为的模块目录,从另一个数据源填充模块目录。

 注意

通常,Prism中的模块使用依赖注入容器和公共服务定位器来检索模块初始化所需的类型实例。Unity和MEF容器都由Prism支持。虽然注册,发现,下载和初始化模块的整个过程是相同的,但细节可以根据是使用Unity还是MEF而有所不同。本主题将解释方法之间特定于容器的差异。

在代码中注册模块

最基本的模块目录由ModuleCatalog类提供。您可以使用此模块目录通过指定模块类类型以编程方式注册模块。您还可以以编程方式指定模块名称和初始化模式。要直接使用ModuleCatalog类注册模块,请在应用程序的Bootstrapper类中调用AddModule方法。以下代码中显示了一个示例。

C#复制

protected override void ConfigureModuleCatalog()
{
    Type moduleCType = typeof(ModuleC);
    ModuleCatalog.AddModule(
      new ModuleInfo()
      {
          ModuleName = moduleCType.Name,
          ModuleType = moduleCType.AssemblyQualifiedName,
      });
}

在前面的示例中,模块由shell直接引用,因此模块类类型已定义,可用于对AddModule的调用。这就是为什么这个例子使用typeof(Module)将模块添加到目录的原因。

 注意

如果您的应用程序直接引用模块类型,您可以按类型添加它,如上所示; 否则,您需要提供完全限定的类型名称和程序集的位置。

要查看在代码中定义模块目录的另一个示例,请参阅Stock Trader参考实现(Stock Trader RI)中的StockTraderRIBootstrapper.cs。

 注意

所述引导程序基类提供的CreateModuleCatalog方法来帮助创建的ModuleCatalog。默认情况下,此方法创建ModuleCatalog实例,但可以在派生类中重写此方法,以便创建不同类型的模块目录。

使用XAML文件注册模块

您可以通过在XAML文件中指定模块目录来以声明方式定义模块目录。XAML文件指定要创建的模块目录类类型以及要添加到哪个模块。通常,.xaml文件作为资源添加到shell项目中。模块目录由引导程序创建,并调用CreateFromXaml方法。从技术角度来看,这种方法非常类似于在代码中定义ModuleCatalog,因为XAML文件只是定义了要实例化的对象的层次结构。

以下代码示例显示了指定模块目录的XAML文件。

XAML复制


<Modularity:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"                          xmlns:sys="clr-namespace:System;assembly=mscorlib"                          xmlns:Modularity="clr-namespace:Microsoft.Practices.Prism.Modularity;assembly=Microsoft.Practices.Prism">
<Modularity:ModuleInfoGroup Ref="file://DirectoryModules/ModularityWithMef.Desktop.ModuleB.dll" InitializationMode="WhenAvailable">
<Modularity:ModuleInfo ModuleName="ModuleB" ModuleType="ModularityWithMef.Desktop.ModuleB, ModularityWithMef.Desktop.ModuleB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
Modularity:ModuleInfoGroup>
<Modularity:ModuleInfoGroup InitializationMode="OnDemand">
<Modularity:ModuleInfo Ref="file://ModularityWithMef.Desktop.ModuleE.dll" ModuleName="ModuleE" ModuleType="ModularityWithMef.Desktop.ModuleE,ModularityWithMef.Desktop.ModuleE, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Modularity:ModuleInfo Ref="file://ModularityWithMef.Desktop.ModuleF.dll" ModuleName="ModuleF" ModuleType="ModularityWithMef.Desktop.ModuleF,ModularityWithMef.Desktop.ModuleF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<Modularity:ModuleInfo.DependsOn>
<sys:String>ModuleEsys:String>
Modularity:ModuleInfo.DependsOn>
Modularity:ModuleInfo>
Modularity:ModuleInfoGroup>

        <Modularity:ModuleInfo Ref="file://DirectoryModules/ModularityWithMef.Desktop.ModuleD.dll" ModuleName="ModuleD" ModuleType="ModularityWithMef.Desktop.ModuleD, ModularityWithMef.Desktop.ModuleD, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
Modularity:ModuleCatalog>

 注意

ModuleInfoGroups提供了一种方便的方法来对同一程序集中的模块进行分组,以相同的方式进行初始化,或者仅依赖于同一组中的模块。
模块之间的依赖关系可以在同一ModuleInfoGroup中的模块中定义; 但是,您无法在不同的ModuleInfoGroups中定义模块之间的依赖关系。
将模块放在模块组中是可选的。为组设置的属性将应用于其包含的所有模块。请注意,模块也可以在不在组内的情况下进行注册。

在应用程序的Bootstrapper类中,您需要指定XAML文件是ModuleCatalog的源,如以下代码所示。

C#复制

protected override IModuleCatalog CreateModuleCatalog()
{
    return ModuleCatalog.CreateFromXaml(new Uri("/MyProject;component/ModulesCatalog.xaml",
UriKind.Relative));
}

使用配置文件注册模块

在WPF中,可以在App.config文件中指定模块信息。此方法的优点是此文件未编译到应用程序中。这使得在运行时添加或删除模块非常容易,无需重新编译应用程序。

以下代码示例显示了指定模块目录的配置文件。如果要自动加载模块,请设置startupLoaded =“true”

XML复制



<configuration>
  <configSections>
    <section name="modules" type="Microsoft.Practices.Prism.Modularity.ModulesConfigurationSection, Microsoft.Practices.Prism"/>
  configSections>
  <modules>    
    <module assemblyFile="ModularityWithUnity.Desktop.ModuleE.dll" moduleType="ModularityWithUnity.Desktop.ModuleE, ModularityWithUnity.Desktop.ModuleE, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleE" startupLoaded="false" />
    <module assemblyFile="ModularityWithUnity.Desktop.ModuleF.dll" moduleType="ModularityWithUnity.Desktop.ModuleF, ModularityWithUnity.Desktop.ModuleF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleF" startupLoaded="false">
      <dependencies>
        <dependency moduleName="ModuleE"/>
      dependencies>
    module>
  modules>
configuration>

 注意

即使程序集位于全局程序集缓存中或与应用程序位于同一文件夹中,也需要assemblyFile属性。该属性用于将moduleType映射到要使用的正确IModuleTypeLoader

在应用程序的Bootstrapper类中,您需要指定配置文件是ModuleCatalog的源。为此,请使用ConfigurationModuleCatalog类,如以下代码所示。

C#复制

protected override IModuleCatalog CreateModuleCatalog()
{
    return new ConfigurationModuleCatalog();
}

 注意

您仍然可以在代码中将模块添加到ConfigurationModuleCatalog。例如,您可以使用它来确保应用程序绝对需要运行的模块在目录中定义。

发现目录中的模块

Prism DirectoryModuleCatalog类允许您将本地目录指定为WPF中的模块目录。此模块目录将扫描指定的文件夹并搜索定义应用程序模块的程序集。要使用此方法,您需要在模块类上使用声明性属性来指定模块名称以及它们具有的任何依赖项。以下代码示例显示了通过发现目录中的程序集来填充的模块目录。

C#复制

protected override IModuleCatalog CreateModuleCatalog()
{
    return new DirectoryModuleCatalog() {ModulePath = @".\Modules"};
}

加载模块

填充ModuleCatalog后,可以加载和初始化模块。模块加载意味着模块组件从磁盘传输到内存。该ModuleManager会负责协调加载和初始化过程。

初始化模块

模块加载后,它们被初始化。这意味着创建了模块类的实例并调用了其Initialize方法。初始化是将模块与应用程序集成的地方。考虑以下模块初始化的可能性:

  • 使用应用程序注册模块的视图。如果您的模块使用视图发现或视图注入参与用户界面(UI)组合,则您的模块将需要将其视图或视图模型与相应的区域名称相关联。这允许视图在应用程序中的菜单,工具栏或其他可视区域上动态显示。

  • 订阅应用程序级别的事件或服务。通常,应用程序会公开您的模块感兴趣的特定于应用程序的服务和/或事件。使用Initialize方法将模块的功能添加到那些应用程序级别的事件和服务。

    例如,应用程序可能会在关闭时引发事件,并且您的模块想要对该事件做出反应。您的模块也可能必须向应用程序级服务提供一些数据。例如,如果您已创建MenuService(它负责添加和删除菜单项),则可以在模块的Initialize方法中添加正确的菜单项。

     注意

    默认情况下,模块实例生存期是短暂的。在加载过程中调用Initialize方法后,将释放对模块实例的引用。如果您没有为模块实例建立强引用链,则会进行垃圾回收。
    如果您订阅包含对模块的弱引用的事件,则此行为可能会导致调试有问题,因为您的模块在垃圾收集器运行时“消失”。

  • 使用依赖项注入容器注册类型。如果使用依赖注入模式(如Unity或MEF),则模块可以为应用程序或其他模块注册要使用的类型。它还可能要求容器解析所需类型的实例。

指定模块依赖项

模块可能依赖于其他模块。如果模块A依赖于模块B,则必须在模块A之前初始化模块B. ModuleManager会跟踪这些依赖关系并相应地初始化模块。根据您定义模块目录的方式,您可以在代码,配置或XAML中定义模块依赖性。

在代码中指定依赖项

对于在代码中注册模块或按目录发现模块的WPF应用程序,Prism提供了在创建模块时使用的声明性属性,如以下代码示例所示。

C#复制

// (when using Unity)
[Module(ModuleName = "ModuleA")]
[ModuleDependency("ModuleD")]
public class ModuleA: IModule
{
    ...
}

在XAML中指定依赖项

以下XAML显示了模块F依赖于模块E的位置。

XAML复制


<Modularity:ModuleInfo Ref="file://ModularityWithMef.Desktop.ModuleE.dll" moduleName="ModuleE" moduleType="ModularityWithMef.Desktop.ModuleE, ModularityWithMef.Desktop.ModuleE, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">

<Modularity:ModuleInfo Ref="file://ModularityWithMef.Desktop.ModuleF.dll" moduleName="ModuleF" moduleType="ModularityWithMef.Desktop.ModuleF, ModularityWithMef.Desktop.ModuleF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" >

<Modularity:ModuleInfo.DependsOn>
    <sys:String>ModuleEsys:String>
Modularity:ModuleInfo.DependsOn>
Modularity:ModuleInfo>
. . . 

在配置中指定依赖项

以下示例App.config文件显示了模块F依赖于模块E的位置。

XML复制

<modules>    <module assemblyFile="ModularityWithUnity.Desktop.ModuleE.dll" moduleType="ModularityWithUnity.Desktop.ModuleE, ModularityWithUnity.Desktop.ModuleE, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleE" startupLoaded="false" />    <module assemblyFile="ModularityWithUnity.Desktop.ModuleF.dll" moduleType="ModularityWithUnity.Desktop.ModuleF, ModularityWithUnity.Desktop.ModuleF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleF" startupLoaded="false">      <dependencies>        <dependency moduleName="ModuleE" />      dependencies>    module>  modules>

按需加载模块

要按需加载模块,您需要指定将它们加载到模块目录中,并将InitializationMode设置为OnDemand。执行此操作后,您需要在应用程序中编写请求加载模块的代码。

在代码中指定按需加载

使用属性将模块指定为按需,如以下代码示例所示。

C#复制

// Boostrapper.cs
protected override void ConfigureModuleCatalog()
{
  . . . 
  Type moduleCType = typeof(ModuleC);
  this.ModuleCatalog.AddModule(new ModuleInfo()
  {
      ModuleName = moduleCType.Name,
      ModuleType = moduleCType.AssemblyQualifiedName,
      InitializationMode = InitializationMode.OnDemand
  });
  . . . 
} 

在XAML中指定按需加载

在XAML中定义模块目录时,可以指定InitializationMode.OnDemand,如以下代码示例所示。

XAML复制


...
<module assemblyFile="ModularityWithUnity.Desktop.ModuleE.dll" moduleType="ModularityWithUnity.Desktop.ModuleE, ModularityWithUnity.Desktop.ModuleE, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleE" startupLoaded="false" />
...

在配置中指定按需加载

在App.config文件中定义模块目录时,可以指定InitializationMode.OnDemand,如以下代码示例所示。

XML复制

<module assemblyFile="ModularityWithUnity.Desktop.ModuleC.dll" moduleType="ModularityWithUnity.Desktop.ModuleC, ModularityWithUnity.Desktop.ModuleC, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" moduleName="ModuleC" startupLoaded="false" />

请求按需加载模块

在按需指定模块后,应用程序可以请求加载模块。想要启动加载的代码需要获取对引导程序向容器注册的IModuleManager服务的引用。

C#复制

private void OnLoadModuleCClick(object sender, RoutedEventArgs e)
{
    moduleManager.LoadModule("ModuleC");
}

检测模块何时加载

所述ModuleManager会服务提供了一个用于事件的应用程序的模块负载时来跟踪或无法加载。您可以通过依赖注入IModuleManager接口来获取对此服务的引用。

C#复制

this.moduleManager.LoadModuleCompleted += this.ModuleManager_LoadModuleCompleted;

C#复制

void ModuleManager_LoadModuleCompleted(object sender, LoadModuleCompletedEventArgs e)
{
    ...
}

为了使应用程序和模块保持松散耦合,应用程序应避免使用此事件将模块与应用程序集成。相反,模块的Initialize方法应该处理与应用程序的集成。

LoadModuleCompletedEventArgs包含IsErrorHandled财产。如果模块无法加载并且应用程序想要阻止ModuleManager记录错误并抛出异常,则可以将此属性设置为true

 注意

加载并初始化模块后,无法卸载模块组件。Prism库不会保存模块实例引用,因此初始化完成后可能会对模块类实例进行垃圾回收。

MEF中的模块

如果您选择使用MEF作为依赖注入容器,本节仅突出显示差异。

 注意

使用MEF时,MefBootstrapper使用MefModuleManager。它扩展了ModuleManager并实现了IPartImportsSatisfiedNotification接口,以确保在MEF导入新类型时更新ModuleCatalog

使用MEF在代码中注册模块

使用MEF时,可以将ModuleExport属性应用于模块类,以使MEF自动发现类型。以下是一个例子。

C#复制

[ModuleExport(typeof(ModuleB), InitializationMode = InitializationMode.OnDemand)]
 public class ModuleB : IModule
{
    ...
}

您还可以使用MEF来发现和加载模块,使用AssemblyCatalog类(可用于发现程序集中的所有导出的模块类)和AggregateCatalog类(允许将多个目录组合到一个逻辑目录中)。默认情况下,Prism MefBootstrapper类创建一个AggregateCatalog实例。然后,您可以覆盖ConfigureAggregateCatalog方法****以注册程序集,如以下代码示例所示。

C#复制

protected override void ConfigureAggregateCatalog()
{
    base.ConfigureAggregateCatalog();
    //Module A is referenced in in the project and directly in code.
    this.AggregateCatalog.Catalogs.Add(
    new AssemblyCatalog(typeof(ModuleA).Assembly));

    this.AggregateCatalog.Catalogs.Add(
        new AssemblyCatalog(typeof(ModuleC).Assembly));
    . . . 
}

Prism MefModuleManager实现使MEF AggregateCatalog和Prism ModuleCatalog保持同步,从而允许Prism发现通过ModuleCatalogAggregateCatalog添加的模块。

 注意

MEF 广泛使用Lazy 来防止导出和导入类型的实例化,直到使用Value属性。

使用MEF在目录中发现模块

MEF提供了一个DirectoryCatalog,可用于检查包含模块(以及其他MEF导出类型)的程序集的目录。在这种情况下,您将覆盖ConfigureAggregateCatalog方法以注册该目录。此方法仅适用于WPF。

要使用此方法,首先需要使用ModuleExport属性将模块名称和依赖项应用于模块,如以下代码示例所示。这允许MEF导入模块并允许Prism 更新ModuleCatalog

C#复制

protected override void ConfigureAggregateCatalog()
{
    base.ConfigureAggregateCatalog();
    . . . 

    DirectoryCatalog catalog = new DirectoryCatalog("DirectoryModules");
    this.AggregateCatalog.Catalogs.Add(catalog);
}

使用MEF在代码中指定依赖关系

对于使用MEF的WPF应用程序,请使用ModuleExport属性,如下所示。

C#复制

// (when using MEF)
[ModuleExport(typeof(ModuleA), DependsOnModuleNames = new string[] { "ModuleD" })]
public class ModuleA : IModule
{
    ...
}

因为MEF允许您在运行时发现模块,所以您还可以在运行时发现模块之间的新依赖关系。虽然您可以在ModuleCatalog旁边使用MEF ,但重要的是要记住ModuleCatalog在从XAML或配置加载时(在加载任何模块之前)验证依赖关系链。如果ModuleCatalog中列出了一个模块,然后使用MEF加载,则将使用ModuleCatalog依赖项,并忽略DependsOnModuleNames属性。

使用MEF指定按需加载

如果使用MEF和ModuleExport属性来指定模块和模块依赖关系,则可以使用InitializationMode属性指定应按需加载模块,如此处所示。

C#复制

[ModuleExport(typeof(ModuleC), InitializationMode = InitializationMode.OnDemand)]
public class ModuleC : IModule
{
}

Model-View-ViewModel(MVVM)模式可帮助您将应用程序的业务和表示逻辑与其用户界面(UI)完全分离。在应用程序逻辑和UI之间保持清晰的分离有助于解决许多开发和设计问题,并使您的应用程序更容易测试,维护和发展。它还可以极大地改善代码重用机会,并允许开发人员和UI设计人员在开发应用程序的各个部分时更轻松地进行协作。

使用MVVM模式,应用程序的UI以及底层表示和业务逻辑被分成三个独立的类:视图,它封装了UI和UI逻辑; 视图模型,它封装了表示逻辑和状态; 和模型,它封装了应用程序的业务逻辑和数据。

Prism包含示例和参考实现,展示如何在Windows Presentation Foundation(WPF)应用程序中实现MVVM模式。Prism Library还提供了可以帮助您在自己的应用程序中实现模式的功能。这些功能体现了实现MVVM模式的最常见实践,旨在支持可测试性并与Expression Blend和Visual Studio配合使用。

本主题概述了MVVM模式,并描述了如何实现其基本特征。高级MVVM方案主题描述了如何使用Prism库实现更高级的MVVM方案。

阶级责任和特征
MVVM模式是Presentation Model模式的近似变体,经过优化以利用WPF的一些核心功能,例如数据绑定,数据模板,命令和行为。

在MVVM模式中,视图封装了UI和任何UI逻辑,视图模型封装了表示逻辑和状态,模型封装了业务逻辑和数据。视图通过数据绑定,命令和更改通知事件与视图模型交互。视图模型查询,观察和协调模型的更新,转换,验证和聚合数据,以便在视图中显示。

下图显示了三个MVVM类及其交互。

MVVM类及其交互

与所有分离的表示模式一样,有效使用MVVM模式的关键在于理解将应用程序代码分解为正确类的适当方式,以及理解这些类在各种场景中交互的方式。以下部分描述了MVVM模式中每个类的职责和特征。

视图类
视图的职责是定义用户在屏幕上看到的内容的结构和外观。理想情况下,视图的代码隐藏只包含一个调用InitializeComponent方法的构造函数。在某些情况下,代码隐藏可能包含UI逻辑代码,该代码实现了在可扩展应用程序标记语言(XAML)中表达难以或低效的视觉行为,例如复杂的动画,或者当代码需要直接操作视觉元素时部分观点。您不应该在视图中放置任何需要进行单元测试的逻辑代码。通常,视图的代码隐藏中的逻辑代码将通过UI自动化测试方法进行测试。

在WPF中,视图中的数据绑定表达式将根据其数据上下文进行评估。在MVVM中,视图的数据上下文设置为视图模型。视图模型实现视图可以绑定的属性和命令,并通过更改通知事件通知视图状态的任何更改。视图与其视图模型之间通常存在一对一的关系。

通常,视图是Control- derived或UserControl派生的类。但是,在某些情况下,视图可以由数据模板表示,该数据模板指定用于在显示对象时可视地表示对象的UI元素。使用数据模板,可视化设计人员可以轻松定义视图模型的呈现方式,也可以修改其默认的可视化表示,而无需更改底层对象本身或用于显示它的控件的行为。

可以将数据模板视为没有任何代码隐藏的视图。它们旨在绑定到特定的视图模型类型,只要需要在UI中显示一个。在运行时,将自动实例化由数据模板定义的视图,并将其数据上下文设置为相应的视图模型。

在WPF中,您可以在应用程序级别将数据模板与视图模型类型相关联。然后,无论何时在UI中显示,WPF都会自动将数据模板应用于指定类型的任何视图模型对象。这称为隐式数据模板。数据模板可以与使用它的控件一起定义,也可以在父视图外的资源字典中定义,并以声明方式合并到视图的资源字典中。

总而言之,该视图具有以下主要特征:

视图是可视元素,例如窗口,页面,用户控件或数据模板。视图定义视图中包含的控件及其可视布局和样式。
视图通过其DataContext属性引用视图模型。视图中的控件是绑定到视图模型公开的属性和命令的数据。
视图可以自定义视图和视图模型之间的数据绑定行为。例如,视图可以使用值转换器来格式化要在UI中显示的数据,或者它可以使用验证规则来向用户提供额外的输入数据验证。
视图定义并处理UI视觉行为,例如可以从视图模型中的状态更改或通过用户与UI的交互触发的动画或过渡。
视图的代码隐藏可以定义UI逻辑以实现在XAML中难以表达的视觉行为,或者需要直接引用视图中定义的特定UI控件。
视图模型类
MVVM模式中的视图模型封装了视图的表示逻辑和数据。它没有直接引用视图或有关视图的特定实现或类型的任何知识。视图模型实现视图可以绑定数据的属性和命令,并通过更改通知事件通知视图任何状态更改。视图模型提供的属性和命令定义UI提供的功能,但视图确定如何呈现该功能。

视图模型负责协调视图与所需的任何模型类的交互。通常,视图模型和模型类之间存在一对多关系。视图模型可以选择直接将模型类公开给视图,以便视图中的控件可以直接将数据绑定到它们。在这种情况下,需要设计模型类以支持数据绑定和相关的更改通知事件。有关此方案的详细信息,请参阅本主题后面的“数据绑定”一节。

视图模型可以转换或操纵模型数据,以便视图可以轻松地使用它。视图模型可以定义其他属性以专门支持视图; 这些属性通常不属于模型的一部分(或不能添加到模型中)。例如,视图模型可以组合两个字段的值以使视图更容易呈现,或者它可以计算具有最大长度的字段的输入剩余字符数。视图模型还可以实现数据验证逻辑以确保数据一致性。

视图模型还可以定义视图可以用于在UI中提供视觉变化的逻辑状态。视图可以定义反映视图模型状态的布局或样式更改。例如,视图模型可以定义指示数据异步提交到Web服务的状态。视图可以在此状态期间显示动画,以向用户提供视觉反馈。

通常,视图模型将定义可在UI中表示并且用户可以调用的命令或操作。一个常见示例是视图模型提供允许用户将数据提交到Web服务或数据存储库的Submit命令。视图可以选择用按钮表示该命令,以便用户可以单击按钮来提交数据。通常,当命令变得不可用时,其关联的UI表示将被禁用。命令提供了一种封装用户操作并将其与UI中的可视表示清晰分离的方法。

总而言之,视图模型具有以下关键特征:

视图模型是非可视类,不是从任何WPF基类派生的。它封装了支持应用程序中的用例或用户任务所需的表示逻辑。视图模型可以独立于视图和模型进行测试。
视图模型通常不直接引用视图。它实现了视图可以绑定数据的属性和命令。它通过INotifyPropertyChanged和INotifyCollectionChanged接口通过更改通知事件通知视图任何状态更改。
视图模型协调视图与模型的交互。它可以转换或操作数据,以便视图可以轻松使用它,并可以实现模型上可能不存在的其他属性。它还可以通过IDataErrorInfo或INotifyDataErrorInfo接口实现数据验证。
视图模型可以定义视图可以在视觉上向用户表示的逻辑状态。
 注意

查看或查看模型?
很多时候,确定应该实现某些功能的地方并不明显。一般的经验法则是:任何与屏幕上UI的特定视觉外观有关并且可以在以后重新设置样式的内容(即使您当前没有计划重新设置样式)也应该进入视图; 对应用程序的逻辑行为很重要的任何内容都应该进入视图模型。此外,由于视图模型应该不具有视图中特定可视元素的明确知识,因此以编程方式操作视图中的可视元素的代码应驻留在视图的代码隐藏中或封装在行为中。同样,检索或操作要通过数据绑定在视图中显示的数据项的代码应驻留在视图模型中。
例如,应在视图中定义列表框中所选项目的突出显示颜色,但应由视图模型定义要显示的项目列表以及对所选项目本身的引用。

模型类
MVVM模式中的模型封装了业务逻辑和数据。业务逻辑被定义为与应用程序数据的检索和管理有关的任何应用程序逻辑,并确保强制执行确保数据一致性和有效性的任何业务规则。为了最大化重用机会,模型不应包含任何特定于用例或特定于用户任务的行为或应用程序逻辑。

通常,模型表示应用程序的客户端域模型。它可以基于应用程序的数据模型和任何支持业务和验证逻辑来定义数据结构。该模型还可以包括支持数据访问和缓存的代码,尽管通常使用单独的数据存储库或服务。通常,模型和数据访问层是作为数据访问或服务策略的一部分生成的,例如ADO.NET实体框架,WCF数据服务或WCF RIA服务。

通常,模型实现了可以轻松绑定到视图的工具。这通常意味着它通过INotifyPropertyChanged和INotifyCollectionChanged接口支持属性和集合更改通知。表示对象集合的模型类通常派生自ObservableCollection 类,该类提供INotifyCollectionChanged接口的实现。

该模型还可以通过IDataErrorInfo(或INotifyDataErrorInfo)接口支持数据验证和错误报告。该IDataErrorInfo的和INotifyDataErrorInfo接口使WPF数据绑定时通知值发生改变,这样的UI可以更新。它们还支持UI层中的数据验证和错误报告。

 注意

如果您的模型类没有实现所需的接口,该怎么办?
有时您需要使用未实现INotifyPropertyChanged,INotifyCollectionChanged,IDataErrorInfo或INotifyDataErrorInfo接口的模型对象。在这些情况下,视图模型可能需要包装模型对象并将所需的属性公开给视图。这些属性的值将由模型对象直接提供。视图模型将为它公开的属性实现所需的接口,以便视图可以轻松地将数据绑定到它们。

该模型具有以下主要特征:

模型类是非可视类,它封装了应用程序的数据和业务逻辑。他们负责管理应用程序的数据,并通过封装所需的业务规则和数据验证逻辑来确保其一致性和有效性。
模型类不直接引用视图或视图模型类,也不依赖于它们的实现方式。
模型类通常通过INotifyPropertyChanged和INotifyCollectionChanged接口提供属性和集合更改通知事件。这允许它们在视图中容易地数据绑定。表示对象集合的模型类通常派生自ObservableCollection 类。
模型类通常通过IDataErrorInfo或INotifyDataErrorInfo接口提供数据验证和错误报告。
模型类通常与封装数据访问和缓存的服务或存储库结合使用。
班级互动
MVVM模式通过将每个应用程序的用户界面,其表示逻辑以及业务逻辑和数据分离为单独的类,提供了清晰的分离。因此,在实现MVVM时,重要的是将应用程序的代码分解为正确的类,如上一节所述。

精心设计的视图,视图模型和模型类不仅会封装正确类型的代码和行为; 它们的设计也使它们可以通过数据绑定,命令和数据验证接口轻松地相互交互。

视图与其视图模型之间的交互可能是最重要的考虑因素,但模型类和视图模型之间的交互也很重要。以下部分描述了这些交互的各种模式,并描述了在应用程序中实现MVVM模式时如何设计它们。

数据绑定
数据绑定在MVVM模式中起着非常重要的作用。WPF提供强大的数据绑定功能。您的视图模型和(理想情况下)您的模型类应设计为支持数据绑定,以便它们可以利用这些功能。通常,这意味着它们必须实现正确的接口。

WPF数据绑定支持多种数据绑定模式。通过单向数据绑定,可以将UI控件绑定到视图模型,以便在呈现显示时它们反映基础数据的值。当用户在UI中修改基础数据时,双向数据绑定也将自动更新基础数据。

为确保在视图模型中数据发生更改时UI保持最新,它应实现相应的更改通知界面。如果它定义了可以绑定数据的属性,它应该实现INotifyPropertyChanged接口。如果视图模型表示集合,则它应实现INotifyCollectionChanged接口,或者从提供此接口实现的ObservableCollection 类派生。这两个接口都定义了每当基础数据发生更改时引发的事件。引发这些事件时,将自动更新任何数据绑定控件。

在许多情况下,视图模型将定义返回对象的属性(反过来,可以定义返回其他对象的属性)。WPF数据绑定支持通过Path属性绑定到嵌套属性。因此,视图的视图模型返回对其他视图模型或模型类的引用是很常见的。视图可访问的所有视图模型和模型类应根据需要实现INotifyPropertyChanged或INotifyCollectionChanged接口。

以下部分描述了如何实现所需的接口以支持MVVM模式中的数据绑定。

实现INotifyPropertyChanged
在视图模型或模型类中实现INotifyPropertyChanged接口允许它们在基础属性值更改时向视图中的任何数据绑定控件提供更改通知。实现此接口非常简单,如以下代码示例所示。

C#

复制
public class Questionnaire : INotifyPropertyChanged
{
    private string favoriteColor;
    public event PropertyChangedEventHandler PropertyChanged;
    ...
    public string FavoriteColor
    {
        get { return this.favoriteColor; }
        set
        {
            if (value != this.favoriteColor)
            {
                this.favoriteColor = value;

                var handler = this.PropertyChanged;
                if (handler != null)
                {
                    handler(this,
                          new PropertyChangedEventArgs("FavoriteColor"));
                }
            }
        }
    }
}
由于需要在event参数中指定属性名称,因此在许多视图模型类上实现INotifyPropertyChanged接口可能是重复且容易出错的。Prism库提供了BindableBase基类,您可以从中派生以类型安全的方式实现INotifyPropertyChanged接口的视图模型类,如此处所示。

C#

复制
public abstract class BindableBase : INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged;
   ...
   protected virtual bool SetProperty(ref T storage, T value, 
                          [CallerMemberName] string propertyName = null)
   {...}
   protected void OnPropertyChanged(
                          Expression> propertyExpression)
   {...}

   protected void OnPropertyChanged(string propertyName)
   {...}
}
派生视图模型类可以通过调用SetProperty方法在setter中引发属性更改事件。所述的SetProperty方法检查被设定的值支持字段是否是不同的。如果不同,则更新后备字段并引发PropertyChanged事件。

下面的代码示例演示如何设置属性,并通过在OnPropertyChanged方法中使用lambda表达式同时发出另一个属性的更改。此示例来自Stock Trader RI。该TransactionInfo和TickerSymbol属性相关。如果TransactionInfo属性更改,则TickerSymbol也可能会更新。通过调用OnPropertyChanged的TickerSymbol中的setter属性TransactionInfo财产,二的PropertyChanged事件将提高,一个用于TransactionInfo,一个用于TickerSymbol。

C#

复制
public TransactionInfo TransactionInfo
{
    get { return this.transactionInfo; } 
    set 
    { 
         SetProperty(ref this.transactionInfo, value); 
         this.OnPropertyChanged(() => this.TickerSymbol);
    }
}
 注意

以这种方式使用lambda表达式涉及较小的性能成本,因为必须为每个调用计算lambda表达式。好处是,如果重命名属性,此方法可提供编译时类型安全性和重构支持。虽然性能成本很低,并且通常不会影响您的应用程序,但如果您有许多更改通知,则会产生成本。在这种情况下,您应该考虑使用非lambda方法重载。

通常,模型或视图模型将包含其值从模型或视图模型中的其他属性计算的属性。处理属性更改时,请务必同时为任何计算属性引发通知事件。

实现INotifyCollectionChanged
您的视图模型或模型类可以表示项的集合,也可以定义一个或多个返回项集合的属性。在任何一种情况下,您可能希望在ItemsControl中显示集合,例如ListBox,或者在视图中的DataGrid控件中。这些控件可以是绑定到视图模型的数据,该视图模型表示集合或通过ItemSource属性返回集合的属性。

XAML

复制

为了正确支持更改通知请求,视图模型或模型类(如果它表示集合)应实现INotifyCollectionChanged接口(除了INotifyPropertyChanged接口)。如果视图模型或模型类定义了返回对集合的引用的属性,则返回的集合类应实现INotifyCollectionChanged接口。

但是,实现INotifyCollectionChanged接口可能具有挑战性,因为它必须在集合中添加,删除或更改项目时提供通知。它不是直接实现接口,而是通常更容易使用或派生自已实现它的集合类。所述的ObservableCollection 类提供这个接口的实现和通常用作任一个基类或执行该代表项的集合的性质。

如果需要为视图提供数据绑定的集合,并且不需要跟踪用户的选择或支持对集合中项目的过滤,排序或分组,则只需在视图模型上定义属性即可返回对ObservableCollection 实例的引用。

C#

复制
public class OrderViewModel : BindableBase
{
    public OrderViewModel( IOrderService orderService )
    {
        this.LineItems = new ObservableCollection(
                               orderService.GetLineItemList() );
    }

    public ObservableCollection LineItems { get; private set; }
}
如果获得对集合类的引用(例如,来自未实现INotifyCollectionChanged的其他组件或服务),则通常可以使用其中一个构造函数将该集合包装在ObservableCollection 实例中,该构造函数采用IEnumerable 或List 参数。

 注意

BindableBase可以在位于Prism.Mvvm NuGet包中的Microsoft.Practices.Prism.Mvvm命名空间中找到。

实现ICollectionView
上面的代码示例演示如何实现一个简单的视图模型属性,该属性返回可以通过视图中的数据绑定控件显示的项集合。由于ObservableCollection 类实现了INotifyCollectionChanged接口,因此在添加或删除项目时,视图中的控件将自动更新以反映集合中的当前项目列表。

但是,您通常需要更精细地控制项目集合在视图中的显示方式,或者在视图模型本身内跟踪用户与显示的项目集合的交互。例如,您可能需要根据视图模型中实现的表示逻辑来过滤或排序项目集合,或者您可能需要跟踪视图中当前选定的项目,以便在视图模型中实现命令可以对当前选定的项目采取行动。

WPF通过提供实现ICollectionView接口的各种类来支持这些场景。此接口提供允许对集合进行过滤,排序或分组的属性和方法,并允许跟踪或更改当前选定的项目。WPF使用ListCollectionView类提供此接口的实现。

集合视图类通过包装基础项目集合来工作,以便它们可以为它们提供自动选择跟踪和排序,过滤和分页。可以使用CollectionViewSource类在XAML中以编程方式或声明方式创建这些类的实例。

 注意

在WPF中,只要控件绑定到集合,就会自动创建默认集合视图。

视图模型可以使用集合视图类来跟踪底层集合的重要状态信息,同时保持视图中的UI与模型中的基础数据之间的关注点的清晰分离。实际上,CollectionViews是专为支持集合而设计的视图模型。

因此,如果需要在视图模型中对集合中的项目进行过滤,排序,分组或选择跟踪,则视图模型应为要向视图公开的每个集合创建集合视图类的实例。然后,您可以使用视图模型中集合视图类提供的方法订阅选择更改的事件(例如CurrentChanged事件)或控制过滤,排序或分组。

视图模型应该实现一个只读属性,该属性返回ICollectionView引用,以便视图中的控件可以将数据绑定到集合视图对象并与之交互。从ItemsControl基类派生的所有WPF控件都可以自动与ICollectionView类交互。

以下代码示例显示了在WPF中使用ListCollectionView来跟踪当前选定的客户。

C#

复制
public class MyViewModel : BindableBase
{
    public ICollectionView Customers { get; private set; }

    public MyViewModel( ObservableCollection customers )
    {
        // Initialize the CollectionView for the underlying model
        // and track the current selection.
        Customers = new ListCollectionView( customers );

        Customers.CurrentChanged +=SelectedItemChanged;
    }

    private void SelectedItemChanged( object sender, EventArgs e )
    {
        Customer current = Customers.CurrentItem as Customer;
        ...
    }
    ...
}
在视图中,您可以通过ItemsSource属性将ItemsControl(如ListBox)绑定到视图模型上的Customers属性,如此处所示。

XAML

复制

   
       
           
               
           

       

   


当用户在UI中选择客户时,将通知视图模型,以便它可以应用与当前所选客户相关的命令。视图模型还可以通过调用集合视图对象上的方法以编程方式更改UI中的当前选择,如以下代码示例所示。

C#

复制
Customers.MoveCurrentToNext();
当选择在集合视图中更改时,UI会自动更新以直观地表示项目的选定状态。

命令
除了提供对要在视图中显示或编辑的数据的访问之外,视图模型还可能定义可由用户执行的一个或多个动作或操作。在WPF中,用户可以通过UI执行的操作或操作通常被定义为命令。命令提供了一种方便的方法来表示可以轻松绑定到UI中的控件的操作或操作。它们封装了实现操作或操作的实际代码,并有助于使其与视图中的实际可视化表示分离。

当用户与视图交互时,用户可以以多种不同的方式直观地表示和调用命令。在大多数情况下,它们是通过鼠标单击调用的,但也可以通过快捷键按下,触摸手势或任何其他输入事件来调用它们。视图中的控件是绑定到视图模型命令的数据,以便用户可以使用控件定义的任何输入事件或手势来调用它们。视图中的UI控件与命令之间的交互可以是双向的。在这种情况下,可以在用户与UI交互时调用该命令,并且可以在启用或禁用基础命令时自动启用或禁用UI。

视图模型可以将命令实现为命令方法或命令对象(实现ICommand接口的对象)。在任何一种情况下,视图与命令的交互都可以以声明方式定义,而不需要在视图的代码隐藏文件中使用复杂的事件处理代码。例如,WPF中的某些控件本身支持命令并提供Command属性,该属性可以是绑定到视图模型提供的ICommand对象的数据。在其他情况下,命令行为可用于将控件与视图模型提供的命令方法或命令对象相关联。

 注意

行为是一种强大而灵活的可扩展性机制,可用于封装交互逻辑和行为,然后可以与视图中的控件进行声明性关联。命令行为可用于将命令对象或方法与未专门设计用于与命令交互的控件相关联。

以下部分描述了如何在视图中实现命令,命令方法或命令对象,以及如何将它们与视图中的控件相关联。

实现基于任务的委托命令
在许多情况下,命令使用长时间运行的事务调用代码,这些事务无法阻止UI线程。对于这种情况,你应该使用FromAsyncHandler的方法DelegateCommand类,它创建的新实例DelegateCommand从一个异步处理方法。

C#

复制
// DelegateCommand.cs
public static DelegateCommand FromAsyncHandler(Func executeMethod, Func canExecuteMethod)
{
    return new DelegateCommand(executeMethod, canExecuteMethod);
}
例如,以下代码显示如何通过指定SignInAsync和CanSignIn视图模型方法的委托来构造表示登录命令的DelegateCommand实例。然后,该命令通过只读属性公开给视图,该属性返回对ICommand的引用。

C#

复制
// SignInFlyoutViewModel.cs
public DelegateCommand SignInCommand { get; private set;  }

...
SignInCommand = DelegateCommand.FromAsyncHandler(SignInAsync, CanSignIn);
实现命令对象
命令对象是实现ICommand接口的对象。该接口定义了一个Execute方法,它封装了操作本身,以及一个CanExecute方法,它指示是否可以在特定时间调用该命令。这两种方法都只使用一个参数作为命令的参数。对命令对象中的操作的实现逻辑的封装意味着它可以更容易地进行单元测试和维护。

实现ICommand接口非常简单。但是,您可以在应用程序中轻松使用此接口的许多实现。例如,您可以使用Blend for Visual Studio SDK中的ActionCommand类或Prism提供的DelegateCommand类。

 注意

DelegateCommand可以在位于Prism.Mvvm NuGet包中的Microsoft.Practices.Prism.Mvvm命名空间中找到。

Prism DelegateCommand类封装了两个委托,每个委托引用在视图模型类中实现的方法。它继承自DelegateCommandBase类,该类通过调用这些委托来实现ICommand接口的Execute和CanExecute方法。您可以在DelegateCommand类构造函数中为视图模型方法指定委托,其定义如下。

C#

复制
// DelegateCommand.cs
public class DelegateCommand : DelegateCommandBase
{
    public DelegateCommand(Action executeMethod,Func canExecuteMethod ): base((o) => executeMethod((T)o), (o) => canExecuteMethod((T)o))
    {
        ...
    }
}
例如,以下代码示例显示如何通过为OnSubmit和CanSubmit视图模型方法指定委托来构造表示Submit命令的DelegateCommand实例。然后,该命令通过只读属性公开给视图,该属性返回对ICommand的引用。

C#

复制
public class QuestionnaireViewModel
{
    public QuestionnaireViewModel()
    {
       this.SubmitCommand = new DelegateCommand(
                                        this.OnSubmit, this.CanSubmit );
    }

    public ICommand SubmitCommand { get; private set; }

    private void OnSubmit(object arg)   {...}
    private bool CanSubmit(object arg)  { return true; }
}
当在DelegateCommand对象上调用Execute方法时,它只是通过您在构造函数中指定的委托将调用转发到视图模型类中的方法。同样,调用CanExecute方法时,将调用视图模型类中的相应方法。构造函数中CanExecute方法的委托是可选的。如果未指定委托,则DelegateCommand将始终为CanExecute返回true。

该DelegateCommand类是一个泛型类型。type参数指定传递给Execute和CanExecute方法的命令参数的类型。在前面的示例中,command参数的类型为object。Prism还提供了非泛型版本的DelegateCommand类,以便在不需要命令参数时使用。

视图模型可以通过调用DelegateCommand对象上的RaiseCanExecuteChanged方法来指示命令的CanExecute状态的更改。这会导致引发CanExecuteChanged事件。UI中绑定到该命令的任何控件都将更新其启用状态以反映绑定命令的可用性。

可以使用ICommand接口的其他实现。Expression Blend SDK提供的ActionCommand类与前面描述的Prism的DelegateCommand类类似,但它仅支持单个Execute方法委托。Prism还提供了CompositeCommand类,它允许将DelegateCommands组合在一起执行。有关使用CompositeCommand类的更多信息,请参阅“ 高级MVVM方案 ”中的“ 复合命令 ” 。

从视图调用命令对象
有许多方法可以将视图中的控件与视图模型提供的命令对象相关联。某些WPF控件,特别是ButtonBase派生控件,如Button或RadioButton,以及Hyperlink或MenuItem派生控件,可以通过Command属性轻松地将数据绑定到命令对象。WPF还支持将视图模型ICommand绑定到KeyGesture。

XAML

复制