https://docs.microsoft.com/en-us/previous-versions/msp-n-p/ff648465(v%3dpandp.10)
使用Prism Library构建的应用程序依赖于容器提供的依赖注入。该库提供了与Unity应用程序块(Unity)或托管扩展性框架(MEF)一起使用的程序集,它允许您使用其他依赖项注入容器。引导过程的一部分是配置此容器并使用容器注册类型。
Prism库包括UnityBootstrapper和MefBootstrapper类,它们实现了在应用程序中使用Unity或MEF作为依赖注入容器所需的大部分功能。
在传统的Windows Presentation Foundation(WPF)应用程序中,启动主窗口的App.xaml文件中指定了启动统一资源标识符(URI)。
在使用Prism Library创建的应用程序中,引导程序负责创建shell或主窗口。这是因为shell依赖于需要在显示shell之前注册的服务(例如Region Manager)
在您决定在应用程序中使用Prism库之后,还需要做出一些额外的决定:
创建启动序列是构建Prism应用程序的重要部分。本节介绍如何创建引导程序并对其进行自定义以创建shell,配置依赖项注入容器,注册应用程序级服务以及如何加载和初始化模块。
如果您选择使用Unity或MEF作为依赖注入容器,则可以轻松地为您的应用程序创建一个简单的引导程序。您需要创建一个派生自MefBootstrapper或UnityBootstrapper的新类。然后,* 实现* CreateShell方法。(可选)您可以覆盖InitializeShell方法以进行特定于shell的初始化。
该CreateShell方法允许开发者指定一个Prism应用顶层窗口。shell通常是MainWindow或MainPage。通过返回应用程序的shell类的实例来实现此方法。在Prism应用程序中,您可以创建shell对象,或者根据应用程序的要求从容器中解析它。
以下代码示例中显示了使用ServiceLocator解析shell对象的示例。
protected override DependencyObject CreateShell()
{
return ServiceLocator.Current.GetInstance();
}
创建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()
}
在UnityBootstrapper和MefBootstrapper类中,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,UnityBootstrapper和MefBootstrapper中有两个Bootstrapper派生的类。创建和配置不同的容器涉及以不同方式实现的类似概念。
该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类的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的核心服务作为单例添加到容器中,因此可以在整个应用程序中通过容器定位它们。
除了提供CreateContainer和ConfigureContainer方法之外,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不具备的几种功能:
容器具有不同的功能和不同的工作方式,但Prism库将与容器一起使用并提供类似的功能。在考虑使用哪个容器时,请记住前面的功能并确定哪种容量更适合您的方案。
在使用容器之前,您应该考虑以下事项:
注意
某些容器(如MEF)无法通过配置文件进行配置,必须通过代码进行配置。
容器用于两个主要目的,即注册和解析。
在将依赖项注入对象之前,需要向容器注册依赖项的类型。注册类型通常涉及向容器传递接口和实现该接口的具体类型。注册类型和对象主要有两种方法:通过代码或通过配置。具体方式因容器而异。
通常,有两种方法可以通过代码在容器中注册类型和对象:
在初始化期间,类型可以注册其他类型,例如视图和服务。注册允许通过容器提供其依赖项,并允许从其他类型访问它们。要做到这一点,类型将需要将容器注入模块构造函数。以下代码显示了命令QuickStart中的OrderModule类型如何注册类型。
C#复制
// OrderModule.cs
public class OrderModule : IModule
{
public void Initialize()
{
this.container.RegisterType(new ContainerControlledLifetimeManager());
...
}
...
}
根据您使用的容器,也可以通过配置在代码外部执行注册。有关此示例,请参阅在模块化应用程序开发中使用配置文件注册模块。
注意
与配置相比,在代码中注册的优点是仅在模块加载时才进行注册。
MEF使用基于属性的系统来向容器注册类型。因此,向容器添加类型注册很简单:它需要在类型中添加[Export]属性,如下面的代码示例所示。
C#复制
[Export(typeof(ILoggerFacade))]
public class CallbackLogger: ILoggerFacade
{
}
使用MEF时的另一个选择是创建类的实例并使用容器注册该特定实例。带有MEF QuickStart的Modularity中的QuickStartBootstrapper在ConfigureContainer方法中显示了一个示例,如下所示。
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中,默认情况下会返回新的对象实例,并且容器不会维护对该对象的引用。
命令快速入门中的以下代码示例显示了从容器中解析OrdersEditorView和OrdersToolBar视图的位置,以将它们与相应的区域相关联。
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 QuickStart的Modularity中的Bootstrapper如何获取shell的实例。代码可以请求接口的实例,而不是请求具体类型。
C#复制
protected override DependencyObject CreateShell()
{
return this.Container.GetExportedValue();
}
在MEF解析的任何类中,您也可以使用构造函数注入,如下面的模块化与MEF QuickStart中的ModuleA中的代码示例所示,其中注入了ILoggerFacade和IModuleTracker。
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 Library提供对Unity容器和MEF的支持,但它不是特定于容器的。因为库通过IServiceLocator接口访问容器,所以可以替换容器。为此,您的容器必须实现IServiceLocator接口。通常,如果要更换容器,则还需要提供自己的容器特定引导程序。该IServiceLocator接口在Common Service Locator Library中定义。这是一项开源工作,旨在提供IoC(控制反转)容器的抽象,例如依赖注入容器和服务定位器。使用此库的目的是利用IoC和服务位置,而不必与特定实现相关联。
Prism库提供UnityServiceLocatorAdapter和MefServiceLocatorAdapter。两个适配器都通过扩展ServiceLocatorImplBase类型来实现ISeviceLocator接口。下图显示了类层次结构。
Prism中的Common Service Locator实现
虽然Prism Library不引用或依赖于特定容器,但应用程序通常依赖于特定容器。这意味着特定应用程序引用容器是合理的,但Prism Library不直接引用容器。例如,Stock Trader RI和Prism附带的几个QuickStart依赖Unity作为容器。其他样品和快速入门依赖于MEF。
以下代码显示了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并不是通用容器。容器具有不同的使用语义,这通常决定了为什么选择容器。考虑到这一点,Stock Trader RI直接使用依赖注入容器而不是使用IServiceLocator。这是您的应用程序开发的推荐方法。
在以下情况下,您可能适合使用IServiceLocator:
4、
模块化应用程序是一个应用程序,它被分成一组松散耦合的功能单元(命名模块),可以集成到更大的应用程序中。客户端模块封装了应用程序的整体功能的一部分,并且通常表示一组相关的问题。它可以包括一组相关组件,例如应用程序功能,包括用户界面和业务逻辑,或应用程序基础结构,例如用于记录或验证用户的应用程序级服务。模块彼此独立,但可以以松散耦合的方式彼此通信。使用模块化应用程序设计,您可以更轻松地开发,测试,部署和维护应用程序。
例如,考虑个人银行应用程序。用户可以访问各种功能,例如在账户之间转账,支付账单以及从单个用户界面(UI)更新个人信息。但是,在幕后,这些功能中的每一个都封装在一个离散模块中。这些模块相互通信,并与后端系统(如数据库服务器和Web服务)进行通信。应用服务集成了每个不同模块中的各种组件,并处理与用户的通信。用户看到的视图类似于单个应用程序的集成视图。
下图显示了具有多个模块的模块化应用程序的设计。
模块组成
您可能已经使用程序集,接口和类构建了一个架构良好的应用程序,并采用了良好的面向对象设计原则。即便如此,除非非常小心,否则您的应用程序设计可能仍然是“单一的”(所有功能都在应用程序内以紧密耦合的方式实现),这可能使应用程序难以开发,测试,扩展和维护。
另一方面,模块化应用程序方法可以帮助您识别应用程序的大规模功能区域,并允许您独立开发和测试该功能。这可以使开发和测试更容易,但它也可以使您的应用程序更灵活,更容易在未来扩展。模块化方法的好处是它可以使您的整体应用程序架构更加灵活和可维护,因为它允许您将应用程序分解为可管理的部分。每个部分都封装了特定的功能,每个部分都通过清晰但松散耦合的通信渠道进行集成。
Prism为您的应用程序中的模块化应用程序开发和运行时模块管理提供支持。使用Prism的模块化开发功能可以节省您的时间,因为您不必实现和测试自己的模块化框架。Prism支持以下模块化应用程序开发功能:
本节介绍与Prism模块化相关的核心概念,包括IModule接口,模块加载过程,模块目录,模块之间的通信以及依赖注入容器。
模块是功能和资源的逻辑集合,以可以单独开发,测试,部署和集成到应用程序中的方式打包。包可以是一个或多个程序集。每个模块都有一个中心类,负责初始化模块并将其功能集成到应用程序中。该类实现了IModule接口。
注意:实现IModule接口的类的存在足以将包标识为模块。
该IModule的接口只有一个方法,名为初始化,您可以在其中实现的任何逻辑需要初始化和模块的功能集成到应用程序。根据模块的用途,它可以将视图注册到组合用户界面,为应用程序提供其他服务,或扩展应用程序的功能。以下代码显示了模块的最低实现。
C#复制
public class MyModule : IModule
{
public void Initialize()
{
// Do something here.
}
}
注意
Stock Trader RI使用声明的,基于属性的方法来注册视图,服务和类型,而不是使用IModule接口提供的初始化机制。
Prism中的模块加载过程包括以下内容:
下图显示了模块加载过程。
模块加载过程
所述ModuleCatalog保存关于能够由应用程序使用的模块的信息。目录本质上是ModuleInfo类的集合。ModuleInfo类中描述了每个模块,该类记录了模块的其他属性中的名称,类型和位置。使用ModuleInfo实例填充ModuleCatalog有几种典型方法:
您应该使用的注册和发现机制取决于您的应用程序需要什么。使用配置文件或XAML文件允许您的应用程序不需要引用模块。使用目录可以允许应用程序发现模块,而无需在文件中指定它们。
Prism应用程序可以尽快初始化模块,称为“可用时”,或者当应用程序需要它们时,称为“按需”。请考虑以下加载模块的准则:
考虑如何对应用程序进行分区,常见使用方案,应用程序启动时间以及下载的数量和大小,以确定如何配置模块以进行下载和初始化。
Prism提供以下类来引导您的应用程序:UnityBootstrapper或MefBootstrapper。这些****类可用于创建和配置模块管理器以发现和加载模块。您可以覆盖配置方法,以在几行代码中注册XAML文件,配置文件或目录位置中指定的模块。
使用模块Initialize方法将模块与应用程序的其余部分集成。执行此操作的方式因应用程序的结构和模块的内容而异。以下是将模块集成到应用程序中的常见操作:
即使模块之间的耦合度较低,模块也可以相互通信。有几种松散耦合的通信模式,每种都有自己的优势。通常,这些模式的组合用于创建所得到的解决方案。以下是其中一些模式:
Unity应用程序块(Unity)和托管可扩展性框架(MEF)等容器允许您轻松使用控制反转(IoC)和依赖注入,它们是强大的设计模式,有助于以松散耦合的方式组合组件。它允许组件获得对它们所依赖的其他组件的引用,而无需对这些引用进行硬编码,从而促进更好的代码重用和更高的灵活性。在构建松散耦合的模块化应用程序时,依赖注入非常有用。Prism旨在与用于组成应用程序中的组件的依赖注入容器无关。容器的选择取决于您,并且在很大程度上取决于您的应用要求和偏好。然而,
模式和实践Unity Application Block提供了一个功能齐全的依赖注入容器。它支持基于属性和基于构造函数的注入和策略注入,允许您透明地在组件之间注入行为和策略; 它还支持许多其他典型的依赖注入容器功能。
MEF(它是.NET Framework 4.5的一部分)通过支持基于依赖注入的组件组合提供对构建可扩展.NET应用程序的支持,并提供支持模块化应用程序开发的其他功能。它允许应用程序在运行时发现组件,然后以松散耦合的方式将这些组件集成到应用程序中。MEF是一个很好的可扩展性和组合框架。它包括程序集和类型发现,类型依赖性解析,依赖注入以及一些不错的程序集下载功能。Prism支持利用MEF功能,以及以下内容:
Unity和MEF依赖注入容器都可以与Prism无缝协作。
您要做的第一个决定是您是否要开发模块化解决方案。如上一节所述,构建模块化应用程序有许多好处,但是您需要花费时间和精力来获得这些好处。如果您决定开发模块化解决方案,还有几个需要考虑的事项:
下一节提供了有关这些决策的详细信息。
当您以模块化方式开发应用程序时,可以将应用程序组织到单独的客户端模块中,这些模块可以单独开发,测试和部署。每个模块都将封装应用程序的一部分整体功能。您必须做出的首要设计决策之一是决定如何将应用程序的功能划分为离散模块。
模块应该封装一组相关的问题,并具有一组独特的职责。模块可以表示应用程序的垂直切片或水平服务层。大型应用程序可能有两种类型的模块。
围绕垂直切片组织模块的应用程序
围绕水平层组织模块的应用程序
较大的应用程序可能具有使用垂直切片和水平层组织的模块。模块的一些示例包括以下内容:
模块应该对其他模块具有最小的依赖关系。当模块依赖于另一个模块时,它应该通过使用共享库中定义的接口而不是具体类型来松散耦合,或者通过使用EventAggregator通过EventAggregator事件类型与其他模块进行通信。
模块化的目标是以一种即使在添加和删除功能和技术时仍保持灵活性,可维护性和稳定性的方式对应用程序进行分区。实现此目的的最佳方法是设计应用程序,使模块尽可能独立,具有良好定义的接口,并尽可能隔离。
有几种方法可以创建和打包模块。建议的和最常见的方法是为每个模块创建一个组件。这有助于保持逻辑模块分离并促进适当的封装。它还使得更容易将组件作为模块边界以及如何部署模块的包装进行讨论。但是,没有什么可以阻止单个程序集包含多个模块,在某些情况下,这可能是首选,以最大限度地减少解决方案中的项目数量。对于大型应用程序,拥有10-50个模块并不罕见。将每个模块分离到自己的项目中会增加解决方案的复杂性,并会降低Visual Studio的性能。
模块可以依赖于主机应用程序或其他模块提供的组件和服务。Prism支持在模块之间注册依赖关系的能力,以便以正确的顺序加载和初始化它们。Prism还支持在将模块加载到应用程序时初始化模块。在模块初始化期间,模块可以检索对其所需的附加组件和服务的引用,和/或注册它包含的任何组件和服务,以使其可供其他模块使用。
模块应使用独立机制来获取外部接口的实例,而不是直接实例化具体类型,例如通过使用依赖注入容器或工厂服务。诸如Unity或MEF之类的依赖注入容器允许类型通过依赖注入自动获取所需的接口和类型的实例。Prism与Unity和MEF集成,允许模块轻松使用依赖注入。
下图显示了加载模块时需要获取或注册组件和服务引用的典型操作顺序。
依赖注入的示例
在此示例中,OrdersModule程序集定义了OrdersRepository类(以及实现顺序功能的其他视图和类)。所述CustomerModule组件限定CustomersViewModel类依赖于OrdersRepository,通常基于由服务暴露的接口上。应用程序启动和引导过程包含以下步骤:
引导程序启动模块初始化过程,模块加载程序加载并初始化OrdersModule。
在OrdersModule的初始化中,它将OrdersRepository注册到容器中。
然后,模块加载器加载CustomersModule。模块加载的顺序可以由模块元数据中的依赖项指定。
该CustomersModule构建的一个实例CustomerViewModel通过容器以解决该问题。该CustomerViewModel对一个依赖OrdersRepository(通常基于它的接口上),并指示它通过构造或财产注射。容器根据OrdersModule注册的类型在视图模型的构造中注入该依赖项。最终结果是从CustomerViewModel到OrderRepository的接口引用,而没有这些类之间的紧密耦合。
注意
用于公开OrderRespository(IOrderRepository)的接口可以驻留在单独的“共享服务”程序集或“订单服务”程序集中,该程序集仅包含公开这些服务所需的服务接口和类型。这样,CustomersModule和OrdersModule之间就没有硬依赖关系。
请注意,两个模块都依赖于依赖注入容器。在模块构建器中的模块构造期间注入该依赖性。
本节介绍在应用程序中使用模块时将遇到的常见方案。这些方案包括定义模块,注册和发现模块,加载模块,初始化模块,指定模块依赖关系,按需加载模块,在后台下载远程模块以及检测模块何时已加载。您可以在代码,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文件作为资源添加到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显示了模块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中定义模块目录时,可以指定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时,MefBootstrapper使用MefModuleManager。它扩展了ModuleManager并实现了IPartImportsSatisfiedNotification接口,以确保在MEF导入新类型时更新ModuleCatalog。
使用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发现通过ModuleCatalog或AggregateCatalog添加的模块。
注意
MEF 广泛使用Lazy
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的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和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
该模型还可以通过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
[CallerMemberName] string propertyName = null)
{...}
protected void OnPropertyChanged
Expression
{...}
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
}
如果获得对集合类的引用(例如,来自未实现INotifyCollectionChanged的其他组件或服务),则通常可以使用其中一个构造函数将该集合包装在ObservableCollection
注意
BindableBase可以在位于Prism.Mvvm NuGet包中的Microsoft.Practices.Prism.Mvvm命名空间中找到。
实现ICollectionView
上面的代码示例演示如何实现一个简单的视图模型属性,该属性返回可以通过视图中的数据绑定控件显示的项集合。由于ObservableCollection
但是,您通常需要更精细地控制项目集合在视图中的显示方式,或者在视图模型本身内跟踪用户与显示的项目集合的交互。例如,您可能需要根据视图模型中实现的表示逻辑来过滤或排序项目集合,或者您可能需要跟踪视图中当前选定的项目,以便在视图模型中实现命令可以对当前选定的项目采取行动。
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
{
// 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
{
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
{
public DelegateCommand(Action
{
...
}
}
例如,以下代码示例显示如何通过为OnSubmit和CanSubmit视图模型方法指定委托来构造表示Submit命令的DelegateCommand实例。然后,该命令通过只读属性公开给视图,该属性返回对ICommand的引用。
C#
复制
public class QuestionnaireViewModel
{
public QuestionnaireViewModel()
{
this.SubmitCommand = new DelegateCommand
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
复制
也可以使用CommandParameter属性选择性地定义命令参数。预期参数的类型在Execute和CanExecute目标方法中指定。当用户与该控件交互时,控件将自动调用目标命令,并且命令参数(如果提供)将作为参数传递给命令的Execute方法。在前面的示例中,按钮将在单击时自动调用SubmitCommand。此外,如果指定了CanExecute处理程序,则在CanExecute返回false时将自动禁用该按钮,如果返回true,将启用它。
另一种方法是使用Blend for Visual Studio 2013交互触发器和InvokeCommandAction行为。有关InvokeCommandAction行为以及将命令与事件关联的更多信息,请参阅“ 高级MVVM方案 ”中的“ 交互触发器和命令 ” 。
数据验证和错误报告
通常需要您的视图模型或模型来执行数据验证并向视图发出任何数据验证错误信号,以便用户可以采取行动纠正它们。
WPF支持管理更改绑定到视图中控件的各个属性时发生的数据验证错误。对于与控件数据绑定的单个属性,视图模型或模型可以通过拒绝传入的错误值并抛出异常来表示属性设置器中的数据验证错误。如果数据绑定上的ValidatesOnExceptions属性为true,则WPF中的数据绑定引擎将处理该异常并向用户显示存在数据验证错误的可视提示。
但是,应尽可能避免以这种方式抛出属性异常。另一种方法是在视图模型或模型类上实现IDataErrorInfo或INotifyDataErrorInfo接口。这些接口允许您的视图模型或模型对一个或多个属性值执行数据验证,并向视图返回错误消息,以便可以通知用户错误。
实现IDataErrorInfo
该IDataErrorInfo的接口提供了性能数据验证和错误报告的基本支持。它定义了两个只读属性:一个索引器属性,其属性名称为索引器参数,以及一个Error属性。两个属性都返回一个字符串值。
indexer属性允许视图模型或模型类提供特定于命名属性的错误消息。空字符串或空返回值向视图指示已更改的属性值有效。的错误属性允许视图模型或模型类,以提供对整个对象的错误消息。但请注意,WPF数据绑定引擎当前不会调用此属性。
所述IDataErrorInfo的时首先显示数据绑定属性索引器属性被访问,并且每当它随后被更改。因为为所有更改的属性调用了indexer属性,所以应该小心确保数据验证尽可能快速有效。
将视图中的控件绑定到要通过IDataErrorInfo接口验证的属性时,请将数据绑定上的ValidatesOnDataErrors属性设置为true。这将确保数据绑定引擎将请求数据绑定属性的错误信息。
XAML
复制
/>
实现INotifyDataErrorInfo
该INotifyDataErrorInfo接口更加灵活* 比* IDataErrorInfo的接口。它支持属性的多个错误,异步数据验证,以及在对象的错误状态更改时通知视图的能力。
所述INotifyDataErrorInfo接口定义了一个HasErrors属性,该属性允许视图模型,以指示用于任何性质的误差(或多个误差)是否存在,和一个GetErrors方法,其允许视图模型返回错误消息的列表的特定属性。
所述INotifyDataErrorInfo接口还限定ErrorsChanged事件。* 这通过允许视图或视图模型通过** ErrorsChanged信号为特定的属性在错误状态的变化支持异步验证场景 *事件。可以通过多种方式更改属性值,而不仅仅是通过数据绑定 - 例如,作为Web服务调用或后台计算的结果。该ErrorsChanged事件使得一旦数据验证错误已被确定视图模型告知错误的观点。
要支持INotifyDataErrorInfo,您需要维护每个属性的错误列表。Model-View-ViewModel参考实现(MVVM RI)演示了一种使用ErrorsContainer集合类来实现此目的的方法,该集合类跟踪对象中的所有验证错误。如果错误列表发生更改,它还会引发通知事件。以下代码示例显示了DomainObject(根模型对象),并使用ErrorsContainer类显示了INotifyDataErrorInfo的示例实现。
C#
复制
public abstract class DomainObject : INotifyPropertyChanged,
INotifyDataErrorInfo
{
private ErrorsContainer
new ErrorsContainer
pn => this.RaiseErrorsChanged( pn ) );
public event EventHandler
public bool HasErrors
{
get { return this.ErrorsContainer.HasErrors; }
}
public IEnumerable GetErrors( string propertyName )
{
return this.errorsContainer.GetErrors( propertyName );
}
protected void RaiseErrorsChanged( string propertyName )
{
var handler = this.ErrorsChanged;
if (handler != null)
{
handler(this, new DataErrorsChangedEventArgs(propertyName) );
}
}
...
}
建筑和电线
MVVM模式可以帮助您将UI与表示和业务逻辑和数据完全分离,因此在正确的类中实现正确的代码是有效使用MVVM模式的重要第一步。通过数据绑定和命令管理视图和视图模型类之间的交互也是需要考虑的重要方面。下一步是考虑如何在运行时实例化视图,视图模型和模型类并将它们相互关联。
注意
如果在应用程序中使用依赖项注入容器,则选择适当的策略来管理此步骤尤为重要。托管可扩展性框架(MEF)和Unity应用程序块(Unity)都提供了指定视图,视图模型和模型类之间的依赖关系以及使容器满足它们的能力。有关更高级的方案,请参阅高级MVVM方案。
通常,视图与其视图模型之间存在一对一的关系。视图和视图模型通过视图的数据上下文属性松散耦合; 这允许视图中的可视元素和行为是绑定到视图模型上的属性,命令和方法的数据。您将需要决定如何在运行时通过DataContext属性来管理视图的实例化以及查看模型类及其关联。
在构建和连接视图和视图模型时也必须小心,以确保保持松耦合。如前一节所述,视图模型理想情况下不应依赖于视图的任何特定实现。同样,理想情况下,视图应该不依赖于视图模型的任何特定实现。
注意
但是,应该注意,视图将隐式依赖于视图模型上的特定属性,命令和方法,因为它定义了数据绑定。如果视图模型未实现所需的属性,命令或方法,则数据绑定引擎将生成运行时异常,该异常将在调试期间显示在Visual Studio输出窗口中。
可以通过多种方式在运行时构建视图和视图模型并将其关联。适合您的应用程序的方法在很大程度上取决于您是首先创建视图还是视图模型,以及是以编程方式还是以声明方式创建视图模型。以下部分描述了在运行时可以创建视图和视图模型类以及相互关联的常用方法。
使用XAML创建视图模型
也许最简单的方法是视图以声明方式在XAML中实例化其对应的视图模型。构造视图时,还将构造相应的视图模型对象。您还可以在XAML中指定将视图模型设置为视图的数据上下文。
XAML
复制
创建此视图时,将自动构建MyViewModel的实例并将其设置为视图的数据上下文。此方法要求您的视图模型具有默认(无参数)构造函数。
视图的声明性构造和视图模型的分配具有以下优点:它很简单并且在诸如Microsoft Expression Blend或Microsoft Visual Studio的设计时工具中运行良好。此方法的缺点是视图具有相应视图模型类型的知识,并且视图模型类型必须具有默认构造函数。
以编程方式创建视图模型
另一种方法是视图在其构造函数中以编程方式实例化其对应的视图模型实例。然后,它可以将其设置为其数据上下文,如以下代码示例所示。
C#
复制
public MyView()
{
InitializeComponent();
this.DataContext = new MyViewModel();
}
在视图的代码隐藏中编程构造和分配视图模型的优点是它很简单,并且在Expression Blend或Visual Studio等设计时工具中运行良好。这种方法的缺点是视图需要了解相应的视图模型类型,并且需要视图代码隐藏中的代码。使用依赖注入容器(如Unity或MEF)可以帮助维护视图和视图模型之间的松散耦合。有关更多信息,请参阅管理组件之间的依赖关系。
使用视图模型定位器创建视图模型
创建视图模型实例并将其与视图关联的另一种方法是使用视图模型定位器。
Prism视图模型定位器具有AutoWireViewModel 附加属性,当设置呼叫 AutoWireViewModelChanged 架法theViewModelLocationProvider类来解决视图的视图模型。默认情况下,它使用基于约定的方法。
在Basic MVVM QuickStart中,MainWindow.xaml使用视图模型定位器来解析视图模型。
XAML
复制
...
prism:ViewModelLocator.AutoWireViewModel="True">
Prism的ViewModelLocator类有一个附加属性AutoWireViewMode l,当设置为true时,将尝试定位视图的视图模型,然后将视图的数据上下文设置为视图模型的实例。若要查找相应的视图模型,ViewModelLocationProvider首先尝试从ViewModelLocationProvider类的Register方法注册的任何映射中解析视图模型。如果使用此方法无法解析视图模型,例如,如果未创建映射,则ViewModelLocationProvider回归到基于约定的方法来解决正确的视图模型类型。此约定假定视图模型与视图类型在同一个程序集中,视图模型位于a。ViewModels子命名空间,该视图位于。查看子命名空间,该视图模型名称与视图名称对应,以“ViewModel”结尾。有关如何更改Prism视图模型定位器约定的说明,请参阅附录E:扩展Prism。
注意
ViewModelLocationProvider可以在找到Microsoft.Practices.Prism.Mvvm组件和ViewModelLocator可以在位于所述Microsoft.Practices.Prism.Mvvm.Desktop组件中找到Prism.Mvvm NuGet包。
创建定义为数据模板的视图
视图可以定义为数据模板并与视图模型类型相关联。数据模板可以定义为资源,也可以在显示视图模型的控件中内联定义。控件的“内容”是视图模型实例,数据模板用于直观地表示它。WPF将自动实例化数据模板,并在运行时将其数据上下文设置为视图模型实例。此技术是首先实例化视图模型,然后创建视图的情况的示例。
数据模板灵活轻便。UI设计人员可以使用它们轻松定义视图模型的可视化表示,而无需任何复杂的代码。数据模板仅限于不需要任何UI逻辑(代码隐藏)的视图。Microsoft Blend for Visual Studio 2013可用于可视化设计和编辑数据模板。
以下示例显示绑定到客户列表的ItemsControl。底层集合中的每个客户对象都是一个视图模型实例。客户的视图由内联数据模板定义。在以下示例中,每个客户视图模型的视图由一个StackPanel组成,其中标签和文本框控件绑定到视图模型上的Name属性。
XAML
复制
您还可以将数据模板定义为资源。以下示例显示了数据模板定义的资源,并通过StaticResource标记扩展应用于内容控件。
XAML
复制
这里,数据模板包装了一个具体的视图类型。这允许视图定义代码隐藏行为。通过这种方式,数据模板机制可用于从外部提供视图和视图模型之间的关联。虽然前面的示例显示了UserControl资源中的模板,但它通常会放在应用程序的资源中以供重用。
关键决定
当您选择使用MVVM模式构建应用程序时,您将不得不做出某些难以在以后更改的设计决策。通常,这些决策是应用程序范围的,并且它们在整个应用程序中的一致使用将提高开发人员和设 以下总结了实现MVVM模式时最重要的决策:
确定查看和查看您将使用的模型构造的方法。您需要确定您的应用程序是首先构造视图还是视图模型,以及是否使用依赖注入容器,例如Unity或MEF。您通常希望这在整个应用程序范围内保持一致。有关更多信息,请参阅本主题中的“构造和连接”部分以及“ 高级MVVM方案”中的“高级构造和连接”部分。
确定是否将视图模型中的命令作为命令方法或命令对象公开。命令方法很容易公开,可以通过视图中的行为来调用。命令对象可以巧妙地封装命令和启用/禁用逻辑,并且可以通过行为或通过ButtonBase派生控件上的Command属性调用。为了使开发人员和设计人员更容易,最好将其作为应用程序范围内的选择。有关更多信息,请参阅本主题中的“命令”一节。
确定视图模型和模型如何向视图报告错误。您的模型可以支持IDataErrorInfo或INotifyDataErrorInfo。并非所有模型都需要报告错误信息,但对于那些模型,最好为开发人员提供一致的方法。有关详细信息,请参阅本主题中的“数据验证和错误报告”部分。
确定Microsoft Blend for Visual Studio 2013设计时数据支持对您的团队是否重要。如果您将使用Blend来设计和维护UI并希望查看设计时数据,请确保您的视图和视图模型提供的构造函数没有参数,并且您的视图提供了设计时数据上下文。或者,考虑使用Microsoft Blend for Visual Studio 2013提供的设计时功能,使用设计时属性,例如d:DataContext和d:DesignSource。有关更多信息,请参阅在编写用户界面中创建设计器友好视图的准则。
上一个主题描述了如何通过将应用程序的用户界面(UI),表示逻辑和业务逻辑分成三个独立的类(视图,视图模型和模型)来实现Model-View-ViewModel(MVVM)模式的基本元素),实现这些类之间的交互(通过数据绑定,命令和数据验证接口),并实施处理构造和连接的策略。本主题描述了一些复杂的场景,并描述了MVVM模式如何支持它们。下一节将介绍如何将命令链接在一起或与子视图关联,以及如何扩展命令以支持自定义要求。
“使用依赖注入容器”(例如Unity应用程序块(Unity))或使用托管可扩展性框架(MEF)时,“高级构造和连接”一节提供了有关处理构造和连接的指导。最后一节描述了如何通过提供单元测试应用程序的视图模型和模型类以及测试行为的指导来测试MVVM应用程序。
命令提供了一种将命令的实现逻辑与其UI表示分开的方法。数据绑定或行为提供了一种方法,用于声明性地将视图中的元素与视图模型提供的命令相关联。的部分中,命令在实施MVVM模式,描述了如何命令可以作为命令对象或在视图模型命令的方法来实现,以及它们是如何可以从控制在视图中通过使用调用的内置命令由某些控件提供的属性。
WPF路由命令:应该注意的是,MVVM模式中作为命令对象或命令方法实现的命令与WPF的名为routed命令的命令的内置实现略有不同。WPF路由命令通过在UI树中的元素(特别是逻辑树)路由它们来传递命令消息。因此,命令消息在UI树中从聚焦元素向上或向下路由到显式指定的目标元素; 默认情况下,它们不会路由到UI树之外的组件,例如与视图关联的视图模型。但是,WPF路由命令可以使用视图代码隐藏中定义的命令处理程序将命令调用转发到视图模型类。
在许多情况下,视图模型定义的命令将绑定到关联视图中的控件,以便用户可以直接从视图中调用该命令。但是,在某些情况下,您可能希望能够从应用程序UI的父视图中的控件调用一个或多个视图模型上的命令。
例如,如果您的应用程序允许用户同时编辑多个项目,您可能希望允许用户使用应用程序工具栏或功能区中的按钮所代表的单个命令来保存所有项目。在这种情况下,Save All命令将调用每个项目的视图模型实例实现的每个Save命令,如下图所示。
实现SaveAll复合命令
Prism通过CompositeCommand类支持这种情况。
所述CompositeCommand类表示从多个子指令构成的指令。调用复合命令时,将依次调用其每个子命令。在需要在UI中将一组命令表示为单个命令或者要调用多个命令来实现逻辑命令的情况下,它非常有用。
例如,CompositeCommand类用于股票交易者参考实现(Stock Trader RI),以实现买入/卖出视图中的“ 全部提交”按钮所代表的SubmitAllOrders命令。当用户单击“ 全部提交”按钮时,将执行由各个买/卖交易定义的每个SubmitCommand。
该CompositeCommand类维护儿童的命令(列表DelegateCommand实例)。在执行该方法CompositeCommand类只是调用执行每个反过来子命令的方法。在CanExecute方法同样调用CanExecute每个孩子命令的方法,但是如果有孩子的命令不能执行时,CanExecute方法将返回错误。换句话说,默认情况下,只有在可以执行所有子命令时才能执行CompositeCommand。
使用RegisterCommand和UnregisterCommand方法注册或取消注册子命令。例如,在Stock Trader RI中,每个买/卖订单的提交和取消命令都使用SubmitAllOrders和CancelAllOrders复合命令进行注册,如以下代码示例所示(请参阅OrdersController类)。
C#复制
// OrdersController.cs
commandProxy.SubmitAllOrdersCommand.RegisterCommand(
orderCompositeViewModel.SubmitCommand );
commandProxy.CancelAllOrdersCommand.RegisterCommand(
orderCompositeViewModel.CancelCommand );
注意
前面的commandProxy对象提供对Submit和Cancel复合命令的实例访问,这些命令是静态定义的。有关更多信息,请参阅类文件StockTraderRICommands.cs。
通常,您的应用程序需要在应用程序的UI中显示子视图的集合,其中每个子视图将具有相应的视图模型,而该视图模型又可以实现一个或多个命令。复合命令可用于表示应用程序UI中子视图实现的命令,并有助于协调从父视图中调用它们的方式。为了支持这些场景,Prism CompositeCommand和DelegateCommand类被设计为与Prism区域一起使用。
Prism的区域(在部分中所描述,区域,在构成用户接口)提供了一种方法用于子视图与在应用程序的UI逻辑占位符相关联。它们通常用于将子视图的特定布局与其逻辑占位符及其在UI中的位置分离。区域基于附加到特定布局控件的命名占位符。下图显示了一个示例,其中每个子视图都已添加到名为EditRegion的区域,并且UI设计器已选择使用Tab控件来布局该区域内的视图。
使用Tab控件定义EditRegion
父视图级别的复合命令通常用于协调如何调用子视图级别的命令。在某些情况下,您将需要执行所有显示视图的命令,如前面所述的Save All命令示例中所示。在其他情况下,您将希望仅在活动视图上执行该命令。在这种情况下,复合命令将仅对被视为活动的视图执行子命令; 它不会在非活动的视图上执行子命令。例如,您可能希望在应用程序的工具栏或功能区上实现缩放命令,该命令仅导致当前活动项目被缩放,如下图所示。
使用Tab控件定义EditRegion
为了支持这种情况,Prism提供了IActiveAware接口。所述IActiveAware接口定义的IsActive属性,返回真时实施者是活动的,以及一个IsActiveChanged每当激活状态改变时引发事件。
您可以在子视图或视图模型上实现IActiveAware接口。它主要用于跟踪区域内子视图的活动状态。视图是否处于活动状态由区域适配器确定,该适配器协调特定区域控件内的视图。对于前面显示的Tab控件,有一个区域适配器,例如,它将当前所选选项卡中的视图设置为活动状态。
该DelegateCommand类还实现了IActiveAware接口。该CompositeCommand可以被配置为评估孩子的活动状态DelegateCommands(除CanExecute指定状态)真正的monitorCommandActivity在构造函数中的参数。当此参数设置为true时,CompositeCommand类将在确定CanExecute方法的返回值以及在Execute方法中执行子命令时考虑每个子DelegateCommand的活动状态。
当monitorCommandActivity参数为true时,CompositeCommand类会出现以下行为:
您可以使用此功能来实现前面描述的示例。通过在子视图模型上实现IActiveAware接口,当您的子视图对该区域变为活动或非活动时,将通知您。当子视图的活动状态更改时,您可以更新子命令的活动状态。然后,当用户调用缩放复合命令时,将调用活动子视图上的缩放命令。
在视图中显示项目集合时经常遇到的另一种常见情况是,您需要将集合中每个项目的UI与父视图级别(而不是项目级别)的命令相关联。
例如,在下图所示的应用程序中,视图显示ListBox控件中的项集合,用于显示每个项的数据模板定义了一个Delete按钮,允许用户从集合中删除单个项。
集合中的绑定命令
因为该视图模型实现的删除命令,面临的挑战是要连接的删除按钮在每个项目的用户界面中,对删除由视图模型实现的命令。之所以出现这种困难是因为ListBox中每个项的数据上下文引用了集合中的项而不是实现Delete命令的父视图模型。
解决此问题的一种方法是使用ElementName绑定属性将数据模板中的按钮绑定到父视图中的命令,以确保绑定相对于父控件而不是相对于数据模板。以下XAML说明了这种技术。
XAML复制
<Grid x:Name="root">
<ListBox ItemsSource="{Binding Path=Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Path=Name}"
Command="{Binding ElementName=root, Path=DataContext.DeleteCommand}" />
DataTemplate>
ListBox.ItemTemplate>
ListBox>
Grid>
数据模板中按钮控件的内容绑定到集合中项目的Name属性。但是,按钮的命令通过根元素的数据上下文绑定到Delete命令。这允许按钮在父视图级别而不是在项目级别绑定到命令。您可以使用CommandParameter属性指定要应用命令的项目,也可以实现命令以对当前所选项目进行操作(通过CollectionView)。
另一种命令方法是使用Blend for Visual Studio 2013交互触发器和InvokeCommandAction操作。
XAML复制
<Button Content="Submit" IsEnabled="{Binding CanSubmit}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction Command="{Binding SubmitCommand}"/>
i:EventTrigger>
i:Interaction.Triggers>
Button>
此方法可用于您可以附加交互触发器的任何控件。如果要将命令附加到未实现ICommandSource接口的控件,或者要在默认事件以外的事件上调用命令,则此功能特别有用。同样,如果需要为命令提供参数,可以使用CommandParameter属性。****
下面显示了如何使用配置为侦听ListBox的SelectionChanged事件的Blend EventTrigger 。发生此事件时,InvokeCommandAction将调用SelectedCommand。
XAML复制
<ListBox ItemsSource="{Binding Items}" SelectionMode="Single">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding SelectedCommand}" />
i:EventTrigger>
i:Interaction.Triggers>
ListBox>
注意
启用命令的控件与行为
支持命令的WPF控件允许您以声明方式将控件挂接到命令。当用户以特定方式与控件交互时,这些控件将调用指定的命令。例如,对于Button控件,将在用户单击按钮时调用该命令。与命令关联的此事件是固定的,无法更改。
行为还允许您以声明方式将控件连接到命令。但是,行为可以与控件引发的一系列事件相关联,并且它们可以用于在视图模型中有条件地调用关联的命令对象或命令方法。换句话说,行为可以解决许多与启用命令的控件相同的场景,并且它们可以提供更大程度的灵活性和控制。
您需要选择何时使用启用命令的控件以及何时使用行为以及要使用的行为类型。如果您希望使用单一机制将视图中的控件与视图模型中的功能相关联或者为了保持一致性,则可以考虑使用行为,即使对于本身支持命令的控件也是如此。
如果您只需要使用启用命令的控件来调用视图模型上的命令,并且如果您对调用命令的默认事件感到满意,则可能不需要行为。同样,如果您的开发人员或UI设计人员不使用Blend for Visual Studio 2013,您可能会支持启用命令的控件(或自定义附加行为),因为Blend行为需要额外的语法。
当您需要调用命令以响应位于视图中的控件引发的事件时,您可以使用Prism的InvokeCommandAction。Prism的InvokeCommandAction与Blend SDK中的同名类有两种不同。首先,Prism InvokeCommandAction根据命令的CanExecute方法的返回值更新关联控件的启用状态。其次,Prism InvokeCommandAction使用从父触发器传递给它的EventArgs参数,如果未设置CommandParameter,则将其传递给关联的命令。
有时你需要一个参数传递给来自父触发,如命令的EventArgs从EventTrigger。在这种情况下,您不能使用Blend的InvokeCommandAction操作。
在下面的代码中,您可以看到Prism的InvokeCommandAction具有一个名为TriggerParameterPath的属性,该属性用于指定作为命令参数传递的参数的成员(可能是嵌套的)。在以下示例中,SelectionChanged EventArgs 的AddedItems属性将传递给SelectedCommand命令。
XAML复制
<ListBox Grid.Row="1" Margin="5" ItemsSource="{Binding Items}" SelectionMode="Single">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<prism:InvokeCommandAction Command="{Binding SelectedCommand}" TriggerParameterPath="AddedItems" />
i:EventTrigger>
i:Interaction.Triggers>
ListBox>
您的视图模型通常需要与应用程序中的服务和组件进行交互,这些服务和组件是异步通信而不是同步通信。如果您通过网络与Web服务或其他资源交互,或者您的应用程序使用后台任务执行计算或I / O,则尤其如此。异步执行这些操作可确保您的应用程序保持响应,这对于提供良好的用户体验至关重要。
当用户启动异步请求或后台任务时,很难预测响应何时到达(或者即使它将到达),并且通常很难预测它将返回什么线程。因为UI只能在UI线程中更新,所以通常需要通过在UI线程上调度请求来更新UI。
在与Web服务或其他远程访问技术交互时,您将经常遇到IAsyncResult模式。在此模式中,您可以使用BeginGetQuestionnaire和EndGetQuestionnaire等方法,而不是调用GetQuestionnaire等方法。要启动异步调用,请调用BeginGetQuestionnaire。要在调用目标方法时获取结果或确定是否存在异常,请在调用完成时调用EndGetQuestionnaire。
要确定何时调用EndGetQuestionnaire,您可以轮询完成或(最好)在调用BeginGetQuestionnaire期间指定回调。使用回调方法,当目标方法的执行完成时,将调用您的回调方法,允许您从那里调用EndGetQuestionnaire,如此处所示。
C#复制
IAsyncResult asyncResult = this.service.BeginGetQuestionnaire(GetQuestionnaireCompleted, null // object state, not used in this example);
private void GetQuestionnaireCompleted(IAsyncResult result)
{
try
{
questionnaire = this.service.EndGetQuestionnaire(ar);
}
catch (Exception ex)
{
// Do something to report the error.
}
}
需要注意的是,在调用End方法(在本例中为EndGetQuestionnaire)时,将引发在执行请求期间发生的任何异常。您的应用程序必须处理这些,并且可能需要通过UI以线程安全的方式报告它们。如果您不处理这些,线程将结束,您将无法处理结果。
由于响应通常不在UI线程上,如果您计划修改任何会影响UI状态的内容,则需要使用线程Dispatcher或SynchronizationContext对象将响应分派给UI线程。在WPF中,您通常会使用调度程序。
在下面的代码示例中,异步检索Questionnaire对象,然后将其设置为QuestionnaireView的数据上下文。您可以使用调度程序的CheckAccess方法来查看您是否在UI线程上。如果不是,则需要使用BeginInvoke方法在UI线程上执行请求。
C#复制
var dispatcher = System.Windows.Deployment.Current.Dispatcher;
if (dispatcher.CheckAccess())
{
QuestionnaireView.DataContext = questionnaire;
}
else
{
dispatcher.BeginInvoke(
() => { Questionnaire.DataContext = questionnaire; });
}
Model-View-ViewModel参考实现(MVVM RI)显示了如何使用类似于前面示例的基于IAsyncResult的服务接口的示例。它还包装服务,为消费者提供更简单的回调机制,并处理调用者对调用者线程的调度。例如,以下代码示例显示了问卷的检索。
C#复制
this.questionnaireRepository.GetQuestionnaireAsync(
(result) =>
{
this.Questionnaire = result.Result;
});
返回的结果对象将包装检索到的结果以及可能发生的错误。以下代码示例显示了如何评估错误。
C#复制
this.questionnaireRepository.GetQuestionnaireAsync(
(result) =>
{
if (result.Error == null) {
this.Questionnaire = result.Result;
...
}
else
{
// Handle error.
}
})
通常,应用程序需要在继续操作之前通知用户事件的发生或要求确认。这些交互通常是简短的交互,旨在简单地告知应用程序应用程序的更改或从中获取简单的响应。这些交互中的一些可以对用户呈现模态,例如当显示对话框或消息框时,或者它们对于用户可能看起来是非模态的,例如当显示Toast通知或弹出窗口时。
在这些情况下,有多种方法可以与用户进行交互,但是在基于MVVM的应用程序中以保持关注点清晰分离的方式实现它们可能具有挑战性。例如,在非MVVM应用程序中,您经常在UI的代码隐藏文件中使用MessageBox类来简单地提示用户进行响应。在MVVM应用程序中,这不合适,因为它会破坏视图和视图模型之间关注点的分离。
就MVVM模式而言,视图模型负责启动与用户的交互以及消费和处理任何响应,而视图负责使用适当的任何用户体验实际管理与用户的交互。保持视图模型中实现的表示逻辑与视图实现的用户体验之间的关注点分离有助于提高可测试性和灵活性。
在MVVM模式中实现这些类型的用户交互有两种常用方法。一种方法是实现视图模型可以使用的服务来启动与用户的交互,从而保持其在视图的实现上的独立性。另一种方法使用视图模型引发的事件来表达与用户交互的意图,以及视图中绑定到这些事件并管理交互的可视方面的组件。以下各节将介绍这些方法中的每一种。
In this approach, the view model relies on an interaction service component to initiate interaction with the user via a message box. This approach supports a clean separation of concerns and testability by encapsulating the visual implementation of the interaction in a separate service component. Typically, the view model has a dependency on an interaction service interface. It frequently acquires a reference to the interaction service's implementation via dependency injection or a service locator.
在视图模型引用交互服务之后,它可以在必要时以编程方式请求与用户的交互。交互服务实现交互的可视方面,如下图所示。根据用户界面的实现要求,在视图模型中使用接口引用允许使用不同的实现。例如,可以提供WPF的交互服务的实现,允许更多地重用应用程序的表示逻辑。
使用交互服务与用户交互
模态交互(例如,在执行可以继续之前向用户呈现MessageBox或模态弹出窗口以获取特定响应的位置)可以使用阻塞方法调用以同步方式实现,如以下代码示例所示。
C#复制
var result =
interactionService.ShowMessageBox(
"Are you sure you want to cancel this operation?",
"Confirm",
MessageBoxButton.OK );
if (result == MessageBoxResult.Yes)
{
CancelRequest();
}
然而,这种方法的一个缺点是它迫使同步编程模型。另一种异步实现允许视图模型提供回调以在完成交互时执行。以下代码说明了这种方法。
C#复制
interactionService.ShowMessageBox(
"Are you sure you want to cancel this operation?",
"Confirm",
MessageBoxButton.OK,
result =>
{
if (result == MessageBoxResult.Yes)
{
CancelRequest();
}
});
通过允许实现模态和非模态交互,异步方法在实现交互服务时提供了更大的灵活性。例如,在WPF中,MessageBox类可用于实现与用户的真正模态交互。
在MVVM模式中实现简单用户交互的另一种方法是允许视图模型通过与视图中的行为耦合的交互请求对象直接向视图本身发出交互请求。交互请求对象封装交互请求的详细信息及其响应,并通过事件与视图进行通信。视图订阅这些事件以启动交互的用户体验部分。视图通常会将交互的用户体验封装在与视图模型提供的交互请求对象数据绑定的行为中,如下图所示。
使用交互请求对象与用户交互
这种方法提供了一种简单但灵活的机制,可以保持视图模型和视图之间的清晰分离 - 它允许视图模型封装应用程序的表示逻辑,包括任何所需的用户交互,同时允许视图完全封装视觉互动的各个方面。可以轻松测试视图模型的实现,包括通过视图与用户的预期交互,并且UI设计器在通过使用封装不同用户的不同行为选择如何在视图中实现交互时具有很大的灵活性互动的经验。
此方法与MVVM模式一致,使视图能够反映它在视图模型上观察到的状态更改,并使用双向数据绑定来实现两者之间的数据通信。在交互请求对象中封装交互的非可视元素,以及使用相应的行为来管理交互的可视元素,与命令对象和命令行为的使用方式非常相似。
这种方法是Prism采用的方法。Prism Library通过IInteractionRequest接口和InteractionRequest
所述InteractionRequest
C#复制
public interface IInteractionRequest
{
event EventHandler Raised;
}
public class InteractionRequest<T> : IInteractionRequest
where T : INotification
{
public event EventHandler Raised;
public void Raise(T context)
{
this.Raise(context, c => { });
}
public void Raise(T context, Action callback)
{
var handler = this.Raised;
if (handler != null)
{
handler(
this,
new InteractionRequestedEventArgs(
context,
() => { if (callback != null) callback(context); } ));
}
}
}
Prism提供预定义的上下文类,支持常见的交互请求场景。该INotification接口用于所有上下文对象。当交互请求用于通知用户应用程序中的重要事件时,使用它。它提供了两个属性 - 标题和内容 - 将显示给用户。通常,通知是单向的,因此预计用户不会在交互期间更改这些值。该通知类是这个接口的默认实现。
所述IConfirmation接口扩展了INotification接口和增加了第三财产而证实 -其用于表示用户已确认或拒绝该操作。在确认类,所提供的IConfirmation实现,用于实现MessageBox的用户想获得一个是从用户/无应答风格的交互。您可以定义实现INotification接口的自定义上下文类,以封装支持交互所需的任何数据和状态。
要使用InteractionRequest
C#复制
public InteractionRequestViewModel()
{
this.ConfirmationRequest = new InteractionRequest();
…
// Commands for each of the buttons. Each of these raise a different interaction request.
this.RaiseConfirmationCommand = new DelegateCommand(this.RaiseConfirmation);
…
}
public InteractionRequest ConfirmationRequest { get; private set; }
private void RaiseConfirmation()
{
this.ConfirmationRequest.Raise(
new Confirmation { Content = "Confirmation Message", Title = "Confirmation" },
c => { InteractionResultMessage = c.Confirmed ? "The user accepted." : "The user cancelled."; });
}
}
的交互性快速入门示出了如何IInteractionRequest接口和InteractionRequest
由于交互请求对象表示逻辑交互,因此在视图中定义了交互的确切用户体验。行为通常用于封装用户体验以进行交互; 这允许UI设计者选择适当的行为并将其绑定到视图模型上的交互请求对象。
必须设置视图以检测交互请求事件,然后为请求显示适当的可视显示。触发器用于在引发特定事件时启动操作。
Blend提供的标准EventTrigger可用于通过绑定到视图模型公开的交互请求对象来监视交互请求事件。但是,Prism Library定义了一个名为InteractionRequestTrigger的自定义EventTrigger,它自动连接到IInteractionRequest接口的相应Raised事件。这减少了所需的可扩展应用程序标记语言(XAML)的数量,并减少了无意中输入错误事件名称的可能性。
引发事件后,InteractionRequestTrigger将调用指定的操作。对于WPF,Prism Library提供PopupWindowAction类,该类向用户显示弹出窗口。显示窗口时,其数据上下文设置为交互请求的上下文参数。使用PopupWindowAction类的WindowContent属性,可以指定将在弹出窗口中显示的视图。弹出窗口的标题绑定到上下文对象的Title属性。
注意
默认情况下,PopupWindowAction类显示的特定类型的弹出窗口取决于上下文对象的类型。对于Notification上下文对象,将显示DefaultNotificationWindow,而对于Confirmation上下文对象,将显示DefaultConfirmationWindow。该DefaultNotificationWindow显示一个简单的弹出窗口显示通知,而DefaultConfirmationWindow还包括接受和取消按钮来捕获用户的响应。您可以通过使用WindowContent指定自定义弹出窗口来覆盖此行为PopupWindowAction类的属性。
以下示例显示了如何使用InteractionRequestTrigger和PopupWindowAction在Interactivity QuickStart中向用户显示确认弹出窗口。
XAML复制
<i:Interaction.Triggers>
<prism:InteractionRequestTrigger SourceObject="{Binding ConfirmationRequest, Mode=OneWay}">
<prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True"/>
prism:InteractionRequestTrigger>
i:Interaction.Triggers>
注意
该PopupWindowAction有三个重要特性,IsModal,其设置弹出时设置为true,模态; CenterOverAssociatedObject,当设置为true时,显示以父窗口为中心的弹出窗口。最后,未指定WindowContent属性,因此将显示DefaultConfirmationWindow。
所述PopupWindowAction设置通知对象作为的数据上下文DefaultNotificationWindow,它显示内容的属性的通知对象。在用户关闭弹出窗口后,通过回调方法将上下文对象与任何更新的值一起传递回视图模型。在Interactivity QuickStart的确认示例中,DefaultConfirmationWindow负责在单击“ 确定”按钮时将提供的Confirmation对象上的Confirmed属性设置为true。
可以定义不同的触发器和动作以支持其他交互机制。Prism InteractionRequestTrigger和PopupWindowAction类的实现可以用作开发自己的触发器和操作的基础。
要成功实现MVVM模式,您需要完全理解视图,模型和视图模型类的职责,以便您可以在正确的类中实现应用程序的代码。实现正确的模式以允许这些类进行交互(通过数据绑定,命令,交互请求等)也是一个重要的要求。最后一步是考虑如何在运行时实例化视图,视图模型和模型类并将它们相互关联。
如果在应用程序中使用依赖项注入容器,则选择适当的策略来管理此步骤尤为重要。托管可扩展性框架(MEF)和Unity应用程序块(Unity)都提供了指定视图,视图模型和模型类之间的依赖关系以及在运行时由容器实现它们的能力。
通常,您将视图模型定义为视图的依赖项,以便在构造视图时(使用容器)它自动实例化所需的视图模型。反过来,视图模型所依赖的任何组件或服务也将由容器实例化。成功实例化视图模型后,视图会将其设置为其数据上下文。
使用MEF,您可以使用import属性指定视图对视图模型的依赖性,并且可以指定要通过导出属性实例化的具体视图模型类型。您可以通过属性或构造函数参数将视图模型导入视图。
例如,StockTrader参考实现中的Shell视图声明了视图模型的只写属性以及导入属性。实例化视图时,MEF会创建相应导出视图模型的实例并设置属性值。属性setter将视图模型指定为视图的数据上下文,如此处所示。
C#复制
[Import]
ShellViewModel ViewModel
{
set { this.DataContext = value; }
}
定义并导出视图模型,如此处所示。
C#复制
[Export]
public class ShellViewModel : BindableBase
{
...
}
另一种方法是在视图上定义导入构造函数,如此处所示。
C#复制
public Shell()
{
InitializeComponent();
}
[ImportingConstructor]
public Shell(ShellViewModel viewModel) : this()
{
this.DataContext = viewModel;
}
然后,视图模型将由MEF实例化,并作为参数传递给视图的构造函数。
注意
您可以在MEF和Unity中使用属性注入或构造函数注入; 但是,您可能会发现属性注入更简单,因为您不必维护两个构造函数。设计时工具(如Visual Studio和Expression Blend)要求控件具有默认的无参数构造函数,以便在设计器中显示它们。您定义的任何其他构造函数应确保调用默认构造函数,以便可以通过InitializeComponent方法正确初始化视图。
使用Unity作为依赖注入容器与使用MEF类似,并且支持基于属性和基于构造函数的注入。主要区别在于通常不会在运行时隐式发现类型; 相反,他们必须在容器中注册。
通常,您在视图模型上定义接口,以便视图模型的特定具体类型可以与视图分离。例如,视图可以通过构造函数参数定义其对视图模型的依赖性,如此处所示。
C#复制
public Shell()
{
InitializeComponent();
}
public Shell(ShellViewModel viewModel)
: this()
{
this.DataContext = viewModel;
}
注意
默认的无参数构造函数是允许视图在设计时工具中工作所必需的,例如Visual Studio和Blend for Visual Studio 2013。
或者,您可以在视图上定义只写视图模型属性,如此处所示。Unity将实例化所需的视图模型,并在实例化视图后调用属性setter。
C#复制
public Shell()
{
InitializeComponent();
}
[Dependency]
public ShellViewModel ViewModel
{
set { this.DataContext = value; }
}
视图模型类型已在Unity容器中注册,如此处所示。
C#复制
IUnityContainer container;
container.RegisterType();
然后可以通过容器实例化视图,如此处所示。
C#复制
IUnityContainer container;
var view = container.Resolve();
通常,您会发现定义控制器或服务类以协调视图的实例化和视图模型类很有用。此方法可以与依赖项注入容器(如MEF或Unity)一起使用,或者在视图显式创建其所需的视图模型时使用。
在您的应用程序中实现导航时,此方法特别有用。在这种情况下,控制器与UI中的占位符控件或区域相关联,并且它协调视图的构造和放置到该占位符或区域中。
例如,服务类可用于使用容器构建视图并在主页面中显示它们。在此示例中,视图由视图名称指定。通过在UI服务上调用ShowView方法启动导航,如此简单示例所示。
C#复制
private void NavigateToQuestionnaireList()
{
// Ask the UI service to go to the "questionnaire list" view.
this.uiService.ShowView(ViewNames.QuestionnaireTemplatesList);
}
UI服务与应用程序的UI中的占位符控件相关联; 它封装了所需视图的创建,并在UI中协调其外观。所述ShowView所述的UIService创建(使得其视图模型和其他依赖可以实现)通过所述容器中的视图的一个实例,并然后显示它在适当的位置,如下所示。
C#复制
public void ShowView(string viewName)
{
var view = this.ViewFactory.GetView(viewName);
this.MainWindow.CurrentView = view;
}
注意
Prism为区域内的导航提供广泛的支持。区域导航使用与前一种方法非常相似的机制,区域管理器负责协调特定区域中视图的实例化和放置。有关更多信息,请参阅导航中基于视图的导航部分。
从MVVM应用程序测试模型和视图模型与测试任何其他类相同,并且可以使用相同的工具和技术 - 例如单元测试和模拟框架。但是,有一些测试模式是典型的模型和视图模型类,可以从标准测试技术和测试助手类中受益。
实现INotifyPropertyChanged接口允许视图对模型和视图模型中发生的更改做出反应。这些更改不仅限于控件中显示的域数据; 它们还用于控制视图,例如视图模型状态,可以启动动画或禁用控件。
可以通过将事件处理程序附加到PropertyChanged事件并检查在为属性设置新值之后是否引发事件来测试可以由测试代码直接更新的属性。辅助类(如PropertyChangeTracker类)可用于附加处理程序并收集结果; 这可以避免编写测试时的重复性任务。以下代码示例显示了使用此类助手类的测试。
C#复制
var changeTracker = new PropertyChangeTracker(viewModel);
viewModel.CurrentState = "newState";
CollectionAssert.Contains(changeTracker.ChangedProperties, "CurrentState");
作为保证INotifyPropertyChanged接口实现的代码生成过程的结果的属性(例如模型设计者生成的代码中的属性)通常不需要进行测试。
当测试代码无法设置属性时 - 例如具有非公共设置器的属性或只读,计算属性 - 测试代码需要激发测试对象导致属性及其相应通知的更改。但是,测试的结构与更简单的情况相同,如下面的代码示例所示,其中模型对象的更改会导致视图模型中的属性发生更改。
C#复制
var changeTracker = new PropertyChangeTracker(viewModel);
var question = viewModel.Questions.First() as OpenQuestionViewModel;
question.Question.Response = "some text";
CollectionAssert.Contains(changeTracker.ChangedProperties, "UnansweredQuestions");
实现INotifyPropertyChanged接口时,允许对象使用null或空字符串作为已更改的属性名称引发PropertyChanged事件,以指示对象中的所有属性可能已更改。这些案例可以像通知各个属性名称的案例一样进行测试。
有几种机制可用于使绑定能够执行输入验证,例如在设置属性时抛出异常,实现IDataErrorInfo接口以及实现INotifyDataErrorInfo接口。实现INotifyDataErrorInfo接口允许更复杂,因为它支持指示每个属性的多个错误并执行异步和跨属性验证; 因此,它也需要最多的测试。
测试INotifyDataErrorInfo实现有两个方面:测试验证规则是否正确实现,并测试接口实现的要求,例如当GetErrors方法的结果不同时引发ErrorsChanged事件。
验证逻辑通常很容易测试,因为它通常是一个自包含的过程,其输出取决于输入。对于与验证规则关联的每个属性,应该对使用有效值,无效值,边界值等的验证属性名称调用GetErrors方法的结果进行测试。如果共享验证逻辑,就像使用数据注释的验证属性以声明方式表示验证规则一样,更详尽的测试可以集中在共享验证逻辑上。另一方面,必须彻底测试自定义验证规则。
C#复制
// Invalid case
var notifyErrorInfo = (INotifyDataErrorInfo)question;
question.Response = -15;
Assert.IsTrue(notifyErrorInfo.GetErrors("Response").Cast().Any());
// Valid case
var notifyErrorInfo = (INotifyDataErrorInfo)question;
question.Response = 15;
Assert.IsFalse(notifyErrorInfo.GetErrors("Response").Cast().Any());
跨属性验证规则遵循相同的模式,通常需要更多测试来适应不同属性的值组合。
除了为GetErrors方法生成正确的值之外,INotifyDataErrorInfo接口的实现必须确保正确引发ErrorsChanged事件,例如GetErrors的结果不同时。此外,HasErrors属性必须反映实现该接口的对象的整体错误状态。
实现INotifyDataErrorInfo接口没有强制方法。但是,依赖于累积验证错误并执行必要通知的对象的实现通常是首选,因为它们更易于测试。这是因为没有必要验证每个验证属性上的每个验证规则是否满足INotifyDataErrorInfo接口的所有成员的要求(当然,因为错误管理对象已经过适当测试)。
测试接口要求至少应包括以下验证:
在测试INotifyPropertyChanged接口的实现时,辅助类(例如MVVM示例项目中的NotifyDataErrorInfoTestHelper类)通常通过处理重复的内务操作和标准检查来更轻松地为INotifyDataErrorInfo接口的实现编写测试。在实现接口时,它们特别有用,而不依赖于某种可重用的错误管理器。以下代码示例显示了此类型的帮助程序类。
C#复制
var helper =
new NotifyDataErrorInfoTestHelperint
?>(
question,
q => q.Response);
helper.ValidatePropertyChange(
6,
NotifyDataErrorInfoBehavior.Nothing);
helper.ValidatePropertyChange(
20,
NotifyDataErrorInfoBehavior.FiresErrorsChanged
| NotifyDataErrorInfoBehavior.HasErrors
| NotifyDataErrorInfoBehavior.HasErrorsForProperty);
helper.ValidatePropertyChange(
null,
NotifyDataErrorInfoBehavior.FiresErrorsChanged
| NotifyDataErrorInfoBehavior.HasErrors
| NotifyDataErrorInfoBehavior.HasErrorsForProperty);
helper.ValidatePropertyChange(
2,
NotifyDataErrorInfoBehavior.FiresErrorsChanged);
在实现MVVM模式时,视图模型通常会异步调用服务上的操作。对调用这些操作的代码的测试通常使用模拟或存根作为实际服务的替代
用于实现异步操作的标准模式提供了有关线程的不同保证,在该线程中发生有关操作状态的通知。虽然基于事件的异步设计模式保证在适合应用程序的线程上调用事件的处理程序,但IAsyncResult设计模式不提供任何此类保证,强制发起调用的视图模型代码以确保任何更改会影响视图发布到UI线程。
Dealing with threading concerns requires more complicated, and, therefore, usually harder to test, code. It also usually requires the tests themselves to be asynchronous. When notifications are guaranteed to occur in the UI thread, either because the standard event-based asynchronous pattern is used or because view models rely on a service access layer to marshal notifications to the appropriate thread, tests can be simplified and can essentially play the role of a "dispatcher for the UI thread."
服务被模拟的方式取决于用于实现其操作的异步事件模式。如果使用基于方法的模式,则使用标准模拟框架创建的服务接口的模拟通常就足够了,但如果使用基于事件的模式,则基于实现添加和删除处理程序的方法的自定义类进行模拟对于服务事件通常是首选。
以下代码示例显示了对使用mocks for services在UI线程中通知的异步操作成功完成时的相应行为的测试。在此示例中,测试代码在进行异步服务调用时捕获视图模型提供的回调。然后,测试通过调用回调来模拟测试后期调用的完成。此方法允许测试使用异步服务的组件,而不会使测试异步。
C#复制
questionnaireRepositoryMock
.Setup(
r =>
r.SubmitQuestionnaireAsync(
It.IsAny(),
It.IsAny>()))
.Callback>(
(q, a) => callback = a);
uiServiceMock
.Setup(svc => svc.ShowView(ViewNames.QuestionnaireTemplatesList))
.Callback<string>(viewName => requestedViewName = viewName);
submitResultMock
.Setup(sr => sr.Error)
.Returns(null);
CompleteQuestionnaire(viewModel);
viewModel.Submit();
// Simulate callback posted to the UI thread.
callback(submitResultMock.Object);
// Check expected behavior – request to navigate to the list view.
Assert.AreEqual(ViewNames.QuestionnaireTemplatesList, requestedViewName);
注意
使用此测试方法仅执行被测对象的功能; 它不测试代码是否是线程安全的。
复合应用程序用户界面(UI)由松散耦合的可视组件组成,这些组件称为视图,通常包含在应用程序模块中,但它们并非必须如此。如果将应用程序划分为模块,则需要一些方法来松散地组合UI,但即使视图不在模块中,您也可以选择使用此方法。对用户而言,该应用程序提供了无缝的用户体验,并提供了完全集成的应用程序。
要构建UI,您需要一个体系结构,允许您创建由在运行时生成的松散耦合的可视元素组成的布局。此外,该体系结构应该为这些可视元素提供以松散耦合方式进行通信的策略。
可以使用以下范例之一构建应用程序UI:
表单的所有必需控件都包含在单个可扩展应用程序标记语言(XAML)文件中,在设计时组成表单。
表单的逻辑区域被分成不同的部分,通常是用户控件。部件由表单引用,表单在设计时组成。
表单的逻辑区域被分成不同的部分,通常是用户控件。表单中的部分未知,并在运行时动态添加到表单中。使用此方法的应用程序称为使用UI组合模式的复合应用程序。
股票交易者参考实施(股票交易者RI)是通过将来自不同模块的多个视图加载到由shell公开的区域组成的,如下图所示。
股票交易者RI地区和视图
UI布局概念
复合应用程序中的根对象称为shell。shell充当应用程序的母版页。shell包含一个或多个区域。区域是将在运行时加载的内容的占位符。区域附加到UI元素,例如ContentControl,ItemsControl,TabControl或自定义控件,并管理UI元素的内容。区域内容可以自动或按需加载,具体取决于应用程序要求。
通常,区域的内容是视图。视图将您希望保留的UI的一部分封装为尽可能与应用程序的其他部分分离。您可以将视图定义为用户控件,数据模板甚至自定义控件。
区域管理视图的显示和布局。区域可以通过其名称以分离的方式访问,并支持动态添加或删除视图。区域附加到主机控件。将区域视为动态加载视图的容器。
以下部分介绍了复合应用程序开发的高级核心概念。
贝壳
shell是包含主UI内容的应用程序根对象。在Windows Presentation Foundation(WPF)应用程序中,shell是Window对象。
shell扮演主页的角色,为应用程序提供布局结构。shell包含一个或多个命名区域,其中模块可以指定将显示的视图。它还可以定义某些顶级UI元素,例如背景,主菜单和工具栏。
shell定义了应用程序的整体外观。它可以定义在shell布局本身中存在和可见的样式和边框,还可以定义将应用于插入到shell中的视图的样式,模板和主题。
通常,shell是WPF应用程序项目的一部分。包含shell的程序集可能引用也可能不引用包含要在shell的区域中加载的视图的程序集。
查看
视图是复合应用程序中UI构造的主要单元。您可以将视图定义为用户控件,页面,数据模板或自定义控件。视图将您希望保留的UI的一部分封装为尽可能与应用程序的其他部分分离。您可以根据封装或功能选择视图中的内容,也可以选择将某些内容定义为视图,因为您的应用程序中将包含该视图的多个实例。
由于WPF的内容模型,定义视图所需的Prism库没有特定的内容。定义视图的最简单方法是定义用户控件。要向UI添加视图,您只需要一种方法来构建它并将其添加到容器中。WPF提供了执行此操作的机制。Prism Library添加了定义可在运行时动态添加视图的区域的功能。
综合视图
支持特定功能的视图可能会变得复杂。在这种情况下,您可能希望将视图划分为多个子视图,并让父视图句柄通过将子视图用作部件来构建自身。应用程序可能会在设计时静态地执行此操作,或者它可能支持让模块在运行时通过包含的区域添加子视图。如果某个视图未在单个视图类中完全定义,则可以将其称为复合视图。在许多情况下,复合视图负责构建子视图和协调它们之间的交互。您可以使用Prism Library命令和事件聚合器设计从其兄弟视图及其父组合视图中更松散耦合的子视图。
视图和设计模式
虽然Prism Library不要求您使用它们,但在实现视图时应考虑使用多种UI设计模式之一。Stock Trader RI和QuickStart演示了Model-View-ViewModel(MVVM)模式,作为在视图布局和视图逻辑之间实现清晰分离的一种方式。
建议使用MVVM UI设计模式,因为它非常适合Microsoft XAML平台。这些平台的依赖属性系统和富数据绑定堆栈使视图和视图模型能够以松散耦合的方式进行通信。
将逻辑与视图分离对于可测试性和可维护性非常重要,并且它改进了开发人员 - 设计人员的工作流程。
如果使用用户控件或自定义控件创建视图并将所有逻辑放在代码隐藏文件中,则您的视图可能难以测试,因为您必须创建视图实例以对逻辑进行单元测试。如果视图派生或依赖于将WPF组件作为其执行上下文的一部分运行,则这是一个问题。为了确保您可以在没有这些依赖关系的情况下单独测试视图逻辑,您需要能够创建视图的模型以删除执行上下文的依赖关系,这需要视图和逻辑的单独类。
如果将视图定义为数据模板,则没有与视图本身关联的代码。因此,您必须将关联逻辑放在其他位置。逻辑与可测试性所需的布局清晰分离也有助于使视图更易于维护。
注意:单元测试和UI自动化测试是两种不同类型的测试,具有不同的覆盖范围。
单元测试最佳实践建议单独测试对象。要实现对象隔离,每个外部依赖项都需要一个模型或存根。然后针对该对象运行粒度单元测试。
UI自动化测试运行应用程序,将手势应用于UI,然后测试预期结果。此类测试验证UI元素是否正确连接到应用程序逻辑。
将逻辑与视图分开可以清晰地分离关注点。除了可测试性考虑因素之外,这种分离使设计人员能够独立于开发人员在UI上工作。有关MVVM的更多信息,请参阅实现MVVM模式。
命令,UI触发器,操作和行为
在代码隐藏文件中使用其逻辑实现视图时,可以向服务UI交互添加事件处理程序。但是,使用MVVM时,视图模型无法直接处理UI引发的事件。要将UI手势事件路由到视图模型,您可以使用命令或UI触发器,操作和行为。
命令
命令将语义和从执行命令的逻辑调用命令的对象分开。内置命令是指示操作是否可用的能力。UI中的命令是绑定到视图模型上的ICommand属性的数据。有关命令的详细信息,请参阅命令在实现MVVM模式。
UI触发器,操作和行为
触发器,操作和行为是Microsoft.Expression.Interactivity命名空间的一部分,随Blend for Visual Studio 2013一起提供。它们也是Blend SDK的一部分。触发器,操作和行为提供了一个用于处理UI事件或命令的综合API,然后将它们路由到DataContext公开的ICommand属性方法。更多有关UI触发,动作和行为,见第调用从在视图中查看和调用命令的方法命令对象实施MVVM模式和交互的触发器和事件,以命令的高级MVVM方案。
用户互动
用户交互是应用程序向用户呈现的交互。这些交互通常是呈现给用户的弹出窗口。在MVVM场景中,可以从视图或视图模型生成这些用户交互。Prism为视图模型需要请求用户交互时的情况提供InteractionRequests和InteractionRequestTriggers,为触发指定事件时视图需要调用命令时提供InvokeCommandAction操作。
有关用户交互,示例以及如何使用它们的更多信息,请参阅Interactivity QuickStart。
数据绑定
数据绑定是XAML平台最重要的框架功能之一。要在XAML平台上成功开发应用程序,您需要充分了解数据绑定。
数据绑定充分利用了依赖属性系统提供的内在更改通知。当与INotifyPropertyChanged接口的公共语言运行时(CLR)类实现结合使用时,更改通知将启用参与数据绑定的目标和源对象之间的无代码交互。
通过使用值转换器将一种类型转换为另一种类型,数据绑定可以使不同的目标和源类型与数据绑定。数据绑定在其管道中有多个验证挂钩,可用于验证用户输入。
强烈建议您阅读MSDN上的依赖项属性概述和数据绑定概述主题。完全了解这两个主题对于在Microsoft XAML平台上成功开发应用程序至关重要。有关数据绑定的更多信息,请参阅数据绑定在实现MVVM模式。
地区
通过区域管理器,区域和区域适配器在Prism库中启用区域。接下来的部分将介绍它们如何协同工作。
区域经理
该RegionManager类负责创建和维护区域的集合主机控制。该RegionManager使用一个新的区域与主机控制相关联的特定控制适配器。下图显示了RegionManager设置的区域,控件和适配器之间的关系。
区域,控件和适配器关系
该RegionManager可以在代码中创建或XAML地区。所述RegionManager.RegionName附加属性是用来通过将附加属性到主机控制以在XAML的区域。
应用程序可以包含RegionManager的一个或多个实例。您可以指定要在其中注册区域的RegionManager实例。如果要在可视树中移动控件并且不希望在删除附加属性值时清除区域,则此选项非常有用。
所述RegionManager提供RegionContext附* 属性 *允许其区域共享数据。
区域实施
区域是实现IRegion接口的类。术语区域表示可以保存UI中呈现的动态数据的容器。区域允许Prism库将包含在模块中的动态内容放置在UI容器中的预定义占位符中。
区域可以包含任何类型的UI内容。模块可以包含作为用户控件呈现的UI内容,与数据模板相关联的数据类型,自定义控件或这些的任何组合。这使您可以定义UI区域的外观,然后让模块将内容放在这些预定区域中。
区域可以包含零个或多个项目。根据区域管理的主机控件的类型,可以看到一个或多个项目。例如,ContentControl只能显示单个对象。但是,它所在的区域可以包含许多项目,ItemsControl可以显示多个项目。这允许区域中的每个项目在UI中可见。
在下图中,Stock Trader RI shell包含四个区域:MainRegion,MainToolbarRegion,ResearchRegion和ActionRegion。这些区域由应用程序中的各种模块填充 - 内容可以随时更改。
股票交易者RI地区
模块用户控制到区域映射
要演示模块和内容如何与区域关联,请参阅下图。它显示了WatchModule和NewsModule与shell中相应区域的关联。
所述MainRegion包含WatchListView用户控制,这被包含在WatchModule。所述ResearchRegion还包含ArticleView用户控制,这被包含在NewsModule。
在使用Prism Library创建的应用程序中,这样的映射将成为设计过程的一部分,因为设计人员和开发人员使用它们来确定在特定区域中建议的内容。这允许设计人员确定所需的总空间以及必须添加的任何其他项目,以确保内容在可允许的空间中可见。
模块用户控制到区域映射
默认区域功能
虽然您不需要完全理解区域实现以使用它们,但了解控件和区域如何关联以及默认区域功能可能很有用:例如,区域如何定位和实例化视图,以及如何在视图时通知视图是活动视图,还是视图生命周期如何与激活相关联。
以下部分描述了区域适配器和区域行为。
区域适配器
要将UI控件公开为区域,它必须具有区域适配器。区域适配器负责创建区域并将其与控件相关联。这允许您使用IRegion接口以一致的方式管理UI控件内容。每个区域适配器都适应特定类型的UI控件。Prism库提供以下三种区域适配器:
ContentControlRegionAdapter。此适配器适应System.Windows.Controls.ContentControl类型和派生类的控件。
SelectorRegionAdapter。此适配器适应从类* System.Windows.Controls.Primitives.Selector 派生的 * 控件,例如System.Windows.Controls.TabControl控件。
ItemsControlRegionAdapter。此适配器适应System.Windows.Controls.ItemsControl类型和派生类的控件。
区域行为
Prism图书馆介绍了区域行为的概念。这些是可插拔组件,为区域提供了大部分功能。引入了区域行为以支持视图发现和区域上下文(在本主题后面描述),并创建在WPF和Silverlight中一致的API。此外,行为提供了扩展区域实施的有效方式。
区域行为是附加到区域的类,以为该区域提供附加功能。此行为附加到该区域,并在该区域的生命周期内保持活动状态。例如,当AutoPopulateRegionBehavior附加到某个区域时,它会自动实例化并添加针对具有该名称的区域注册的任何ViewType。对于该区域的生命周期,它会持续监视RegionViewRegistry以进行新注册。可以在系统范围或每个区域的基础上轻松添加自定义区域行为或替换现有行为。
下一节将介绍自动添加到所有区域的默认行为。一种行为SelectorItemsSourceSyncBehavior仅附加到从Selector派生的控件。
注册行为
该RegionManagerRegistrationBehavior是负责确保该区域被注册到正确的RegionManager。当* 视图或控件作为另一个控件或区域的子项添加到可视树中时,控件中定义的任何区域都应该在父控件的* RegionManager中注册。删除子控件后,注册的区域将取消注册。
自动人口行为
有两个类负责实现视图发现。其中之一是AutoPopulateRegionBehavior。当它附加到某个区域时,它会检索在该区域名称下注册的所有视图类型。然后,它创建这些视图的实例并将它们添加到该区域。创建区域后,AutoPopulateRegionBehavior将监视RegionViewRegistry以查找该区域名称的任何新注册的视图类型。
如果您想要更多地控制视图发现过程,请考虑创建自己的IRegionViewRegistry实现和AutoPopulateRegionBehavior。
区域上下文行为
区域上下文功能包含在两个行为中:SyncRegionContextWithHostBehavior和BindRegionContextToDependencyObjectBehavior。这些行为负责监视对区域所做的上下文的更改,然后将上下文与附加到视图的上下文依赖项属性同步。
激活行为
所述RegionActiveAwareBehavior负责通知的图,如果它是有效或无效。视图必须实现IActiveAware才能接收这些更改通知。此主动感知通知是单向的(它从行为传播到视图)。通过更改IActiveAware接口上的活动属性,视图不会影响其活动状态。
地区终身行为
所述RegionMemberLifetimeBehavior负责确定如果一个项目应该从区域时被去激活被移除。该RegionMemberLifetimeBehavior监控区域的ActiveViews收集发现的项目,过渡到非激活状态。该行为检查已删除的项目是否为IRegionMemberLifetime或RegionMemberLifetimeAttribute(按此顺序),以确定它是否应在删除时保持活动状态。
如果集合中的项是System.Windows.FrameworkElement,它还将检查其DataContext的IRegionMemberLifetime或RegionMemberLifetimeAttribute。
按以下顺序检查区域项:
IRegionMemberLifetime.KeepAlive值
DataContext的IRegionMemberLifetime.KeepAlive值
RegionMemberLifetimeAttribute.KeepAlive值
DataContext的RegionMemberLifetimeAttribute.KeepAlive值
控制特定行为
所述SelectorItemsSourceSyncBehavior仅用于从导出的控制选择器,例如在一个WPF标签控制。它负责将区域中的视图与选择器的项同步,然后将区域中的活动视图与选择器的选定项同步。
扩展区域实施
Prism Library提供了扩展点,允许您自定义或扩展所提供API的默认行为。例如,您可以编写自己的区域适配器,区域行为或更改Navigation API分析URI的方式。有关扩展Prism库的更多信息,请参阅扩展Prism库。
查看组成
视图合成是视图的构建。在复合应用程序中,必须在运行时在应用程序UI中的特定位置显示来自多个模块的视图。要实现此目的,您需要定义视图的显示位置以及在这些位置创建和显示视图的方式。
可以通过视图发现自动创建和显示视图,也可以通过视图注入以编程方式显示视图。这两种技术决定了各个视图如何映射到应用程序UI中的命名位置。
查看发现
在视图发现中,您可以在RegionViewRegistry中在区域名称和视图类型之间建立关系。创建区域时,该区域将查找与该区域关联的所有ViewType,并自动实例化并加载相应的视图。因此,使用视图发现时,您无法明确控制何时加载和显示与区域对应的视图。
查看注射
在视图注入中,您的代码获取对区域的引用,然后以编程方式向其中添加视图。通常,这在模块初始化或作为用户操作的结果时完成。您的代码将按名称查询RegionManager中的特定区域,然后将视图注入其中。通过视图注入,您可以更好地控制何时加载和显示视图。您还可以从该地区删除视图。但是,使用视图注入时,无法将视图添加到尚未创建的区域。
导航
Prism Library 4.0包含导航API。Navigation API允许您将区域导航到URI,从而简化了视图注入过程。Navigation API实例化视图,将其添加到区域,然后激活它。此外,Navigation API允许导航回包含在区域中的先前创建的视图。有关导航API的更多信息,请参阅导航。
何时使用View Discovery与View Injection
选择要用于区域的视图加载策略取决于应用程序要求和区域的功能。
在以下情况下使用视图发现:
需要或需要自动加载视图。
视图的单个实例将加载到该区域中。
在以下情况下使用视图注入:
您的应用程序使用导航API。
您需要对创建和显示视图的时间进行显式或程序控制,或者需要从区域中删除视图; 例如,作为应用程序逻辑或导航的结果。
您需要在区域中显示相同视图的多个实例,其中每个视图实例都绑定到不同的数据。
您需要控制添加视图的区域的哪个实例。例如,您要将客户详细信息视图添加到特定客户详细信息区域。(此方案需要实现作用域,如本主题后面所述。)
UI布局方案
在复合应用程序中,来自多个模块的视图在运行时显示在应用程序UI中的特定位置。要实现此目的,您需要定义视图的显示位置以及在这些位置创建和显示视图的方式。
视图和将在其中显示的UI中的位置的分离允许应用程序的外观和布局独立于区域内出现的视图而发展。
下一节将介绍开发复合应用程序时将遇到的核心方案。适当时,Stock Trader RI的示例将用于演示该场景的解决方案。
实现Shell
shell是应用程序根对象,其中包含主UI内容。在Windows Presentation Foundation(WPF)应用程序中,shell是Window对象。
shell可以包含命名区域,其中模块可以指定将出现的视图。它还可以定义某些顶级UI元素,例如主菜单和工具栏。shell定义应用程序的整体结构和外观,类似于ASP.NET母版页控件。它可以定义在shell布局本身中存在和可见的样式和边框,还可以定义应用于插入到shell中的视图的样式,模板和主题。
作为应用程序体系结构的一部分,您不需要使用独特的shell来使用Prism库。如果要构建一个全新的复合应用程序,实现一个shell提供了一个定义良好的根和初始化模式,用于设置应用程序的主UI。但是,如果要将Prism Library功能添加到现有应用程序,则无需更改应用程序的基本体系结构即可添加shell。相反,您可以更改现有的窗口定义或控件,以添加可根据需要提取视图的区域。
您的应用程序中也可以有多个shell。如果您的应用程序旨在为用户打开多个顶级窗口,则每个顶级窗口都充当其包含的内容的shell。
股票交易员RI壳牌
WPF Stock Trader RI有一个shell作为主窗口。在下图中,将突出显示shell和视图。shell是Stock Trader RI启动时显示的主窗口,其中包含所有视图。它定义了模块添加其视图的区域以及一些顶级UI项目,包括CFI Stock Trader标题和Watch List撕下横幅。
股票交易者RI壳窗口,区域和视图
Stock Trader RI中的shell实现由Shell.xaml及其代码隐藏文件Shell.xaml.cs及其视图模型ShellViewModel.cs提供。Shell.xaml包括作为shell一部分的布局和UI元素,包括模块添加其视图的区域的定义。
以下XAML显示了定义shell的结构和主要XAML元素。请注意,RegionName附加属性用于定义四个区域,窗口背景图像为shell提供背景。
XAML
复制
prism:RegionManager.RegionName="{x:Static inf:RegionNames.MainToolBarRegion}">
prism:RegionManager.RegionName="{x:Static inf:RegionNames.MainRegion}"/>
prism:RegionManager.RegionName="{x:Static inf:RegionNames.ActionRegion}">
在实施壳牌代码隐藏文件是非常简单的。在壳牌出口,这样,当引导程序创建它,它的依赖将被托管扩展框架(MEF)解决。shell具有单一依赖关系 - ShellViewModel -在构造期间注入,如以下示例所示。
C#
复制
// Shell.xaml.cs
[Export]
public partial class Shell : Window
{
public Shell()
{
InitializeComponent();
}
[Import]
ShellViewModel ViewModel
{
set
{
this.DataContext = value;
}
}
}
代码隐藏文件中的最小代码说明了复合应用程序体系结构的强大功能和简单性以及shell与其组成视图之间的松散耦合。
定义区域
您可以通过定义具有命名位置的布局(称为区域)来定义视图的显示位置。区域充当将在运行时显示的一个或多个视图的占位符。模块可以在布局中的区域中定位和添加内容,而无需知道区域的显示方式和位置。这允许更改布局而不影响将内容添加到布局的模块。
通过将区域名称分配给WPF控件(在上一个Shell.xaml文件中显示的XAML中或代码中)来定义区域。可以通过区域名称访问区域。在运行时,视图将添加到命名的Region控件,然后根据视图实现的布局策略显示视图。例如,选项卡控件区域将以选项卡式排列其子视图。区域支持添加或删除视图。可以通过编程方式或自动方式在区域中创建和显示视图。在Prism Library中,前者通过视图注入实现,后者通过视图发现实现。这两种技术决定了各个视图如何映射到应用程序UI中的命名区域。
应用程序的shell定义了最高级别的应用程序布局; 例如,通过指定主要内容和导航内容的位置,如下图所示。这些高级视图中的布局类似地定义,允许以递归方式组合整个UI。
模板shell
区域有时用于定义逻辑上相关的多个视图的位置。在这种情况下,区域控件通常是一个ItemsControl派生的控件,它将根据它实现的布局策略显示视图,例如以堆叠或选项卡式布局排列。
区域也可用于定义单个视图的位置; 例如,通过使用ContentControl。在这种情况下,区域控件一次只显示一个视图,即使多个视图映射到该区域位置也是如此。
股票交易者RI壳区域
Stock Trader RI显示了单视图和多视图布局方法的使用。您可以在shell中看到应用程序,它定义了应用程序的高级视图的位置。下图显示了Stock Trader RI shell定义的区域。
股票交易者RI壳区域
当应用程序购买或出售股票时,股票交易者RI也会演示多视图布局。买/卖区域是一个列表样式区域,显示多个买入/卖出视图(OrderCompositeView)作为其列表的一部分,如下图所示。
ItemsControl区域
shell的ActionRegion包含OrdersView。该OrdersView包含提交所有和取消所有按钮还有OrdersRegion。所述OrdersRegion附着到列表框,其显示多个控制OrderCompositeViews。
IREGION
区域是实现IRegion接口的类。该区域是容纳控件显示内容的容器。以下代码显示了IRegion接口。
C#
复制
public interface IRegion : INavigateAsync, INotifyPropertyChanged
{
IViewsCollection Views { get; }
IViewsCollection ActiveViews { get; }
object Context { get; set; }
string Name { get; set; }
Comparison
注意使用x:Static标记扩展来引用MainRegion字符串常量。这种做法消除了XAML中的魔术字符串。
XAML
复制
prism:RegionManager.RegionName="{x:Static inf:RegionNames.MainRegion}"/>
使用代码添加区域
所述RegionManager可以直接在不使用XAML寄存器区域。以下代码示例演示如何从代码隐藏文件向控件添加区域。首先,获得对区域管理者的引用。然后,使用RegionManager静态方法SetRegionManager和SetRegionName,将该区域附加到UI的ActionContent控件,然后将该区域命名为ActionRegion。
C#
复制
IRegionManager regionManager = ServiceLocator.Current.GetInstance
RegionManager.SetRegionManager(this.ActionContent, regionManager);
RegionManager.SetRegionName(this.ActionContent, "ActionRegion");
区域加载时显示区域中的视图
使用视图发现方法,模块可以为特定的命名位置注册视图(视图模型或表示模型)。在运行时显示该位置时,将自动创建并在其中显示已为该位置注册的所有视图。
模块使用注册表注册视图。父视图查询此注册表以发现为命名位置注册的视图。发现它们之后,父视图会将这些视图添加到占位符控件中,从而将这些视图放在屏幕上。
加载应用程序后,将通知组合视图以处理已添加到注册表的新视图的放置。
下图显示了视图发现方法。
查看发现
Prism Library定义了一个标准注册表RegionViewRegistry,用于注册这些命名位置的视图。
要显示区域中的视图,请使用区域管理器注册视图,如以下代码示例所示。您可以直接向区域注册视图类型,在这种情况下,视图将由依赖项注入容器构造,并在加载托管区域的控件时添加到区域。
C#
复制
// View discovery
this.regionManager.RegisterViewWithRegion("MainRegion", typeof(EmployeeView));
(可选)您可以提供一个返回要显示的视图的委托,如下一个示例所示。区域管理器将在创建区域时显示视图。
C#
复制
// View discovery
this.regionManager.RegisterViewWithRegion("MainRegion", () => this.container.Resolve
UI Composition QuickStart在EmployeeModule ModuleInit.cs文件中有一个演练,演示了如何使用RegisterViewWithRegion方法。
以编程方式显示区域中的视图
在视图注入方法中,视图以编程方式添加或由管理它们的模块从命名位置删除。要启用此功能,应用程序将在UI中包含命名位置的注册表。模块可以使用注册表查找其中一个位置,然后以编程方式将视图注入其中。为了确保可以类似地访问注册表中的位置,每个命名位置都遵循用于注入视图的公共接口。下图显示了视图注入方法。
查看注射
Prism库定义了一个标准注册表RegionManager和一个标准接口IRegion,用于访问这些位置。
要使用视图注入向区域添加视图,请从区域管理器中获取区域,然后调用Add方法,如以下代码所示。使用视图注入时,仅在将视图添加到区域后才会显示视图,这可能在加载模块或用户操作完成预定义操作时发生。
C#
复制
// View injection
IRegion region = regionManager.Regions["MainRegion"];
var ordersView = container.Resolve
region.Add(ordersView, "OrdersView");
region.Activate(ordersView);
除了Stock Trader RI之外,UI Composition QuickStart还有一个实现视图注入的演练。
导航
Prism Library 5.0包含导航API,它提供了丰富且一致的API,用于在WPF应用程序中实现导航。
区域导航是视图注入的一种形式。处理导航请求时,它将尝试在可以满足请求的区域中查找视图。如果找不到匹配的视图,它会调用应用程序容器来创建对象,然后将对象注入目标区域并激活它。
Stock Trader RI ArticleViewModel的以下代码示例说明了如何发起导航请求。
C#
复制
this.regionManager.RequestNavigate(RegionNames.SecondaryRegion,
new Uri("/NewsReaderView", UriKind.Relative));
有关区域导航的更多信息,请参阅导航。视图切换导航快速入门和基于状态的导航快速入门也是实现应用程序导航的示例。
在区域中排序视图
无论是使用视图发现还是查看注入,应用程序都可能需要命令视图在TabControl,ItemsControl或显示多个活动视图的任何其他控件中的显示方式。默认情况下,视图按其注册顺序显示并添加到区域中。
构建复合应用程序时,通常会从不同的模块注册视图。声明模块之间的依赖关系有助于缓解问题,但是当模块和视图没有任何真正的相互依赖关系时,声明人工依赖会不必要地耦合模块。
为了允许视图参与自己的排序,Prism库提供了ViewSortHint属性。此属性包含字符串Hint属性,该属性允许视图声明在区域中如何排序的提示。
显示视图时,Region类使用默认视图排序例程,该例程使用提示对视图进行排序。这是一个简单的区分大小写的排序。具有sort hint属性的视图在没有排序的视图之前排序。此外,没有属性的那些按照添加到区域的顺序显示。
如果要更改视图的排序方式,Region类提供了一个SortComparison属性,您可以使用自己的Comparison *
View Switching QuickStart演示了一种简单的编号方案,用于对左侧导航区域中的视图进行排序。以下代码示例显示应用于每个导航项视图的ViewSortHint。
C#
复制
[Export]
[ViewSortHint("01")]
public partial class EmailNavigationItemView
[Export]
[ViewSortHint("02")]
public partial class CalendarNavigationItemView
[Export]
[ViewSortHint("03")]
public partial class ContactsDetailNavigationItemView
[Export]
[ViewSortHint("04")]
public partial class ContactsAvatarNavigationItemView
在多个区域之间共享数据
Prism Library提供了多种方法来在视图之间进行通信,具体取决于您的方案。区域管理器提供RegionContext属性作为这些方法之一。
当您想要共享父视图和区域中托管的子视图之间的上下文时,RegionContext非常有用。RegionContext是附加属性。您可以在区域控件上设置上下文的值,以便可以使该区域控件中显示的所有子视图都可以使用它。区域上下文可以是任何简单或复杂的对象,也可以是数据绑定值。该RegionContext可以与任一视图中发现或视图注射使用。
注意
WPF中的DataContext属性用于设置视图的本地数据上下文。它允许视图使用数据绑定与视图模型,本地演示者或模型进行通信。RegionContext用于在多个视图之间共享上下文,而不是单个视图的本地视图。它提供了一种在多个视图之间共享上下文的简单机制。
以下代码显示了如何在XAML中使用RegionContext附加属性。
XAML
复制
prism:RegionManager.RegionContext="{Binding Path=SelectedEmployee.EmployeeId}"
...>
您还可以在代码中设置RegionContext,如以下示例所示。
C#
复制
RegionManager.Regions["Region1"].Context = employeeId;
要在视图中检索RegionContext,请使用RegionContext类的GetObservableContext静态方法。它将视图作为参数传递,然后访问其Value属性,如以下代码示例所示。
C#
复制
private void GetRegionContext()
{
this.Model.EmployeeId = (int)RegionContext.GetObservableContext(this).Value;
}
所述的值RegionContext可以从视图中通过简单地分配一个新的值到它的改变值属性。通过订阅GetObservableContext方法返回的ObservableObject上的PropertyChanged事件,可以选择通过视图通知RegionContext的更改。这允许在更改RegionContext时保持多个视图同步。以下代码示例演示了订阅PropertyChanged事件。
C#
复制
ObservableObject
private void ViewRegionContext_OnPropertyChangedEvent(object sender,
PropertyChangedEventArgs args)
{
if (args.PropertyName == "Value")
{
var context = (ObservableObject
所述RegionContext被设置为在该区域托管内容对象上附加属性。这意味着内容对象必须从DependencyObject派生。在前面的示例中,视图是一个可视控件,最终从DependencyObject派生。
如果选择使用WPF数据模板来定义视图,则内容对象将表示ViewModel或PresentationModel。如果视图模型或表示模型需要检索RegionContext,则需要从DependencyObject基类派生。
创建区域的多个实例
只有视图注入才能使用范围区域。如果您需要视图以拥有自己的区域实例,则应使用它们。定义具有附加属性的区域的视图会自动继承其父级的RegionManager。通常,这是在shell窗口中注册的全局RegionManager。如果应用程序创建该视图的多个实例,则每个实例都会尝试使用父RegionManager注册其区域。RegionManager只允许唯一命名的区域; 因此,第二次注册会产生错误。
相反,使用作用域区域,以便每个视图都有自己的RegionManager,其区域将使用该RegionManager而不是父RegionManager注册,如下图所示。
父级和范围的RegionManagers
要为视图创建本地RegionManager,请指定在将视图添加到区域时应创建新的RegionManager,如以下代码示例所示。
C#
复制
IRegion detailsRegion = this.regionManager.Regions["DetailsRegion"];
View view = new View();
bool createRegionManagerScope = true;
IRegionManager detailsRegionManager = detailsRegion.Add(view, null,
createRegionManagerScope);
该添加方法将返回新RegionManager该视图可以保留进一步访问本地范围。
创建视图
应用程序的可视化表示形式可以采用多种形式,包括用户控件,自定义控件和数据模板等。对于Stock Trader RI,用户控件通常用于表示主窗口上的不同部分,但这不是标准。在您的应用程序中,您应该使用您最熟悉的方法,这种方法适合您作为设计师的工作方式。无论应用程序中的主要可视化表示如何,您都将不可避免地在整体设计中使用用户控件,自定义控件和数据模板的组合。下图显示了Stock Trader RI使用这些不同项目的位置。此图还可作为以下部分的参考,这些部分描述了每个项目。
股票交易者RI用户控件,自定义控件和数据模板的使用
用户控制
Blend for Visual Studio 2013和Visual Studio 2013都为创建用户控件提供了丰富的支持。因此,建议使用这些工具创建的用户控件使用Prism Library创建UI内容。如本主题前面所述,Stock Trader RI广泛使用它们来创建将插入区域的内容。所述WatchListView.xaml用户控制是包含内部的简单的用户界面表示的一个很好的例子WatchModule。此控件是一个非常简单的控件,使用此模型可以直接创建。
自定义控件
在某些情况下,用户控制太有限。在这些情况下,自定义布局或可扩展性比创建的简便性更重要。这是自定义控件有用的地方。在Stock Trader RI中,饼图控件就是一个很好的例子。该控制由来自头寸的数据组成,并显示整个投资组合的图表。与用户控件相比,这种类型的控件比创建用户控件更具挑战性,与用户控件相比,它在Blend for Visual Studio 2013和Visual Studio 2013中的视觉设计支持有限。
数据模板
数据模板是大多数类型的数据驱动应用程序的重要组成部分。基于列表的控件的数据模板的使用在整个股票交易者RI中很普遍。在许多情况下,您可以使用数据模板来创建完整的可视化表示,而无需创建任何类型的控件。该ResearchRegion使用数据模板显示的文章,并与一个联合项目的风格,提供了的选择项目的指示。
Visual Studio 2013和Visual Studio 2013的Blend具有对数据模板的完全可视化设计支持。
资源
样式,资源字典和控件模板等资源可以分散在整个应用程序中。复合应用程序尤其如此。在考虑放置资源的位置时,请特别注意UI元素与所需资源之间的依赖关系。Stock Trader RI解决方案(如下图所示)包含指示资源可以存在的各个区域的标签。
跨解决方案的资源分配
应用资源
通常,应用程序资源是整个应用程序可用的资源。这些资源往往集中在根应用程序上,但它们也可以在模型或控件的类型基础上提供默认样式。例如,文本框样式应用于根应用程序中的文本框类型。除非在模块或控件级别覆盖样式,否则此样式将可用于应用程序中的所有文本框。
模块资源
模块资源与根应用程序资源的作用相同,因为它们可以应用于模块中的所有项目。使用此级别的资源可以在整个模块中提供一致的外观,并且还允许在跨越一个或多个可视组件的更具体实例中重用。模块级别的资源使用应包含在单个模块中。在UI元素显示不正确时,创建模块之间的依赖关系可能导致难以找到的问题。
控制资源
控制资源通常包含在控制库中,可供控制库中的所有控件使用。这些资源往往具有最有限的范围,因为控制库通常包含非常特定的控件,并且不包含用户控件。(在使用Prism Library创建的应用程序中,用户控件通常放在使用它们的模块中。)
UI设计指南
本主题的目标是为正在使用Prism Library和WPF构建应用程序的XAML设计人员和开发人员提供一些高级指导。本主题描述UI布局,可视化表示,数据绑定,资源和表示模型。阅读本主题后,您应该高度了解如何基于Prism库设计应用程序的UI,以及一些可以帮助您在复合应用程序中创建可维护UI的技术。
设计用户界面的指南
使用Prism Library创建的复合应用程序的布局建立在WPF的标准主体上 - 布局使用包含相关项的面板的概念。但是,对于复合应用程序,各种面板内的内容是动态的,在设计时不知道。这迫使设计人员和开发人员创建可以包含布局内容的页面结构,然后分别设计适合布局的每个元素。作为设计人员或开发人员,这意味着您必须考虑Prism库中的两个主要布局概念:容器组合和区域。
容器组成
容器组合实际上只是WPF本身提供的包含模型的扩展。术语容器可以表示任何元素,包括窗口,页面,用户控件,面板,自定义控件,控件模板或数据模板,它们可以包含其他元素。
您可视化UI的方式因实现而异,但您会发现突出的重复主题。您将创建包含固定内容和动态内容的窗口,页面或用户控件。固定内容将包含包含UI元素的整体结构,动态内容将放置在区域内。
例如,WPF Stock Trader RI有一个名为Shell.xaml的启动窗口,其中包含应用程序的整体结构。下图显示了Blend for Visual Studio 2013中加载的shell。请注意,只有UI的固定部分可见。当应用程序加载时,shell的其余部分由模块动态插入到各个区域中。
在这种类型的应用程序中,设计时体验有点受限,但是您知道内容将在运行时放置在不同区域这一事实是您需要设计的。要查看此示例,请将下一个插图中主页面的设计器视图与其后的插图中的运行时视图进行比较。在设计器视图中,页面大多是空的。与运行时视图对比,其中存在包含具有位置数据的选项卡控件的位置区域,以及与所选股票相关的趋势线,饼图和新闻区域。设计器视图和运行时视图之间的差异表明了设计人员和开发人员在创建使用Prism Library构建的应用程序时所面临的挑战。
在设计时间内无法看到物品; 因此,确定它们的大小以及它们如何适应应用程序的整体外观有点困难。在为容器创建布局时,请考虑以下事项:
是否有任何大小限制会限制内容的大小?如果有,请考虑使用支持滚动的容器。
考虑使用扩展器和ScrollViewer组合,以适应大量动态内容需要适应受限区域的情况。
密切关注内容随着屏幕内容的增长而扩大的程度,以确保应用程序的外观在任何分辨率下都具有吸引力。
股票交易者RI主窗口在Blend for Visual Studio 2013
在运行时间的股票交易商RI主窗口
在设计时查看复合应用程序
前面的两个图说明了使用在运行时组成的高级视图的挑战之一。复合应用程序中的每个UI元素必须单独设计。这使得很难直观地看出复合页面或窗口在运行时的外观。要在组合状态下可视化组合视图,可以使用包含要测试的视图的所有UI元素的页面或窗口创建测试项目。
此外,请考虑在Blend for Visual Studio 2013和Visual Studio 2013中使用设计时样本数据功能,以使用数据填充UI元素。使用数据模板,列表控件,图表或图形时,设计时数据非常有用。有关更多信息,请参阅设计时样本数据指南部分。
布局
在设计复合应用程序的布局时,请考虑以下事项:
shell定义了应用程序的主要布局。布局的每个区域都是一个区域,应保留为空容器。不要在设计时将内容放在区域内,因为内容将在运行时加载到那里。
shell应包含背景,标题和页脚。将shell视为ASP.NET母版页。
充当区域的控制容器与它们包含的视图分离。因此,您应该能够在不修改控件的情况下更改视图的大小,并且应该能够在不修改视图的情况下更改控件的大小。定义视图大小时应考虑以下事项:
如果视图将在多个区域中使用,或者如果不确定将在何处使用,请使用动态宽度和高度进行设计。
如果视图具有固定大小,则shell的区域应使用动态大小。
如果shell区域具有固定大小,则视图应使用动态大小。
视图可能需要固定的高度和动态宽度。这方面的一个例子是位于Stock Trader RI侧栏的PositionPieChart视图。
其他视图可能需要动态高度和宽度。例如,StockTrader RI侧栏中的NewsReader视图。高度本身取决于标题的长度,宽度应始终适应区域的大小(侧边栏宽度)。这同样适用于PositionSummaryView视图,其中网格的宽度应适应屏幕大小,高度应适应网格中的行数。
视图通常应具有透明背景,允许shell背景提供应用程序视觉背景。
始终使用命名资源来分配颜色,画笔,字体和字体大小,而不是直接在XAML中分配属性值。这使得应用程序维护更容易。它还允许应用程序在运行时响应资源字典中的更改。
动画
在shell或视图中使用动画时,请考虑以下事项:
您可以为shell的布局设置动画,但您必须单独为其内容和视图设置动画。
分别设计和动画每个视图。
使用柔和或温和的动画来提供UI元素被带入视图或从视图中移除的视觉线索。这为应用程序提供了抛光的外观和感觉。
Blend for Visual Studio 2013提供了丰富的行为,简化功能,以及基于可视状态更改或事件动画和转换UI元素的出色编辑体验。有关更多信息,请参阅MSDN上的VisualStateManager类。
运行时优化
请考虑以下有关性能优化的提示:
将任何公共资源放在App.xaml文件或合并字典中以避免重复样式。
设计时优化
以下部分描述了设计时方案,并提供了充分利用设计时体验的解决方案。
具有许多XAML资源的大型解决方案
在具有许多XAML资源的大型应用程序中,可视化设计器的加载时间可能会受到影响,有时会显着影响。这种性能下降的存在是因为可视化设计器必须解析所有合并的XAML资源。此问题的解决方案是将所有XAML资源移动到另一个解决方案,编译该解决方案,然后从大型解决方案引用新的XAML资源DLL。由于XAML资源位于二进制引用的程序集中,因此可视化设计器不会解析XAML资源,从而提高了设计时性能。将XAML资源移动到外部程序集时,您可能需要考虑为您的资源公开ComponentResourceKeys 。有关更多信息,请参阅ComponentResourceKey标记扩展 在MSDN上。
XAML资产
XAML是一种功能强大且富有表现力的语言,用于创建图像,图表,绘图和三维场景等资源。一些开发人员和设计人员更喜欢创建XAML资产,而不是使用.ico,.jpg或.png图像文件。他们更喜欢XAML方法的一个原因是利用XAML渲染的分辨率独立性。另一个是他们可以使用一个工具集Blend for Visual Studio 2013来创建所有必需的资产并设计他们的应用程序。
如果解决方案具有许多这些资产,则可能会影响设计时可视化设计器的加载。将资产移动到单独的DLL可以解决性能问题。移动资产还可以跨多个解决方案重用。
视觉设计师和参考装配
将XAML资源和资产移动到二进制引用程序集的一个令人遗憾的副作用是,Blend for 2013和Visual Studio 2013属性编辑器不会列出位于二进制引用程序集中的资源。这意味着您将无法从工具提供的其中一个资源选择器中选择命名资源。相反,您需要输入资源的名称。
创建设计友好视图的指南
以下是设计人员友好(也称为可混合或可工具)应用程序的一些特征:
它通过使用Visual Studio和Blend设计器提供了高效的编辑体验。
它是启用工具的。例如,它允许您使用绑定构建器。
它在需要时提供设计时样本数据。
它允许在设计时执行代码,而不会导致未处理的异常。
在编辑会话期间多次执行以下操作。非设计友好的用户代码将导致这些操作中的一个或多个失败,从而降低开发人员或设计人员的工作效率和创造力。
设计表面动作:
构造对象
加载对象
设置属性值
执行设计表面手势
使用控件作为根元素
在另一个控件内部托管控件
重复打开,关闭和重新打开XAML文件
重建项目
重塑设计师
绑定构建器操作:
发现DataContext
列出可用的数据源
列出数据源类型属性
设计时样本数据操作:
使用设计图面上的控件正确显示样本数据
编码设计时间
为了给您丰富的设计时体验,Visual Studio和Blend设计人员在设计时实例化对象并运行代码。但是,在实例化之前尝试访问引用类型的代码导致的空引用异常会导致高百分比的加载失败和不必要的设计时异常。
下表列出了设计时体验不佳的主要原因。通过避免以下问题并使用这些技术来缓解这些问题,您的设计时体验和生产力将大大提高,开发人员到设计人员的工作流程将更加顺畅。
用户代码中避免使用此功能
Visual Studio 2013
混合Visual Studio 2013
在设计时旋转多个线程。例如,在构造函数中实例化和启动Timer或在设计时启动Loaded事件。
使用在设计时导致堆栈溢出的控件。
使用尝试递归加载自身的控件。
在转换器或数据模板选择器中抛出空引用异常。
在构造函数中抛出null引用或其他异常。这些是由:
使用调用业务或数据层的代码在设计时从数据库或网络返回数据。
在引导或容器初始化代码运行之前,尝试使用MEF,控制反转(IoC)或服务定位器来解决依赖关系。
在控件或用户控件的Loaded事件中抛出空引用或其他异常。当您对运行时可能为真的控件状态进行假设但在设计时不正确时会发生这种情况。
尝试在设计时访问Application或Application.Current对象。
创建非常大的项目。
减少设计时用户代码中的问题
一些防御性编码实践将消除上表中描述的大多数问题。但是,在您可以缓解设计时用户代码中的问题之前,您必须了解您的应用程序控件和代码是由设计人员在未初始化的应用程序域中单独执行的。在这种情况下,未初始化意味着通常的启动,引导或初始化代码尚未运行。
当您的应用程序在运行时执行时,将运行App.xaml.cs或App.xaml.vb中的启动代码。如果您的应用程序的其余部分依赖于此代码,则此代码将不会在设计时执行。如果您在代码中没有预料到这一点,则会发生不必要的异常。(这就是为什么在设计时尝试在用户代码中访问Application或Application.Current对象会导致异常。)为了缓解这些问题:
永远不要假设引用的对象将在设计时代码中实例化。在可以在设计时执行的代码中,始终在访问任何引用对象之前执行空检查。
如果您的代码访问Application或Application.Current对象,请在访问对象之前执行空引用检查。
如果构造函数或Loaded事件处理程序需要运行访问数据库或调用网络的复杂代码或代码,请考虑以下解决方案之一:
将代码包装在一个检查中,该检查通过调用System.ComponentModel DesignerProperties方法DesignerProperties.GetIsInDesignMode来确定代码是否在设计时运行。
而不是直接在构造函数或Loaded事件处理程序中运行代码,抽象调用接口后面的类,然后使用许多技术之一在设计时,运行时和测试时以不同方式解析该依赖项。
例如,不是直接调用数据服务来检索数据,而是将数据服务调用包装在通过接口公开方法的类中。然后,在设计时,使用模拟或设计时对象解析接口。
了解用户控制代码何时在设计时执行
Blend和Visual Studio都使用设计器窗格中显示的根对象的模型。这对于提供所需的设计体验是必要的。因为根对象是模拟的,所以它的构造函数和Loaded事件代码不会在设计时执行。但是,场景中的其余控件正常构造,并且它们的Loaded事件就像在运行时一样被引发。
在下图中,将不执行根Windows构造函数和已加载事件代码。子用户控件构造函数和Loaded事件代码将被执行。
这些概念很重要,尤其是在构建在运行时动态构建的复合应用程序或应用程序时。
大多数应用程序视图都是独立编码和设计 因为它们是独立设计的,所以它们通常是设计器中的根对象。因此,它们的构造函数和Loaded事件代码永远不会执行。
但是,如果您使用相同的用户控件并将其作为另一个控件的子项放置在设计图面上,则曾经隔离的用户控件代码现在正在设计时执行。如果您没有遵循上述减轻设计时代码问题的做法,那么现在托管的用户控件可能会变得不友好并导致设计器加载问题。
设计时属性
内置的“d:”设计时属性为成功的设计时工具体验提供了平稳的道路。
我们需要解决的问题是如何在设计时为Binding Builder工具提供形状。在这种情况下,形状是Binding Builder可以反映的实例化类型,然后在构建绑定时列出这些属性以供选择。
形状也由设计时样本数据提供。样本数据包含在“设计时样本数据指南”一节中。
以下部分描述了如何使用d:DataContext属性和d:DesignInstance标记扩展。
属性和标记扩展中的“d:”是设计属性所属的设计命名空间的别名。有关更多信息,请参阅WPDN主题,WPF设计器中的设计时属性。
无法在用户代码中创建或扩展“d:”属性和标记扩展; 它们只能在XAML中使用。“d:”属性和标记扩展名未编译到您的应用程序中; 它们仅由Visual Studio和Blend工具使用。
d:DataContext属性
d:DataContext,为控件及其子控件指定设计时数据上下文。指定d:DataContext时,应始终为设计时DataContext提供与运行时DataContext相同的形状。
如果为控件指定了DataContext和d:DataContext,则工具将使用d:DataContext。
d:DesignInstance标记扩展
如果标记扩展对您来说是新手,请在MSDN上阅读标记扩展和WPF XAML。
d:DesignInstance返回一个实例化的Type(“shape”),您希望将其指定为绑定到设计器中控件的数据源。该类型不需要是可创建的以用于建立形状。下表说明了d:DesignInstance标记扩展属性。
标记扩展属性
定义
类型
要创建的类型的名称。Type是构造函数中的默认参数。
IsDesignTimeCreatable
可以创建指定的Type吗?如果为false,将创建一个虚拟类型而不是真正的类型。默认值为false。
CreateList
如果为true,则返回指定Type的通用列表。默认值为false。
典型的d:DataContext场景
以下三个代码示例演示了可重复的模式,用于连接视图和视图模型以及启用设计器的工具。
该PersonViewModel是,依赖PersonView具有在运行时。虽然示例中的视图模型非常简单,但实际视图模型通常具有必须解析的一个或多个外部依赖项,并且这些依赖项通常会注入其构造函数中。
当PersonView被构造,其依赖PersonViewModel将建及其依赖由MEF或依赖注入容器解决。
注意
如果视图模型没有需要解析的外部依赖项,则可以在视图的XAML中实例化视图模型,并且不需要其DataContext和d:DataContext。
C#
复制
// PersonViewModel.cs
[Export]
public class PersonViewModel {
public String FirstName { get; set; }
public String LasName { get; set; }
}
这是一个很好的模式,用于连接视图和视图模型; 但是,它在设计时让视图不知道它的DataContext的形状(视图模型)。
在下面的XAML示例中,您可以看到网格上使用的d:DesignInstance标记扩展,用于返回PersonViewModel的虚假实例,然后由d:DataContext公开。因此,Grid的所有子控件都将继承d:DataContext,使设计器工具能够发现并使用其类型和属性,从而为开发人员和设计人员提供更高效的设计体验。
XAML
复制
x:Class="WpfApplication1.PersonView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
HorizontalAlignment="Left" VerticalAlignment="Top"
Text="{Binding Path=FirstName, Mode=TwoWay}" />
HorizontalAlignment="Left" VerticalAlignment="Top"
Text="{Binding Path=LasName, Mode=TwoWay}" />
注意
附加属性和ViewModel定位器解决方案
有几种替代技术可用于关联开发人员社区提供的视图和视图模型。其中一个挑战是在运行时运行良好的解决方案并不总是在设计时工作。一种这样的解决方案是使用附加属性和视图模型定位器来分配视图的DataContext。视图模型定位器是必需的,以便可以构建视图模型并解析其依赖关系。
此解决方案的问题在于您还必须包含d:DataContext - d:DesignInstance组合,因为可视化设计器工具无法以附加属性的结果反映出来d:DesignInstance。
无论您在应用程序中使用哪种技术在设计时解决形状,最重要的目标是在整个应用程序中保持一致。一致性将使应用程序维护变得更加容易,并将导致成功的设计人员 - 开发人员工作流程。
设计时样本数据指南
WPF和Silverlight Designer团队发布了一篇深入的,基于场景的培训文章,该文章讨论了WPF和Silverlight项目中样本数据的使用。MSDN上提供了WPF和Silverlight Designer中的文章Sample Data。
使用设计时样本数据
如果使用可视化设计工具(如Blend或Visual Studio 2013),则设计时样本数据变得非常重要。视图可以填充数据和图像,使设计任务更容易,更快速地完成。这样可以提高生产力和创造力。
包含数据模板的空列表控件将不可见,除非它们填充了数据,使编辑空控件的任务更耗时,因为您需要运行应用程序以查看上次编辑在运行时的外观。
示例数据源
您可以使用以下任何来源的示例数据:
混合Visual Studio 2013 XML示例数据
混合Visual Studio 2013和Visual Studio 2013 XAML示例数据
XAML资源
码
以下各小节介绍了每个来源的数据。
混合XML示例数据
Blend使您能够快速创建XML模式并填充相应的XML文件。这是在不依赖任何项目类的情况下完成的。
此类示例数据的目的是让设计人员快速启动他们的项目,而无需等待开发人员或应用程序类可供使用之前。
虽然Blend和Visual Studio设计器都支持大多数示例数据,但XML示例数据是Blend功能,并且不在Visual Studio设计器中呈现。
注意
构建时,XML样本数据文件未编译或添加到程序集中; 但是,XML模式将编译到构建的程序集中。
Visual Studio 2013和Visual Studio 2013 XAML示例数据的混合
从Expression Blend 4和Visual Studio 2010开始,添加了d:DesignData标记扩展,以启用XAML样本数据的设计时加载。
示例数据XAML文件包含实例化一个或多个类型的XAML,并为属性分配值。
d:DesignData具有Source属性,该属性将统一资源标识符(URI)提取到位于项目中的示例数据XAML文件。的d:DesignData标记扩展加载XAML文件,解析它,然后返回一个对象图。对象图可以由d:DataContext属性,CollectionViewSource d:DesignSource属性或DomainDataSource d:DesignData属性使用。
d:DesignData标记扩展克服的挑战之一是它可以为不可创建的用户类型创建样本数据。例如,无法在代码中创建WCF富Internet应用程序(RIA)服务实体派生对象。此外,开发人员可能拥有自己的类型,这些类型不可创建,但仍希望拥有这些类型的示例数据。
您可以通过在解决方案资源管理器中设置示例数据文件上的Build Action属性来更改d:DesignData处理示例数据文件的方式,如下所示:
Build Action = DesignData - 将创建虚拟类型
Build Action = DesignDataWithDesignTimeCreatableTypes - 将创建真实类型
当Blend用于为类创建示例数据时,它会创建一个XAML示例数据文件,并将Build Action设置为DesignData。如果需要实际类型,请在Visual Studio中打开解决方案,并将示例数据文件的Build Action更改为DesignDataWithDesignTimeCreatableTypes。
注意
在下图中,“ 自定义工具”属性为空。这是样本数据正常工作所必需的。默认情况下,Blend正确地将此属性设置为空。
使用Visual Studio 2013添加示例数据文件时,通常会添加新的资源字典项并从那里进行编辑。在这种情况下,您必须设置“ 构建操作”并清除“ 自定义工具”属性。
示例数据文件属性
Expression Blend提供了用于快速创建和绑定XAML样本数据的工具。可以在Visual Studio 2013设计器中使用和查看XAML示例数据,如下图所示。
在Blend for Visual Studio 2013中定义样本数据
生成样本数据后,数据将显示在“数据”窗格中,如下图所示。
数据窗格
然后,您可以将其拖到视图的根元素(例如UserControl)上,并将其设置为d:DataContext属性。您还可以将样本数据集合拖放到项目控件上,Blend会将示例数据连接到控件。
注意
XAML示例数据文件未编译到构建的程序集中或包含在构建的程序集中。
XAML资源
您可以在XAML中创建实例化所需类型的资源,然后将该资源绑定到DataContext或列表控件。
此技术可用于快速创建用于编辑数据模板的丢弃样本数据,该数据模板在没有样本数据的情况下编辑需要更长时间。
码
如果您更喜欢在代码中创建示例数据,则可以编写一个类,该类公开将样本数据返回给其使用者的属性或方法。例如,您可以编写一个Customers类,该类在其默认的空构造函数中填充了Customer类的多个实例。每个Customer实例也将设置适当的属性值。
可用于使用前面描述的示例数据类的一种技术是使用d:DataContext,d:DesignInstance组合,确保将d:DesignInstanceIsDesignTimeCreatable属性设置为True。IsDesignTimeCreatable必须为True的原因是您希望执行customer构造函数,以便运行填充该类的代码。如果将客户视为虚假类型,则客户代码将永远不会运行,并且工具只能发现“形状”。
以下XAML示例实例化Customers类,然后将其设置为d:DataContext。此Grid的子控件可以使用Customers类公开的数据。
XAML
复制
UI布局关键决策
当您开始复合应用程序项目时,您需要做出一些UI设计决策,这些决策以后很难更改。通常,这些决策是应用程序范围的,它们的一致性有助于开发人员和设
以下是重要的UI布局决策:
确定应用程序流程并相应地定义区域。
确定加载每个区域将使用的视图类型。
确定是否要使用区域导航API。
确定要使用的UI设计模式(MVVM,演示模型等)。
确定样本数据策略。
当用户与富客户端应用程序交互时,其用户界面(UI)将不断更新以反映用户正在处理的当前任务和数据。随着用户与应用程序内的各种任务交互并完成各种任务,UI可能会随着时间发生相当大的变化。应用程序协调这些UI更改的过程通常称为导航。本主题描述如何使用Prism库实现复合Model-View-ViewModel(MVVM)应用程序的导航。
通常,导航意味着删除UI中的某些控件,同时添加其他控件。在其他情况下,导航可以意味着更新一个或多个现有控件的视觉状态 - 例如,一些控件可以被简单地隐藏或折叠,而其他控件被显示或扩展。类似地,导航可能意味着控件显示的数据被更新以反映应用程序的当前状态 - 例如,在主 - 细节场景中,详细视图中显示的数据将基于当前选择的项目进行更新在主视图中。所有这些场景都可以被视为导航,因为更新了用户界面以反映用户的当前任务和应用程序的当前状态。
应用程序内的导航可以由用户与UI的交互(通过鼠标事件或其他UI手势)或由于内部逻辑驱动的状态改变而从应用程序本身引起。在某些情况下,导航可能涉及非常简单的UI更新,不需要自定义应用程序逻辑。在其他情况下,应用程序可以实现复杂的逻辑以编程方式控制导航以确保强制执行某些业务规则 - 例如,应用程序可能不允许用户离开某个表单而不首先确保输入的数据是正确的。
在Windows Presentation Foundation(WPF)应用程序中实现所需的导航行为通常可以相对简单,因为它提供了对导航的直接支持。但是,在使用Model-View-ViewModel(MVVM)模式的应用程序中或在使用多个松散耦合模块的复合应用程序中实现导航可能更复杂。Prism提供了在这些情况下实施导航的指导。
导航定义为应用程序通过其与应用程序或内部应用程序状态更改进行交互而更改其UI的过程。
UI更新可以通过在应用程序的可视树中添加或删除元素,或者通过对可视树中的现有元素应用状态更改来完成。WPF是一个非常灵活的平台,通常可以使用这种方法实现特定的导航场景。但是,最适合您应用的方法取决于多种因素。
Prism区分了前面描述的两种导航方式。通过对可视树中的现有控件的状态更改完成的导航被称为基于状态的导航。通过从可视树中添加或移除元素来完成的导航被称为基于视图的导航。Prism提供了实现两种导航样式的指导,重点关注应用程序使用Model-View-ViewModel(MVVM)模式将UI(封装在视图中)与表示逻辑和数据(封装在视图中)分开的情况模型)。
在基于状态的导航中,表示UI的视图可以通过视图模型中的状态更改或通过视图本身内的用户交互来更新。在这种导航方式中,不是用另一个视图替换视图,而是更改视图的状态。根据视图状态的更改方式,更新的UI可能会让用户感觉像导航一样。
这种导航方式适用于以下情况:
这种导航方式不适用于UI必须向用户呈现不同数据或用户必须执行不同任务的情况。在这些情况下,最好实现单独的视图(和视图模型)来表示数据或任务,然后使用基于视图的导航在它们之间导航,如本主题后面所述。类似地,如果实现导航所需的UI状态更改的数量过于复杂,则这种导航方式不适合,因为视图的定义可能变得很大并且难以维护。在这种情况下,最好通过使用基于视图的导航在不同的视图中实现导航。
以下部分描述了可以使用基于状态的导航的典型情况。这些部分中的每一部分都涉及基于状态的导航快速入门,它实现了即时消息传递式应用程序,允许用户管理和与其联系人聊天。
您的应用程序可能经常需要向用户显示相同的数据,但格式或样式不同。在这种情况下,您可以在视图中使用基于状态的导航在不同样式之间切换,可能使用它们之间的动画过渡。例如,基于状态的导航快速入门允许用户选择联系人的显示方式 - 简单文本列表或头像(图标)。用户可以通过单击“ 列表”按钮或“ 头像”按钮在这些可视表示之间切换。该视图提供了两个表示之间的动画过渡,如下图所示。
联系基于状态的导航快速入门中的视图导航
由于视图呈现的是相同的数据,但是在不同的可视化表示中,视图模型不需要参与表示之间的导航。在这种情况下,导航完全在视图本身内处理。这种方法为UI设计人员提供了很大的灵活性,可以设计出引人注目的用户体验,而无需更改应用程序的代码。
混合行为提供了在视图中实现此导航样式的好方法。基于状态的导航QuickStart应用程序使用Blend的DataStateBehavior数据绑定到单选按钮,在使用可视状态管理器定义的两个可视状态之间切换,一个按钮将联系人显示为列表,一个按钮将联系人显示为图标。
XAML复制
<ei:DataStateBehavior Binding="{Binding IsChecked, ElementName=ShowAsListButton}" Value="True"
TrueState="ShowAsList" FalseState="ShowAsIcons"/>
当用户单击“ 联系人”或“ 头像”单选按钮时,可视状态在ShowAsList可视状态和ShowAsIcons可视状态之间切换。还使用可视状态管理器定义这些状态之间的翻转过渡动画。
当用户切换到当前所选联系人的详细信息视图时,基于状态的导航快速入门应用程序会显示此类导航的另一个示例。下图显示了此示例。
基于状态的导航快速入门中的“联系人详细信息”视图
同样,这可以使用Blend DataStateBehavior轻松实现; 但是,这次它被绑定到视图模型上的ShowDetails属性,该属性使用翻转过渡动画在ShowDetails和ShowContacts视觉状态之间切换。
类似地,应用程序中的视图有时可能需要根据对内部应用程序状态的更改来更改其布局或样式,而内部应用程序状态又由视图模型上的属性表示。基于状态的导航快速入门中显示了此方案的示例,其中使用ConnectionStatus属性在Chat视图模型类上表示用户的连接状态。当用户的连接状态发生变化时,将通知视图(通过属性更改通知事件),允许视图在视觉上可视地表示当前连接状态,如下图所示。
基于状态的导航快速入门中的连接状态表示
为实现此目的,视图定义绑定到视图模型的ConnectionStatus属性的DataStateBehavior数据,以在适当的可视状态之间切换。
XAML复制
<ei:DataStateBehavior Binding="{Binding ConnectionStatus}" Value="Available" TrueState="Available" FalseState="Unavailable"/>
请注意,用户可以通过UI或应用程序根据某些内部逻辑或事件更改连接状态。例如,如果用户在特定时间段内没有与视图交互或者当用户的日历指示他或她在会议中时,应用程序可以移动到“不可用”状态。基于状态的导航快速入门通过使用计时器随机切换连接状态来模拟此场景。更改连接状态后,将更新视图模型上的属性,并通过属性更改事件通知视图。然后更新UI以反映当前连接状态。
前面的所有示例都涉及在视图中定义视觉状态,以及由于用户与视图的交互或视图模型定义的属性更改而在视图之间切换。此方法允许UI设计器在视图中实现类似导航的可视行为,而无需替换视图或要求对应用程序代码进行任何代码更改。当需要视图以不同的样式或布局呈现相同的数据时,此方法是合适的。它不适用于向用户呈现不同数据或应用程序功能或导航到应用程序的不同部分的情况。
通常,应用程序需要以有限的方式与用户交互。在这些情况下,通常更适合在当前视图的上下文中与用户交互,而不是导航到新视图。例如,在基于状态的导航快速入门中,用户可以通过单击“ 发送消息”按钮向联系人发送消息。然后,视图将显示一个弹出窗口,允许用户键入消息,如下图所示。因为与用户的这种交互是有限的并且逻辑上发生在父视图的上下文中,所以它可以容易地实现为基于状态的导航。
使用基于状态的导航快速入门中的弹出窗口与用户交互
若要实现此行为,基于状态的导航快速入门实现SendMessage命令,该命令绑定到“ 发送消息”按钮。调用此命令时,视图模型将与视图交互以显示弹出窗口。这是使用实现MVVM模式中描述的交互请求模式实现的。
以下代码示例显示了基于状态的导航QuickStart应用程序中的视图如何响应视图模型提供的SendMessageRequest交互请求对象。收到请求事件后,SendMessageChildWindow将显示为弹出窗口。
XAML复制
<prism:InteractionRequestTrigger SourceObject="{Binding SendMessageRequest}"> <prism:PopupWindowAction IsModal="True">
<prism:PopupWindowAction.WindowContent> <vs:SendMessagePopupView /> prism:PopupWindowAction.WindowContent> prism:PopupWindowAction> prism:InteractionRequestTrigger>
虽然基于状态的导航对于前面概述的场景可能很有用,但是应用程序中的导航通常通过将应用程序UI中的一个视图替换为另一个视图来实现。在Prism中,这种导航方式称为基于视图的导航。
根据应用程序的要求,此过程可能相当复杂,需要仔细协调。以下是在实现基于视图的导航时经常需要解决的常见挑战:
Prism通过扩展Prism的region机制来支持导航,为这些挑战提供支持和指导。以下部分提供了Prism region的简要概述,并描述了它们如何扩展以支持基于视图的导航。
Prism region旨在通过允许应用程序的整体UI以松散耦合的方式构建来支持复合应用程序(即,由多个模块组成的应用程序)的开发。区域允许在模块中定义的视图显示在应用程序的UI中,而无需模块明确了解应用程序的整体UI结构。它们允许轻松更改应用程序UI的布局,从而允许UI设计人员为应用程序选择最合适的UI设计和布局,而无需更改模块本身。
Prism region基本上命名为占位符,在其中可以显示视图。通过简单地向其添加RegionName附加属性,应用程序UI中的任何控件都可以声明为region,如此处所示。
XAML复制
<ContentControl prism:RegionManager.RegionName="MainRegion" ... />
对于指定为区域的每个控件,Prism创建一个Region对象来表示区域,并创建一个RegionAdapter对象,该对象管理视图在指定控件中的放置和激活。Prism Library 为大多数常见的WPF控件提供RegionAdapter实现。您可以创建自定义RegionAdapter以支持其他控件,或者在需要定义自定义行为时。该RegionManager类提供给接入地区的应用程序中的对象。
在许多情况下,区域控件将是一个简单的控件,例如ContentControl,它可以一次显示一个视图。在其他情况下,Region控件将是一个能够同时显示多个视图的控件,例如TabControl或ListBox控件。
区域适配器管理关联区域内的视图列表。这些视图中的一个或多个将根据其定义的布局策略显示在区域控件中。可以为视图分配一个名称,该名称可用于稍后检索该视图。区域适配器* 管理区域内视图的活动状态。活动视图是选定视图或最顶视图 - 例如,在*TabControl中,活动视图是在所选选项卡中显示的视图; 在ContentControl中,活动视图是当前显示为控件内容的视图。
注意
在导航期间,视图的活动状态很重要。通常,您希望活动视图参与导航,以便它可以在用户导航之前保存数据,或者可以确认或取消导航操作。
先前版本的Prism允许以两种方式在区域中显示视图。第一种称为视图注入,允许以编程方式在区域中显示视图。此方法对于动态内容很有用,根据应用程序的表示逻辑,要在区域中显示的视图会频繁更改。
通过Region类的Add方法支持视图注入。下面的代码示例演示如何通过RegionManager类获取对Region对象的引用,并以编程方式向其添加视图。在此示例中,使用依赖项注入容器创建视图。
C#复制
IRegionManager regionManager = ...;
IRegion mainRegion = regionManager.Regions["MainRegion"];
InboxView view = this.container.Resolve();
mainRegion.Add(view);
第二种方法称为视图发现,它允许模块针对区域名称注册视图类型。每当显示具有指定名称的区域时,将自动创建指定视图的实例并在该区域中显示。此方法对于相对静态的内容很有用,其中要在区域中显示的视图不会更改。
通过RegionManager类上的RegisterViewWithRegion方法支持视图发现。此方法允许您指定在显示命名区域时将调用的回调方法。以下代码示例显示了在首次显示主区域时如何创建视图(通过依赖项注入容器)。
C#复制
IRegionManager regionManager = ...;
regionManager.RegisterViewWithRegion("MainRegion", () =>
container.Resolve());
有关Prism区域支持的详细概述以及有关如何利用视图注入和发现来利用区域组成应用程序UI的信息,请参阅编写用户界面。本主题的其余部分描述了如何扩展区域以支持基于视图的导航,以及如何解决前面描述的各种挑战。
视图注入和视图发现都可以被认为是有限形式的导航 - 视图注入是一种显式的,程序化的导航形式,而视图发现是一种隐式或延迟导航的形式。但是,在Prism 4.0中,基于URI和可扩展的导航机制,区域已经扩展到支持更一般的导航概念。
区域内的导航意味着将在该区域内显示新视图。要显示的视图通过URI标识,默认情况下,URI指的是要创建的视图的名称。您可以使用INavigateAsync接口定义的RequestNavigate方法以编程方式启动导航。
注意
尽管名称如此,但INavigateAsync接口并不代表在单独的后台线程上执行的异步导航。相反,INavigateAsync接口表示执行伪异步导航的能力。该RequestNavigate方法可以返回同步导航操作完成之后,或者它可以返回而导航操作仍悬而未决,如在用户需要确认导航的情况。通过允许您在导航期间指定回调和延续,Prism提供了一种机制来启用这些方案,而无需在后台线程上导航的复杂性。
INavigateAsync接口由Region类实现,允许您在该区域内启动导航。
C#复制
IRegion mainRegion = ...;
mainRegion.RequestNavigate(new Uri("InboxView", UriKind.Relative));
您也可以拨打RequestNavigate的方法RegionManager,它允许你指定要驾驶的区域的名称。这个方便的方法获取对指定区域的引用,然后调用RequestNavigate方法,如上面的代码示例所示。
C#复制
IRegionManager regionManager = ...;
regionManager.RequestNavigate("MainRegion",
new Uri("InboxView", UriKind.Relative));
默认情况下,导航URI指定在容器中注册的视图的名称。
使用MEF,您只需导出具有指定名称的视图类型即可。
C#复制
[Export("InboxView")]
public partial class InboxView : UserControl
在导航期间,指定视图通过容器或MEF以及其对应的视图模型和其他相关服务和组件进行实例化。在实例化视图之后,将其添加到指定区域并激活(本主题后面将更详细地描述激活)。
注意
前面的描述说明了视图优先导航,其中URI指的是视图类型的名称,因为它是随容器导出或注册的。使用视图优先导航,依赖视图模型将创建为视图的依赖项。另一种方法是使用视图模型优先导航,其中导航URI指的是视图模型类型的名称,因为它是随容器导出或注册的。当视图定义为数据模板时,或者您希望独立于视图定义导航方案时,查看模型优先导航非常有用。
该RequestNavigate方法还允许您指定一个回调方法,或委托,当导航完成后,将被调用。
C#复制
private void SelectedEmployeeChanged(object sender, EventArgs e)
{
...
regionManager.RequestNavigate(RegionNames.TabRegion,
"EmployeeDetails", NavigationCompleted);
}
private void NavigationCompleted(NavigationResult result)
{
...
}
所述NavigationResult类定义了提供有关导航操作信息的属性。该结果属性指示导航是否成功。如果导航失败,则Error属性提供对导航期间引发的任何异常的引用。的语境属性提供给导航URI,它包含任何参数,并且向协调导航操作导航服务的引用。
通常,应用程序中的视图和视图模型将要参与导航。该INavigationAware接口支持这个。您可以在视图上或(更常见地)视图模型上实现此接口。通过实现此界面,您的视图或视图模型可以选择参与导航过程。
注意
在下面的描述中,尽管在视图之间的导航期间引用了对该接口的调用,但应该注意,无论是由视图还是由视图模型实现,都将在导航期间调用INavigationAware接口。
在导航过程中,Prism会检查视图是否实现了INavigationAware接口; 如果是,它会在导航期间调用所需的方法。Prism还会检查设置为视图的DataContext的对象是否实现了此接口; 如果是,它会在导航期间调用所需的方法。
此界面允许视图或视图模型参与导航操作。所述INavigationAware接口定义了三个方法。
C#复制
public interface INavigationAware
{
bool IsNavigationTarget(NavigationContext navigationContext);
void OnNavigatedTo(NavigationContext navigationContext);
void OnNavigatedFrom(NavigationContext navigationContext);
}
该IsNavigationTarget方法允许现有的(显示的)视图或视图模型,以指示它是否能够处理所述导航请求。在您可以重复使用现有视图来处理导航操作或导航到已存在的视图时,这非常有用。例如,可以更新显示客户信息的视图以显示不同的客户信息。有关使用此方法的详细信息,请参阅本主题后面的“导航到现有视图”一节。
该OnNavigatedFrom和的OnNavigatedTo方法是导航操作期间调用。如果区域中当前活动的视图实现此接口(或其视图模型),则在导航发生之前调用其OnNavigatedFrom方法。该OnNavigatedFrom方法允许一个视图保存任何状态,或为它的失活或去除从UI制备,例如,以保存用户已经到web服务或数据库所作的任何更改。
如果新创建的视图实现此接口(或其视图模型),则在导航完成后调用其OnNavigatedTo方法。所述的OnNavigatedTo方法允许新显示的视图初始化自身,可能使用传递给它上的导航URI任何参数。有关详细信息,请参阅下一节“导航期间传递参数”。
在实例化,初始化并添加到目标区域之后,它将成为活动视图,并且停用先前视图。有时您会希望从区域中删除已停用的视图。Prism提供IRegionMemberLifetime接口,允许您指定是否要从区域中删除已停用的视图或仅将其标记为已停用,从而控制区域内视图的生命周期。
C#复制
public class EmployeeDetailsViewModel : IRegionMemberLifetime
{
public bool KeepAlive
{
get { return true; }
}
}
所述IRegionMemberLifetime接口定义的单个只读属性,的KeepAlive。如果此属性返回false,则在停用视图时将从该区域中删除该视图。由于该区域不再具有对视图的引用,因此它有资格进行垃圾回收(除非应用程序中的某些其他组件维护对它的引用)。您可以在视图或视图模型类上实现此接口。虽然IRegionMemberLifetime接口主要用于在激活和取消激活期间管理区域内视图的生命周期,但在目标区域中激活新视图后,还会在导航期间考虑KeepAlive属性。
注意
可以显示多个视图的区域(例如使用ItemsControl或TabControl的区域)将显示非活动视图和活动视图。从这些类型的区域中删除非活动视图将导致视图从UI中移除。
要在应用程序中实现所需的导航行为,通常需要在导航请求期间指定其他数据,而不仅仅是目标视图名称。所述NavigationContext对象提供对导航URI,并在其内被指定的或外部的任何参数。您可以从IsNavigationTarget,OnNavigatedFrom和OnNavigatedTo方法中访问NavigationContext。
Prism提供NavigationParameters类以帮助指定和检索导航参数。该NavigationParameters类维护名称-值对,每个参数列表。您可以使用此类将参数作为导航URI的一部分传递或传递对象参数。
以下代码示例演示如何将单个字符串参数添加到NavigationParameters实例,以便将其附加到导航URI。
C#复制
Employee employee = Employees.CurrentItem as Employee;
if (employee != null)
{
var navigationParameters = new NavigationParameters();
navigationParameters.Add("ID", employee.Id);
_regionManager.RequestNavigate(RegionNames.TabRegion,
new Uri("EmployeeDetailsView" + navigationParameters.ToString(), UriKind.Relative));
}
此外,您可以通过将对象参数添加到NavigationParameters实例并将其作为RequestNavigate方法的参数传递来传递对象参数。这在以下代码中显示。
C#复制
Employee employee = Employees.CurrentItem as Employee;
if (employee != null)
{
var parameters = new NavigationParameters();
parameters.Add("ID", employee.Id);
parameters.Add("myObjectParameter", new ObjectParameter());
regionManager.RequestNavigate(RegionNames.TabRegion,
new Uri("EmployeeDetailsView", UriKind.Relative), parameters);
}
您可以使用NavigationContext对象上的Parameters属性检索导航参数。此属性返回NavigationParameters类的实例,该类提供索引器属性以允许轻松访问各个参数,而不管它们是通过查询还是通过RequestNavigate方法传递。
C#复制
public void OnNavigatedTo(NavigationContext navigationContext)
{
string id = navigationContext.Parameters["ID"];
ObjectParameter myParameter = navigationContext.Parameters["myObjectParameter"];
}
通常,在导航期间重复使用,更新或激活应用程序中的视图更合适,而不是由新视图替换。这通常是您导航到相同类型的视图但需要向用户显示不同信息或状态的情况,或者当UI中已有适当视图但需要激活(即,选择或制作)时最顶部)。
对于第一个场景的示例,假设您的应用程序允许用户使用EditCustomer视图编辑客户记录,并且用户当前正在使用该视图编辑客户ID 123.如果客户决定编辑客户的客户记录ID 456,用户可以直接导航到EditCustomer视图并输入新的客户ID。然后,EditCustomer视图可以检索新客户的数据并相应地更新其UI。
第二种情况的示例是应用程序允许用户一次编辑多个客户记录。在这种情况下,应用程序在选项卡控件中显示多个EditCustomer视图实例 - 例如,一个用于客户ID 123,另一个用于客户ID 456.当用户导航到EditCustomer视图并输入客户ID 456时,相应的视图将是激活(即,将选择其相应的选项卡)。如果用户导航到EditCustomer视图并输入客户ID 789,则将创建一个新实例并显示在选项卡控件中。
由于各种原因,导航到现有视图的能力很有用。更新现有视图通常更有效,而不是使用相同类型的新实例替换它。同样,激活现有视图而不是创建重复视图可提供更一致的用户体验。此外,无需多少自定义代码即可无缝处理这些情况,这意味着应用程序更易于开发和维护。
Prism支持通过前面描述的两种方案IsNavigationTarget on方法INavigationAware接口。在导航期间,在与目标视图类型相同的区域中的所有视图上调用此方法。在前面的示例中,视图的目标类型是EditCustomer视图,因此将在当前位于该区域中的所有现有EditCustomer视图实例上调用IsNavigationTarget方法。Prism从视图URI确定目标类型,它假定它是目标类型的短类型名称。
注意
要使Prism确定目标视图的类型,导航URI中的视图名称应与实际目标类型的短类型名称相同。例如,如果您的视图由MyApp.Views.EmployeeDetailsView类实现,则导航URI中指定的视图名称应为EmployeeDetailsView。这是Prism提供的默认行为。您可以通过实现自定义内容加载器类来自定义此行为; 您可以通过实现IRegionNavigationContentLoader接口或从RegionNavigationContentLoader类派生来实现此目的。
IsNavigationTarget方法的实现可以使用NavigationContext参数来确定它是否可以处理导航请求。所述NavigationContext对象提供对导航URI和导航参数。在前面的示例中,EditCustomer视图模型中此方法的实现将当前客户ID与导航请求中指定的ID进行比较,如果匹配则返回true。
C#复制
public bool IsNavigationTarget(NavigationContext navigationContext)
{
string id = navigationContext.Parameters["ID"];
return _currentCustomer.Id.Equals(id);
}
如果IsNavigationTarget方法始终返回true,则无论导航参数如何,都将始终重用该视图实例。这允许您确保在特定区域中仅显示特定类型的一个视图。
您经常会发现在导航操作期间需要与用户进行交互,以便用户可以确认或取消它。例如,在许多应用中,用户可以在输入或编辑数据的过程中尝试导航。在这些情况下,您可能想要询问用户是否要在继续离开页面之前保存或丢弃已输入的数据,或者用户是否想要完全取消导航操作。Prism通过IConfirmNavigationRequest接口支持这些场景。
所述IConfirmNavigationRequest接口从所述派生INavigationAware接口并添加ConfirmNavigationRequest方法。通过在视图或视图模型类上实现此接口,您允许它们以允许用户与用户交互的方式参与导航序列,以便用户可以确认或取消导航。您将经常使用交互请求对象,如在高级MVVM方案中使用交互请求对象中所述,以显示确认弹出窗口。
注意
该ConfirmNavigationRequest方法称为活动视图或视图模型,类似于OnNavigatedFrom前面描述的方法。
该ConfirmNavigationRequest方法提供了两个参数,如前文所述当前导航背景下,当你想要导航,继续,你可以调用回调方法的参考。因此,回调称为延续回调。您可以存储对continuation回调的引用,以便应用程序在完成与用户交互后调用它。如果您的应用程序通过交互请求对象与用户交互,您可以将调用链接到交互请求中的回调的连续回调。下图说明了整个过程。
使用InteractionRequest对象确认导航
以下步骤总结了使用InteractionRequest对象确认导航的过程:
为了说明这一点,请查看View-Switching Navigation Quick Start。此应用程序使用户能够使用ComposeEmailView和ComposeEmailViewModel类撰写新电子邮件。视图模型类实现IConfirmNavigation接口。如果用户导航(例如通过单击“ 日历”按钮),则在编写电子邮件时,将调用ConfirmNavigationRequest方法,以便视图模型可以确认与用户的导航。为了支持这一点,视图模型类定义了交互请求,如以下代码示例所示。
C#复制
public class ComposeEmailViewModel : NotificationObject, IConfirmNavigationRequest
{
. . .
private readonly InteractionRequest
confirmExitInteractionRequest;
public ComposeEmailViewModel(IEmailService emailService)
{
. . .
this.confirmExitInteractionRequest = new
InteractionRequest();
}
public IInteractionRequest ConfirmExitInteractionRequest
{
get { return this.confirmExitInteractionRequest; }
}
}
在ComposeEmailVew类中,定义了交互请求触发器,并将数据绑定到视图模型上的ConfirmExitInteractionRequest属性。当进行交互请求时,将向用户显示简单的弹出窗口。
XAML复制
<UserControl.Resources>
<DataTemplate x:Key="ConfirmExitDialogTemplate">
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
Text="{Binding}"/>
DataTemplate>
UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<ei:Interaction.Triggers> <prism:InteractionRequestTrigger SourceObject="{Binding ConfirmExitInteractionRequest}"> <prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True"/> prism:InteractionRequestTrigger>ei:Interaction.Triggers>
...
在ConfirmNavigationRequest对方法ComposeEmailVewModel类,如果用户尝试导航而电子邮件正在被由被调用。此方法的实现调用先前定义的交互请求,以便用户可以确认或取消导航操作。
C#复制
void IConfirmNavigationRequest.ConfirmNavigationRequest(
NavigationContext navigationContext, Action<bool> continuationCallback)
{
. . .
this.confirmExitInteractionRequest.Raise(
new Confirmation {Content = "...", Title = "..."},
c => {continuationCallback(c.Confirmed);});
}
当用户单击确认弹出窗口中的按钮以确认或取消操作时,将调用交互请求的回调。此回调只调用continuation回调,传入Confirmed标志的值,并导致导航继续或被取消。
注意
应该注意的是,在引发交互请求事件之后,立即返回ConfirmNavigationRequest方法,以便用户可以继续与应用程序的UI交互。当用户单击弹出窗口上的“ 确定”或“ 取消”按钮时,将生成交互请求的回调方法,该方法又调用继续回调以完成导航操作。在UI线程上调用所有方法。使用此技术,不需要后台线程。
使用此机制,您可以控制导航请求是立即执行还是延迟执行,等待与用户的交互或某些其他异步交互(例如,作为Web服务请求的结果)。要启用导航,您只需调用continuation回调方法,传递true即表示它可以继续。同样,您可以传递false以指示应取消导航。
C#复制
void IConfirmNavigationRequest.ConfirmNavigationRequest(
NavigationContext navigationContext, Action<bool> continuationCallback)
{
continuationCallback(true);
}
如果要延迟导航,可以存储对继续回调的引用,然后在与用户(或Web服务)的交互完成时调用。在您调用continuation回调之前,导航操作将处于暂挂状态。
如果用户同时启动另一个导航操作,则导航请求将被取消。在这种情况下,调用continuation回调没有任何效果,因为它所涉及的导航操作不再是最新的。同样,如果您决定不调用延续回调,则导航操作将处于暂挂状态,直到将其替换为新的导航操作。
所述NavigationContext类提供进入该地区导航服务,它负责的区域内导航期间协调操作的序列。它提供对正在进行导航的区域以及与该区域相关联的导航日志的访问。区域导航服务实现IRegionNavigationService,其定义如下。
C#复制
public interface IRegionNavigationService : INavigateAsync
{
IRegion Region {get; set;}
IRegionNavigationJournal Journal {get;}
event EventHandler Navigating;
event EventHandler Navigated;
event EventHandler NavigationFailed;
}
由于区域导航服务实现了INavigateAsync接口,因此您可以通过调用其RequestNavigate方法在父区域内启动导航。在导航开始导航操作时引发事件。在导航中的区域内的导航结束时引发事件。该NavigationFailed如果导航过程中遇到的错误引发。
该杂志属性提供与该区域相关的导航日记。导航日志实现IRegionNavigationJournal接口,其定义如下。
C#复制
public interface IRegionNavigationJournal
{
bool CanGoBack { get; }
bool CanGoForward { get; }
IRegionNavigationJournalEntry CurrentEntry { get; }
INavigateAsync NavigationTarget { get; set; }
void Clear();
void GoBack();
void GoForward();
void RecordNavigation(IRegionNavigationJournalEntry entry);
}
您可以在导航期间通过OnNavigatedTo方法调用获取并存储对视图中区域导航服务的引用。默认情况下,Prism提供了一个简单的基于堆栈的日志,允许您在区域内向前或向后导航。
您可以使用导航日志允许用户从视图本身进行导航。在以下示例中,视图模型实现了GoBack命令,该命令使用主机区域中的导航日志。因此,视图可以显示“ 后退”按钮,允许用户轻松导航回区域内的上一个视图。同样,您可以实现GoForward命令来实现向导样式工作流。
C#复制
public class EmployeeDetailsViewModel : INavigationAware
{
...
private IRegionNavigationService navigationService;
public void OnNavigatedTo(NavigationContext navigationContext)
{
navigationService = navigationContext.NavigationService;
}
public DelegateCommand<object> GoBackCommand { get; private set; }
private void GoBack(object commandArg)
{
if (navigationService.Journal.CanGoBack)
{
navigationService.Journal.GoBack();
}
}
private bool CanGoBack(object commandArg)
{
return navigationService.Journal.CanGoBack;
}
}
如果需要在该区域内实现特定的工作流模式,则可以为区域实现自定义日记。
注意
导航日志只能用于由区域导航服务协调的基于区域的导航操作。如果使用视图发现或视图注入来实现区域内的导航,则导航日志将不会在导航期间更新,也不能用于在该区域内向前或向后导航。
Prism区域导航旨在解决在使用MVVM模式和依赖注入容器(如Unity)或Managed Extensibility Framework的松散耦合的模块化应用程序中实现导航时可能遇到的各种常见场景和挑战。 (MEF)。它还旨在支持导航确认和取消,导航到现有视图,导航参数和导航日志。
通过支持Prism区域内的导航,它还支持在各种布局控件中进行导航,并支持在不影响其导航结构的情况下更改应用程序UI的布局。它还支持伪同步导航,允许在导航期间进行丰富的用户交互。
但是,Prism区域导航并非旨在取代WPF的导航框架。相反,Prism区域导航被设计为与WPF导航框架并排使用。
WPF导航框架很难用于支持MVVM模式和依赖注入。它还基于Frame控件,在日记和导航UI方面提供类似的功能。您可以将WPF导航框架与Prism区域导航一起使用,但仅使用Prism区域实现导航可能更容易,也更灵活。
下图提供了导航操作期间操作顺序的概述。它仅供参考,以便您可以在导航请求期间查看Prism区域导航的各种元素如何协同工作。
Prism区域导航序列
在构建大型复杂WPF应用程序时,常见的方法是将功能划分为离散模块程序集。还希望最小化这些模块之间静态引用的使用,这可以通过使用委托命令,区域上下文,共享服务和事件聚合器来实现。这允许模块被独立地开发,测试,部署和更新,并且它迫使松散耦合的通信。本主题提供有关何时使用委托命令和路由命令以及何时使用事件聚合器和.NET框架事件的指导。
在模块之间进行通信时,了解方法之间的差异非常重要,这样您才能最好地确定在特定方案中使用哪种方法。Prism Library提供以下通信方法:
如果您需要响应用户手势,例如单击命令调用程序(例如,按钮或菜单项),并且您希望基于业务逻辑启用调用程序,请使用命令。
Windows Presentation Foundation(WPF)提供RoutedCommand,它擅长将命令调用程序(如菜单项和按钮)与命令处理程序连接,命令处理程序与具有键盘焦点的可视树中的当前项相关联。
但是,在复合方案中,命令处理程序通常是视图模型,在可视树中没有任何关联元素,或者不是焦点元素。为了支持这种情况,Prism库提供了DelegateCommand,它允许您在执行命令时调用委托方法,而CompositeCommand允许您组合多个命令。这些命令与内置的RoutedCommand不同,后者将在可视树上上下路由命令执行和处理。这允许您在可视树中的某个点触发命令并在更高级别处理它。
该CompositeCommand是一个实现ICommand的,以便它可以被绑定到调用者。CompositeCommands可以连接到几个子命令; 调用CompositeCommand时,也会调用子命令。
CompositeCommands支持启用。CompositeCommands侦听每个连接命令的CanExecuteChanged事件。然后它会引发此事件通知其调用者。调用者通过在CompositeCommand上调用CanExecute来对此事件做出反应。然后,CompositeCommand通过在每个子命令上调用CanExecute来再次轮询其所有子命令。如果对CanExecute的任何调用返回false,则CompositeCommand将返回false,从而禁用调用者。
这对于跨模块通信有何帮助?基于Prism库的应用程序可能具有在shell中定义的全局CompositeCommands,它们具有跨模块的含义,例如Save,Save All和Cancel。然后,模块可以使用这些全局命令注册其本地命令并参与其执行。
注意:DelegateCommand和CompositeCommands可以在位于Prism.Mvvm NuGet包中的Microsoft.Practices.Prism.Mvvm命名空间中找到。
关于WPF路由事件和路由命令
路由事件是一种事件,可以在元素树中的多个侦听器上调用处理程序,而不是仅通知直接订阅该事件的对象。WPF路由命令通过可视树中的UI元素传递命令消息,但树外的元素将不会接收这些消息,因为它们仅从聚焦元素或明确声明的目标元素向上或向下冒泡。路由事件可用于通过元素树进行通信,因为事件的事件数据会持续到路由中的每个元素。一个元素可能会更改事件数据中的某些内容,并且该更改可用于路径中的下一个元素。
因此,您应该在以下方案中使用WPF路由事件:在公共根目录定义公共处理程序或定义您自己的自定义控件类。****
要创建委托命令,请在视图模型的构造函数中实例化DelegateCommand字段,然后将其作为ICommand属性公开。
C#复制
// ArticleViewModel.cs
public class ArticleViewModel : BindableBase
{
private readonly ICommand showArticleListCommand;
public ArticleViewModel(INewsFeedService newsFeedService,
IRegionManager regionManager,
IEventAggregator eventAggregator)
{
this.showArticleListCommand = new DelegateCommand(this.ShowArticleList);
}
public ICommand ShowArticleListCommand
{
get { return this.showArticleListCommand; }
}
}
要创建复合命令,请在构造函数中实例化CompositeCommand字段,向其中添加命令,然后将其作为ICommand属性公开。
C#复制
public class MyViewModel : BindableBase
{
private readonly CompositeCommand saveAllCommand;
public ArticleViewModel(INewsFeedService newsFeedService,
IRegionManager regionManager,
IEventAggregator eventAggregator)
{
this.saveAllCommand = new CompositeCommand();
this.saveAllCommand.RegisterCommand(new SaveProductsCommand());
this.saveAllCommand.RegisterCommand(new SaveOrdersCommand());
}
public ICommand SaveAllCommand
{
get { return this.saveAllCommand; }
}
}
通常,要创建全局可用命令,请创建DelegateCommand或CompositeCommand的实例,并通过静态类公开它。
C#复制
public static class GlobalCommands
{
public static CompositeCommand MyCompositeCommand = new CompositeCommand();
}
在您的模块中,将子命令与全局可用命令相关联。
C#复制
GlobalCommands.MyCompositeCommand.RegisterCommand(command1);
GlobalCommands.MyCompositeCommand.RegisterCommand(command2);
注意
为了提高代码的可测试性,可以使用代理类访问全局可用的命令并在测试中模拟该代理类。
以下代码示例演示如何将按钮绑定到WPF中的命令。
XAML复制
<Button Name="MyCompositeCommandButton" Command="{x:Static local:GlobalCommands.MyCompositeCommand}">Execute My Composite Command Button>
注意
另一种方法是将命令作为资源存储在Application.Resources部分的App.xaml文件中。然后,在视图中 - 必须在设置该资源后创建 - 您可以设置Command =“{Binding MyCompositeCommand,Source = {StaticResource GlobalCommands}}”以向命令添加调用者。
在很多情况下,您可能希望在托管区域的视图和区域内的视图之间共享上下文信息。例如,类似主细节的视图显示业务实体并公开区域以显示该业务实体的其他详细信息。Prism Library使用名为RegionContext的概念来共享区域主机与区域内加载的任何视图之间的对象,如下图所示。
使用RegionContext
根据具体情况,您可以选择共享单条信息(例如标识符)或共享模型。视图可以检索RegionContext,然后注册更改通知。视图还可以更改RegionContext的值。有几种方法可以公开和使用RegionContext:
注意
如果该视图是DependencyObject,Prism Library目前仅支持从区域内的视图中使用RegionContext。如果您的视图不是DependencyObject(例如,您正在使用WPF自动数据模板并直接在区域中添加视图模型),请考虑创建自定义RegionBehavior以将RegionContext转发到视图对象。
注意
关于数据上下文属性
数据上下文是一种允许元素从其父元素继承有关用于绑定的数据源的信息的概念。子元素自动继承其父元素的DataContext。数据沿着可视化树向下流动。
跨模块通信的另一种方法是通过共享服务。加载模块后,模块会将其服务添加到服务定位器。通常,通过公共接口类型从服务定位器注册和检索服务。这允许模块使用其他模块提供的服务,而无需对模块进行静态引用。服务实例在模块之间共享,因此您可以共享数据并在模块之间传递消息。
在Stock Trader参考实施(Stock Trader RI)中,Market模块提供了IMarketFeedService的实现。Position模块通过使用shell应用程序的依赖注入容器来使用这些服务,该容器提供服务位置和解析。所述IMarketFeedService是指由其他模块被消耗,因此它可以在找到StockTraderRI.Infrastructure共同组装,但该接口的具体实现并不需要被共享的,所以它是市场模块中直接定义,并且可以是独立于其他模块更新。
要查看这些服务如何导出到MEF,请参阅MarketFeedService.cs和MarketHistoryService.cs文件,如以下代码示例所示。Position模块的ObservablePosition通过构造函数依赖注入接收IMarketFeedService服务。
C#复制
// MarketFeedService.cs
[Export(typeof(IMarketFeedService))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class MarketFeedService : IMarketFeedService, IDisposable
{
...
}
这有助于跨模块通信,因为服务使用者不需要对提供服务的模块的静态引用。此服务可用于在模块之间发送或接收数据。
注意
一些依赖注入容器允许使用属性注册依赖项,如此示例所示。其他容器可以使用显式注册。在这些情况下,注册通常在模块加载期间发生,此时Prism调用IModule.Initialize方法。有关更多信息,请参阅模块化应用程序开
Prism库提供了一种事件机制,可以在应用程序中松散耦合的组件之间进行通信。该机制基于事件聚合器服务,允许发布者和订阅者通过事件进行通信,但仍然没有彼此直接引用。
该EventAggregator提供组播发布/订阅功能。这意味着可以有多个发布者引发相同的事件,并且可以有多个订阅者收听同一事件。考虑使用EventAggregator跨模块发布事件,以及在业务逻辑代码(如控制器和演示者)之间发送消息时。
Stock Trader RI的一个例子就是点击Process Order按钮并顺序处理订单; 在这种情况下,其他模块需要知道订单已成功处理,以便他们可以更新他们的视图。
使用Prism Library创建的事件是键入的事件。这意味着您可以在运行应用程序之前利用编译时类型检查来检测错误。在Prism库中,EventAggregator允许订阅者或发布者定位特定的EventBase。事件聚合器还允许多个发布者和多个订阅者,如下图所示。
事件聚合器
注意
关于.NET Framework事件
如果不要求松散耦合,则使用.NET Framework事件是组件之间通信的最简单,最直接的方法。.NET Framework中的事件实现了Publish-Subscribe模式,但是要订阅对象,您需要直接引用该对象,在复合应用程序中,该对象通常驻留在另一个模块中。这导致紧密耦合的设计。因此,.NET Framework事件用于模块内的通信,而不是模块之间的通信。
如果使用.NET Framework事件,则必须非常小心内存泄漏,尤其是如果您有一个非静态或短期组件,它可以在静态或长寿命的事件上订阅事件。如果您没有取消订阅订阅者,则发布者将保留该订阅者,这将阻止第一个订阅者被垃圾收集。
所述EventAggregator类被提供作为在容器中的服务,并且可以通过检索IEventAggregator接口。事件聚合器负责定位或构建事件以及保留系统中事件的集合。
C#复制
public interface IEventAggregator
{
TEventType GetEvent() where TEventType : EventBase;
}
该EventAggregator构建事件在其第一次访问,如果它尚未建立。这使发布者或订阅者无需确定事件是否可用。
连接发布者和订阅者的真正工作是由PubSubEvent类完成的。这是Prism库中包含的EventBase类的唯一实现。此类维护订户列表并处理订阅者的事件调度。
所述PubSubEvent类是一个一般类,需要将其定义为一般类型的有效载荷类型。这有助于在编译时强制发布者和订阅者提供成功事件连接的正确方法。以下代码显示了PubSubEvent类的部分定义。
注意
PubSubEvent可以在位于Prism.PubSubEvents NuGet包中的Microsoft.Practices.SubSubEvents命名空间中找到。
C#复制
// PubSubEvent.cs
public class PubSubEvent<TPayload> : EventBase
{
...
public SubscriptionToken Subscribe(Action action);
public SubscriptionToken Subscribe(Action action, ThreadOption threadOption);
public SubscriptionToken Subscribe(Action action, bool keepSubscriberReferenceAlive)
public SubscriptionToken Subscribe(Action action, ThreadOption threadOption, bool keepSubscriberReferenceAlive)
public virtual SubscriptionToken Subscribe(Action action, ThreadOption threadOption, bool keepSubscriberReferenceAlive);
public virtual SubscriptionToken Subscribe(Action action, ThreadOption threadOption, bool keepSubscriberReferenceAlive, Predicate filter);
public virtual void Publish(TPayload payload);
public virtual void Unsubscribe(Action subscriber);
public virtual bool Contains(Action subscriber)
...
}
以下各节介绍如何创建,发布和订阅PubSubEvent使用IEventAggregator接口。
所述PubSubEvent
例如,以下代码显示了股票交易者参考实现(Stock Trader RI)中的TickerSymbolSelectedEvent。有效负载是包含公司符号的字符串。请注意此类的实现是如何为空。
C#复制
public class TickerSymbolSelectedEvent : PubSubEvent<string>{}
注意
在复合应用程序中,事件经常在多个模块之间共享,因此它们在公共位置定义。在Stock Trader RI中,这是在StockTraderRI.Infrastructure项目中完成的。
发布者通过从EventAggregator检索事件并* 调用* Publish方法来引发事件。要访问EventAggregator,可以通过向类构造函数添加类型为IEventAggregator的参数来使用依赖项注入。****
以下代码演示了如何发布TickerSymbolSelectedEvent。
C#复制
this.eventAggregator.GetEvent().Publish("STOCK0");
订阅者可以使用PubSubEvent类上提供的一个Subscribe方法重载来登记事件。有几种方法可以订阅PubSubEvents。使用以下标准来帮助确定最适合您需求的选项:
以下部分描述了这些选项。
订阅者通常需要更新UI元素以响应事件。在WPF中,只有UI线程可以更新UI元素。
默认情况下,订阅者在发布者的线程上接收事件。如果发布者从UI线程发送事件,则订阅者可以更新UI。但是,如果发布者的线程是后台线程,则订阅者可能无法直接更新UI元素。在这种情况下,订户需要使用Dispatcher类在UI线程上安排更新。
Prism Library提供的PubSubEvent可以通过允许订阅者在UI线程上自动接收事件来提供帮助。订阅者在订阅期间指示此信息,如以下代码示例所示。
C#复制
public void Run()
{
...
this.eventAggregator.GetEvent().Subscribe(ShowNews, ThreadOption.UIThread);
);
}
public void ShowNews(string companySymbol)
{
this.articlePresentationModel.SetTickerSymbol(companySymbol);
}
ThreadOption有以下选项:
注意
为了让PubSubEvents在UI线程上发布给订阅者,必须首先在UI线程上构造EventAggregator。
订阅者可能不需要处理已发布事件的每个实例。在这些情况下,订户可以使用过滤器参数。的滤波器参数的类型的System.Predicate
通常,此过滤器作为lambda表达式提供,如以下代码示例所示。
C#复制
FundAddedEvent fundAddedEvent = this.eventAggregator.GetEvent();
fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.UIThread, false,
fundOrder => fundOrder.CustomerId == this.customerId);
注意
Subscribe方法返回Microsoft.Practices.Prism.Events.SubscriptionToken类型的订阅令牌,可用于稍后删除对该事件的订阅。当您使用匿名委托或lambda表达式作为回调委托时,或者使用不同的过滤器订阅相同的事件处理程序时,此标记特别有用。
建议不要在回调委托中修改有效负载对象,因为多个线程可能同时访问有效负载对象。您可以使有效负载不可变,以避免并发错误。
如果您在短时间内提出多个事件并注意到它们的性能问题,则可能需要使用强委托引用进行订阅。如果您这样做,则需要在处置订户时手动取消订阅该事件。
默认情况下,PubSubEvent维护对订阅者处理程序的弱委托引用,并对订阅进行过滤。这意味着PubSubEvent所持有的引用不会阻止订阅者的垃圾收集。使用弱委托引用可以使订户免于取消订阅并允许正确的垃圾收集。
但是,维护此弱委托引用比相应的强引用要慢。对于大多数应用程序,此性能不会很明显,但如果您的应用程序在短时间内发布大量* 事件,您可能需要使用带有* PubSubEvent的强引用。如果您确实使用了强委托引用,则订阅者应该取消订阅,以便在不再使用订阅对象时启用正确的垃圾回收。
要使用强引用进行订阅,请在Subscribe方法上使用keepSubscriberReferenceAlive参数,如以下代码示例所示。
C#复制
FundAddedEvent fundAddedEvent = eventAggregator.GetEvent();
bool keepSubscriberReferenceAlive = true;
fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.UIThread, keepSubscriberReferenceAlive, fundOrder => fundOrder.CustomerId == _customerId);
所述keepSubscriberReferenceAlive参数的类型的布尔:
对于最小订阅或默认订阅,订阅者必须提供具有接收事件通知的适当签名的回调方法。例如,TickerSymbolSelectedEvent的处理程序要求该方法采用字符串参数,如以下代码示例所示。
C#复制
public TrendLineViewModel(IMarketHistoryService marketHistoryService, IEventAggregator eventAggregator)
{
... eventAggregator.GetEvent().Subscribe(this.TickerSymbolChanged);
}
public void TickerSymbolChanged(string newTickerSymbol)
{
MarketHistoryCollection newHistoryCollection = this.marketHistoryService.GetPriceHistory(newTickerSymbol);
this.TickerSymbol = newTickerSymbol;
this.HistoryCollection = newHistoryCollection;
}
如果您的订户不再想要接收活动,您可以使用订阅者的处理程序取消订阅,也可以使用订阅令牌取消订阅。
以下代码示例演示如何直接取消订阅处理程序。
C#复制
FundAddedEvent fundAddedEvent = this.eventAggregator.GetEvent();
fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.PublisherThread);
fundAddedEvent.Unsubscribe(FundAddedEventHandler);
以下代码示例演示如何取消订阅订阅令牌。令牌作为Subscribe方法的返回值提供。
C#复制
FundAddedEvent fundAddedEvent = this.eventAggregator.GetEvent();
subscriptionToken = fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.UIThread, false, fundOrder => fundOrder.CustomerId == this.customerId);
fundAddedEvent.Unsubscribe(subscriptionToken);
引导程序。负责初始化使用Prism库构建的应用程序的类。
命令。一种松散耦合的方式,您可以处理用户界面(UI)操作。命令将UI手势绑定到执行操作的逻辑。
复合应用。复合应用程序由许多离散和独立的模块组成。这些组件在主机环境中集成在一起,形成一个单一的无缝应用程序。
复合命令。具有多个子命令的命令。
容器。为创建对象提供一个抽象层。依赖注入容器可以通过提供实例化类的实例并根据容器的配置管理它们的生命周期来减少对象之间的依赖关系。
DelegateCommand。允许将命令处理逻辑委派给选定的方法,而不是在代码隐藏中需要处理程序。它使用.NET Framework委托作为调用目标处理方法的方法。
EventAggregator。一种服务,主要是事件的容器,允许发布者和订阅者分离,以便他们可以独立发展。这种解耦在模块化应用程序中很有用,因为可以添加新模块来响应shell或其他模块定义的事件。
模块化。能够从名为模块的离散功能单元创建复杂应用程序。当您以模块化方式开发时,您可以将应用程序组织成单独的模块,这些模块可以由不同的团队单独开发,测试和部署。它还通过在UI和业务功能之间保持清晰的分离,帮助您解决问题的分离。
模特。封装应用程序的业务逻辑和数据。
Model-View-ViewModel(MVVM)。MVVM模式有助于将应用程序的业务和表示逻辑与其用户界面(UI)完全分离。在应用程序逻辑和UI之间保持清晰的分离有助于解决许多开发和设计问题,并使应用程序更易于测试,维护和发展。
模块。应用程序中的逻辑分离单元。
ModuleCatalog。定义最终用户运行应用程序所需的模块。模块目录知道模块的位置和模块的依赖关系。
ModuleManager。管理验证模块目录的过程的主类,如果模块是远程的则检索模块,将模块加载到应用程序域中,以及调用模块的Initialize方法。
模块管理阶段。导致模块初始化的阶段。这些阶段是模块发现,模块加载和模块初始化。
导航。应用程序通过用户与应用程序交互或由于内部应用程序状态更改而更改其UI的过程。
ViewModel-第一个组合。组合方法首先在逻辑上创建视图模型,然后是视图。
通知。当基础属性值更改时,向视图中的任何数据绑定控件提供更改通知。这是实现MVVM模式所必需的,并且是使用BindableBase类实现的。
按需模块。仅在应用程序明确请求时检索和初始化的模块。
地区。一个命名位置,可用于定义视图的显示位置。模块可以在布局中的区域中定位和添加内容,而无需准确了解区域在视觉上的显示方式和位置。这允许更改外观和布局,而不会影响将内容添加到布局的模块。
RegionContext。一种技术,可用于在父视图和区域中托管的子视图之间共享上下文。所述RegionContext可以通过代码或通过使用数据绑定XAML来设置。
RegionManager。负责维护区域集合并为控件创建新区域的类。所述RegionManager发现映射到WPF控制适配器和一个新的区域,以该控制相关联。所述RegionManager还提供附加的属性,该属性可以被用于从XAML简单区域创建。
分离的演示模式。用于实现视图的模式,它将表示和业务逻辑与UI分开。使用单独的表示允许独立于UI测试表示和业务逻辑,使维护代码更容易,并增加重用机会。
贝壳。WPF应用程序的主窗口,其中包含主要UI内容。
范围区域。属于特定区域范围的区域。区域范围由父视图分隔,并包括父视图的所有子视图。
服务。服务通过接口以松散耦合的方式向其他模块提供功能,并且通常是单例。
基于状态的导航。通过状态更改到可视树中的现有控件完成导航。
UI组合。通过在运行时从离散视图组合接口来构建接口的行为,可能来自单独的模块。
查看。复合UI应用程序中UI构造的主要单元。该视图封装了您希望保持与应用程序其他部分尽可能分离的UI和UI逻辑。您可以将视图定义为用户控件,数据模板甚至自定义控件。
基于视图的导航。通过添加或删除可视树中的元素来完成导航。
视图优先组成。组合方法首先在逻辑上创建视图,然后是视图模型或它所依赖的演示者。
查看发现。通过将视图类型与区域名称相关联来添加,显示或删除区域中的视图的方法。只要显示具有该名称的区域,就会自动创建已注册的视图并将其添加到该区域。
查看注射。通过向区域添加或删除视图实例来添加,显示或删除区域中的视图的方法。与区域交互的代码不直接了解区域将如何处理显示视图。
查看模型。封装视图的表示逻辑和状态。它负责协调视图与所需的任何模型类的交互。
查看模型位置。通常使用约定基本方法来定位和实例化视图模型并关联到它们各自的视图。
构建应用程序时,通常会遇到或使用模式。在Prism库和示例参考实现中,该指南演示了适配器,应用程序控制器,命令,复合和复合视图,依赖注入,事件聚合器,外观,控制反转,观察者,模型 - 视图 - 视图模型(MVVM),注册表,存储库,分离接口,插件和服务定位器模式,在本附录中简要讨论。下图显示了使用Prism库和一些常见模式的典型复合应用程序体系结构。使用Prism时,更简单的应用程序可能会遇到一些这些模式,但不一定都是这些模式。
具有通用模式的示例复合应用程
本节按字母顺序提供模式的简要概述,以及指向Prism代码中每个模式示例的指针。
顾名思义,适配器模式适应一个类的接口以匹配另一个类所期望的接口。在Prism库中,适配器模式用于使区域适应Windows Presentation Foundation(WPF)ItemsControl,ContentControl和Selector。要查看应用的Adapters模式,请参阅Prism Library中的ItemsControlRegionAdapter.cs文件。
Application Controller模式允许您将创建和显示视图的职责分离到控制器类中。这种控制器与MVC应用程序中的控制器略有不同。应用程序控制器的职责是封装视图表示的控件。它可以处理实例化视图; 它通过将它们放在用户界面(UI)中的适当容器中,在共享同一容器的视图之间切换,有时协调视图或视图模型之间的通信来实现。即使模式的名称是Application Controller,控制器通常也限定为应用程序的子集,例如Prism应用程序中的模块控制器或跨越一组相关视图的控制器。结果是,在Prism应用程序中,您通常会有多个控制器。有关此模式的示例实现,请参阅股票交易者参考实施(Stock Trader RI)中的OrdersController类。
Command模式是一种设计模式,其中对象用于表示操作。命令对象封装了一个动作及其参数。这允许命令的调用者和命令的处理程序的解耦。Prism.Mvvm库提供了一个CompositeCommand,它允许组合多个ICommand项和一个DelegateCommand,它允许ViewModel或控制器提供连接到本地方法以执行和通知执行能力的ICommand。要查看Stock Trader RI中CompositeCommand和DelegateCommand的用法,请参阅StockTraderRICommands.cs和OrderDetailsViewModel.cs文件。
复合应用程序的核心是将各个视图组合到复合视图中的能力。组合视图通常定义子视图的布局。例如,应用程序的shell可以定义导航区域和内容区域以在运行时托管子视图,如下图所示。
作文示例
在Stock Trader RI中,可以通过使用shell中的区域来看到这一点。shell定义了模块在初始化过程中定位和添加视图的区域。有关定义区域的示例,请参见Shell.xaml文件。
复合视图不必动态组合,就像使用Prism区域时一样。复合视图也可以是由几个其他子视图构成的视图,这些视图通过UI定义静态组合。一个例子是在可扩展应用程序标记语言(XAML)中声明的子用户控件。
依赖注入模式是控制反转模式的专用版本(在本附录后面描述),其中被反转的关注点是获得所需依赖关系的过程。在整个Stock Trader RI和Prism Library中使用依赖注入。使用容器时,构造的责任放在容器而不是消耗类上。在对象构造期间,依赖项注入容器解析任何外部依赖项。因此,随着系统的发展,可以更容易地更改依赖关系的具体实现。由于更松散的耦合,这更好地支持系统的可测试性和增长。Stock Trader RI使用托管可扩展性框架(MEF)来帮助管理组件之间的依赖关系。然而,Prism库本身并不依赖于特定的依赖注入容器; 您可以自由选择所需的依赖注入容器,但必须提供实现该容器的适配器IServiceLocator接口。Prism Library为MEF和Unity应用程序块(Unity)提供适配器。要查看在Stock Trader RI中通过注入解析其依赖项的组件示例,请参阅NewsController.cs文件中的构造函数。有关使用Unity的示例,请参阅UI Composition QuickStart中的ModuleInit类。
Event Aggregator模式通过单个对象引导来自多个对象的事件,以简化客户端的注册。在Prism库中,Event Aggregator模式的变体允许多个对象定位,发布或订阅事件。要查看EventAggregator及其管理的事件,看到EventAggregator和PubSubEvent的Prism.PubSubEvents资源库中 要查看Stock Trader RI中EventAggregator的用法,请参阅文件**** WatchListViewModel.cs。
Façade模式简化了更复杂的接口或接口集,以简化其使用或隔离对这些接口的访问。Prism Library为容器和日志记录服务提供外观,以帮助将库与这些服务中的更改隔离开来。这允许图书馆的消费者提供与Prism图书馆合作的自己的服务。该IServiceLocator和ILoggerFacade接口****定义的外观界面,当它与容器或日志记录服务通信的棱镜库的预期。
通常,控制反转(IoC)模式用于在类或框架中实现可扩展性。例如,在某些执行点设计了事件模型的类通过允许事件侦听器在调用事件时采取操作来反转控制。
在Prism Library和Stock Trader RI中演示的两种形式的IoC模式包括依赖注入和模板方法模式。先前描述了依赖注入。在模板方法模式中,基类提供调用虚方法或抽象方法的配方或过程。因此,继承的类可以覆盖适当的方法以启用所需的行为。在Prism库中,这显示在UnityServiceLocatorAdapter类中。要查看使用模板模式的另一个示例,请参阅Stock Trader RI中的StockTraderRIBootstrapper.cs文件。
观察者模式试图将对象的状态变化感兴趣的人与变化的对象分离。在.NET Framework中,这通常是通过事件看到的。Prism演示了观察者模式的变体,以将与用户交互的请求与实际选择的交互分开。这是通过InteractionRequest对象完成的,该对象通常由Model-View-ViewModel(MVVM)模式中的视图模型提供。
此InteractionRequest是一个对象,它封装了视图监视的事件。当视图收到交互请求时,它可以选择如何处理交互。视图可以决定显示模态窗口以向用户提供反馈,或者它可以显示不显眼的通知而不中断用户的工作流程。将此请求作为对象提供了一种在WPF中将数据绑定到请求并指定响应的方法,而无需在视图中使用代码隐藏。
Presentation Model是几种UI模式之一,专注于将演示的逻辑与可视化表示分开。这样做是为了将视觉呈现的关注点与视觉逻辑的关注点分开,这有助于提高可维护性和可测试性。相关的UI模式包括模型 - 视图 - 控制器(MVC)和模型 - 视图 - 演示器(MVP)。在Prism的Stock Trader RI中演示的Model-View-ViewModel(MVVM)方法是Presentation Model模式的特定实现变体。
Prism库本身在分离UI模式的选择方面是中立的。虽然考虑到WPF中用于数据绑定,命令和行为的设施,但您可以成功使用任何模式,MVVM模式是推荐的方法,Prism指南提供文档和示例以帮助您开始使用MVVM。要查看Basic MVVM QuickStart中的MVVM示例,请参阅文件QuestionnaireView.xaml,QuestionnaireView.xaml.cs和QuestionnaireViewModel.cs。
Registry模式指定了一种从众所周知的对象中定位一个或多个对象的方法。在将视图类型与区域关联时,Prism库应用注册表模式。的IRegionViewRegistry接口和RegionViewRegistry类定义用于区域名称时被加载的那些区域创建的视图的类型相关联的注册。此注册表在UI Composition QuickStart的ModuleInit.cs文件中使用。
存储库允许您从需要数据的代码中分离获取应用程序数据的方式。存储库表示应用程序代码可以使用的域对象的集合,而无需耦合到检索这些对象的特定机制。域对象是应用程序模型的一部分,通过存储库获取这些对象,可以更改存储库检索和更新策略,而不会影响应用程序的其余部分。此外,存储库接口变得容易依赖于替换单元测试的目的。
在运行时定位和加载模块的能力为并行开发提供了更大的机会,扩展了模块部署选择,并鼓励更松散耦合的体系结构。以下模式启用此功能:
插件。此模式允许在运行时确定类的具体实现,以避免在更改使用哪个具体实现时或由于具体实现的更改而需要重新编译。在Prism库中,这是通过DirectoryModuleCatalog,ConfigurationModuleCatalog和ModuleInitializer来处理的,它们一起工作以定位和初始化IModule插件。有关支持插件的示例,请参阅Prism库中的文件DirectoryModuleCatalog.cs,ConfigurationModuleCatalog.cs和ModuleInitializer.cs。
注意
MEF旨在支持插件模型,允许组件以声明方式导出和导入具体实现。
Service Locator模式解决了依赖注入模式解决的相同问题,但它使用了不同的方法。它允许类定位他们感兴趣的特定服务,而无需知道谁实现了该服务。通常,这用作依赖注入的替代方法,但有时候类需要使用服务位置而不是依赖注入,例如当它需要解析服务的多个实现者时。在Prism库中,当ModuleInitializer服务解析单个IModule时可以看到这一点。有关使用UnityContainer在UI组合快速入门中查找服务的示例,请参阅文件ModuleInit.cs。