使用Prism Library for WPF编写用户界面
UI布局方案
在复合应用程序中,来自多个模块的视图会在运行时显示在应用程序UI中的特定位置。为此您需要定义视图出现的位置、视图的创建方式和在这些位置的显示方式。
视图与其在UI中的显示位置的解耦允许了应用程序的外观和布局独立于区域内出现的视图而发展。
接下来的部分描述了您在开发复合应用程序时将遇到的核心场景。
实现Shell
Shell是应用程序的根对象,其中包含了主要的UI内容。在WPF应用程序中,Shell是Window
对象。
Shell可以包含命名区域,模块可以在其中指定将要出现的视图。它还可以定义某些顶层UI元素,例如主菜单和工具栏。Shell定义了应用程序的整体结构和外观,类似于ASP.NET母版页控件。它可以定义在Shell布局本身中存在且可见的样式和边框,也可以定义应用于插入到Shell中的视图的样式、模板和主题。
要使用Prism库,您不需要将一个独立的Shell作为应用程序架构的一部分。而如果你正在构建一个全新的复合应用程序时,实现Shell可以为应用程序主UI的设立提供一个定义良好的根目录和初始化模式。然而如果您正在向现有的应用程序添加Prism库功能,则不必更改应用程序的基本架构来添加Shell。相反您可以更改现有的窗口定义或控件,根据需要来添加可以拉入视图的区域。
您的应用程序中也可以有多个Shell。如果您的应用程序被设计为为用户打开多个顶层窗口,每个顶层窗口都充当其包含内容的Shell。
Shell示例
这个示例有一个Shell作为它的主窗口。在下图中,Shell和视图已被突出显示。Shell是应用程序启动时出现的主窗口,其中包含所有视图。它定义了模块添加视图的区域和一些顶层UI项,包括标题和监视列表横幅。
应用程序中的Shell的实现由Shell.xaml、其代码后置文件Shell.xaml.cs及其视图模型ShellViewModel.cs提供。Shell.xaml包含Shell窗口的布局和UI项,包括模块向其添加视图的区域的定义。
下面的XAML展示了定义Shell的结构和主要的XAML元素。请注意附加属性RegionName
用于定义四个区域,以及窗口的背景图像为Shell提供了背景。
<!--Shell.xaml (WPF) -->
<Window x:Class="StockTraderRI.Shell">
<!--shell background -->
<Window.Background>
<ImageBrush ImageSource="Resources/background.png" Stretch="UniformToFill"/>
</Window.Background>
<Grid>
<!-- logo -->
<Canvas x:Name="Logo" ...>
<TextBlock Text="CFI" ... />
<TextBlock Text="STOCKTRADER" .../>
</Canvas>
<!-- main bar -->
<ItemsControl
x:Name="MainToolbar"
prism:RegionManager.RegionName="{x:Static inf:RegionNames.MainToolBarRegion}"/>
<!-- content -->
<Grid>
<Controls:AnimatedTabControl
x:Name="PositionBuySellTab"
prism:RegionManager.RegionName="{x:Static inf:RegionNames.MainRegion}"/>
</Grid>
<!-- details -->
<Grid>
<ContentControl
x:Name="ActionContent"
prism:RegionManager.RegionName="{x:Static inf:RegionNames.ActionRegion}"/>
</Grid>
<!-- sidebar -->
<Grid x:Name="SideGrid">
<Controls:ResearchControl
prism:RegionManager.RegionName="{x:Static inf:RegionNames.ResearchRegion}" />
</Grid>
</Grid>
</Window>
Shell
代码后置文件的实现非常简单。其中Shell
对象被暴露出来,这样当您的App
对象创建它时,它的依赖项也将被添加。
// Shell.xaml.cs
[Export]
public partial class Shell : Window
{
public Shell()
{
InitializeComponent();
}
}
代码后置文件的代码最小说明了复合应用程序架构的强大和简单性,以及Shell与其组成视图之间的松耦合。
区域定义
你可以通过定义一个已命名位置的布局来定义视图出现的位置,这个布局通常被称为区域。区域充当一个或多个视图的占位符,这些视图将在运行时显示。模块可以在布局中定位和添加内容到区域,而不不需要知道区域如何显示以及在哪里显示。这样可以在不影响布局中其他模块的情况下更改布局。
区域是通过将区域名称分配给WPF控件来进行定义的,其可以在XAML中定义(如前面的Shell.xaml文件中所示),也可以在代码中定义。您可以通过区域名称来访问区域。在程序运行时,视图会被添加到已命名区域控件上,然后根据视图实现的布局策略显示一个或多个视图,例如选项卡控件区域将以选项卡式排列布置其子视图。区域支持添加或删除视图,您可以以编写代码的方式或以自动的方式在区域中创建和显示视图。在Prism库中,前者是通过视图注入(View Injection)实现的,后者是通过视图发现(View Discovery)实现的。这两种技术决定了各个视图如何映射到应用程序UI中的已命名的区域上的。
应用程序的Shell定义了最高级别的应用程序布局;例如,如下图所示指定主要内容和导航内容的位置。而且这些高层视图中的布局也允许类似地进行定义,并且允许递归并组合成整个UI页面。
区域有时用于定义逻辑上相关的多个视图的位置。在这种情况下,区域控件通常是一个ItemsControl
派生的控件,它将根据其实现的布局策略显示视图,例如以堆叠或选项卡布局进行安排的策略。
区域还可以用来定义单个视图的位置;例如通过使用ContentControl
。在这种情况下,即使有多个视图映射到该区域位置,区域控件一次也只显示一个视图。
App Shell区域示例
当应用程序在买卖股票时,应用程序示例的用户界面中还展示了多视图布局。其中买入/卖出区域是一个列表样式区域,并显示多个买入/卖出视图 (OrderCompositeView) 作为其列表的一部分,如下图所示。
Shell的ActionRegion包含OrdersView。OrdersView包含Submit All和Cancel All按钮以及OrdersRegion。OrdersRegion附加了一个显示多个OrderCompositeViews的ListBox控件。
在XAML中添加区域
区域是一个实现IRegion
接口的类。并且区域是一个包含了要显示的内容的控件容器。
RegionManager
提供了一个附加属性,可用于在XAML中创建简单的区域。若要使用附加属性,必须将Prism Library命名空间加载到XAML中,然后使用RegionName
附加属性。以下示例显示了如何在窗口中使用带有AnimatedTabControl
的附加属性。
请注意,使用x:Static
标记扩展来引用MainRegion
字符串常量。这种做法消除了XAML中的魔术字符串。(csharpshare.com注:魔术字符串指的是,在代码之中多次出现,并与代码形成强耦合的某一个具体的字符串或者数值。)
<!-- (WPF) -->
<Controls:AnimatedTabControl
x:Name="PositionBuySellTab"
prism:RegionManager.RegionName="{x:Static inf:RegionNames.MainRegion}"/>
使用代码添加一个区域
RegionManager
可以在不使用XAML的情况下直接注册区域。下面的代码示例演示如何在代码后置的文件中将区域添加到控件上。首先获得对区域管理器的引用。然后使用RegionManager
中的静态方法SetRegionManager
和SetRegionName
将区域附加到UI的ActionContent
控件上,并且命名该区域为ActionRegion
。
IRegionManager regionManager = ServiceLocator.Current.GetInstance<IRegionManager>();
RegionManager.SetRegionManager(this.ActionContent, regionManager);
RegionManager.SetRegionName(this.ActionContent, "ActionRegion");
当区域加载时显示视图
使用视图发现方法,模块可以为特定名称的位置注册视图(视图模型或表示模型)。当该位置在运行中显示时,您为该位置注册的任何视图都将自动创建并在其中显示。
模块通过注册表注册视图。父视图查询此注册表以发现已为该命名位置注册的视图。在它们被发现后,父视图通过将它们添加到占位控件将它们放置在屏幕上。
加载应用后,复合视图会被通知处理添加到注册表中的新视图的位置。
下面的插图展示了视图发现方法。
Prism库定义了一个标准注册表,RegionViewRegistry
,来为这些已命名的位置注册视图。
要在一个区域中显示视图,需要向区域管理器中注册视图,如下面的代码示例所示。你可以选择直接向区域中注册视图类型,在这种情况下视图将由依赖注入容器构建,并在加载托管区域的控件时添加到该区域。
// View discovery
this.regionManager.RegisterViewWithRegion("MainRegion", typeof(EmployeeView));
或者,您也可以提供一个委托来返回要显示的视图,如下面的代码示例所示。区域管理器将在区域创建时显示视图。
// View discovery
this.regionManager.RegisterViewWithRegion("MainRegion", () => this.container.Resolve<EmployeeView>());
编写代码来显示区域中的视图
在视图注入方法中,视图是由管理它们的模块以编写代码的方式从指定位置进行添加或删除的。为此应用程序在UI中需要包含一个已命名的位置的注册表。模块可以使用该注册表来查找其中一个位置,然后以编写代码的方式向其中注入视图。为了确保可以类似地访问注册表中的位置,每个命名位置都遵循用于注入视图的通用接口。下面的插图展示了视图注入的方法。
Prism库定义了一个标准的注册处RegionManager
和一个标准接口IRegion
来访问这些位置。
要使用视图注入将视图添加到区域中,需要从区域管理器中获取区域,然后调用Add
方法,如下面的代码所示。在使用视图注入时,只有在视图被添加到区域后视图才会显示,这可能会在模块加载或用户操作完成预定义操作时发生。
// View injection
IRegion region = regionManager.Regions["MainRegion"];
var ordersView = container.Resolve<OrdersView>();
region.Add(ordersView, "OrdersView");
region.Activate(ordersView);
在区域中排序视图
无论是使用视图发现还是视图注入,应用程序都可能需要对视图在TabControl
、ItemsControl
或任何其他可以显示多个活动视图的控件中的显示方式进行排序。在默认情况下,视图按照注册和添加到区域的顺序显示。
在构建复合应用时,视图通常是从不同的模块注册的。而声明模块之间的依赖关系可以帮助缓解这个问题,但是当模块和视图之间没有任何真正的依赖关系时,人为地声明依赖关系会不必要地使模块耦合起来。
为了允许视图参与排序,Prism库提供了ViewSortHint
属性。这个属性包含一个字符串Hint
属性,允许视图声明它应该如何在区域中排序的提示。
当显示视图时,Region
类会使用默认的视图排序的例行规则,该例行规则会使用提示来对视图进行排序。其是一种区分大小写的简单有序排序。具有排序Hint属性的视图会排在没有属性的视图前面。此外那些没有属性的元素会按照添加到区域的顺序进行显示。
如果你想改变视图的排序方式,Region
类提供了一个SortComparison
属性,你可以用你自己的Comparison<_object_>
委托方法来配置它。需要注意的是,区域的Views
和ActiveViews
的顺序会反映在UI中,因为诸如ItemsControlRegionAdapter
之类的适配器是直接绑定到这些属性上的。一个自定义的区域适配器可以实现它自己的排序和过滤器,以覆盖区域对视图排序的方式。
在多个区域之间共享数据
Prism库提供了多种方法来在视图之间进行通信,具体使用取决于您的场景。区域管理器中提供了RegionContext
属性作为这些方法之一。
当您想要在区域中托管的父视图和子视图之间共享上下文时,RegionContext
附加属性会很有用。您可以在区域控件上设置上下文的值,以便该区域控件中显示的所有子视图都可以使用它。 区域上下文可以是任何简单或复杂的对象,并且可以是数据绑定值。RegionContext
属性可以与视图发现或视图注入一起使用。
注意: WPF中的
DataContext
属性也可用于设置视图的本地数据上下文。它允许视图使用数据绑定的方式与视图模型、本地呈现器(csharpshare.com注:指的是xxx.xaml.cs文件)或模型进行通信。RegionContext
用于在多个视图之间共享上下文,并且不局限于单个视图。它提供了一种在多个视图之间共享上下文的简单机制。
以下代码显示了如何在XAML中使用RegionContext
附加属性。
<TabControl AutomationProperties.AutomationId="DetailsTabControl"
prism:RegionManager.RegionName="{x:Static local:RegionNames.TabRegion}"
prism:RegionManager.RegionContext="{Binding Path=SelectedEmployee.EmployeeId}"
...>
您还可以在代码中设置RegionContext
,如下所示。
RegionManager.Regions["Region1"].Context = employeeId;
要检索视图中的RegionContext
,可以使用RegionContext
类的GetObservableContext
静态方法。它会将视图作为参数传递,然后访问其Value
属性即可,如以下代码示例所示。
private void GetRegionContext()
{
this.Model.EmployeeId = (int)RegionContext.GetObservableContext(this).Value;
}
RegionContext
的值可以在视图中通过简单地给它的Value
属性赋一个新值来改变。视图可以选择通过订阅由GetObservableContext
方法返回的ObservableObject
对象上的PropertyChanged
事件来通知RegionContext
的变化。这允许多个视图在RegionContext
更改时保持同步。以下代码示例演示了如何订阅PropertyChanged
事件。
ObservableObject<object> viewRegionContext =
RegionContext.GetObservableContext(this);
viewRegionContext.PropertyChanged += this.ViewRegionContext_OnPropertyChangedEvent;
private void ViewRegionContext_OnPropertyChangedEvent(object sender,
PropertyChangedEventArgs args)
{
if (args.PropertyName == "Value")
{
var context = (ObservableObject<object>) sender;
int newValue = (int)context.Value;
}
}
注意:
RegionContext
被设置为区域中托管的内容对象的附加属性。这意味着内容对象必须派生自DependencyObject
。在前面的示例中,视图是一个可视化控件,它最终派生自DependencyObject
。
如果您选择使用WPF数据模板来定义您的视图,内容对象将表示为
ViewModel
或PresentationModel
。如果您的ViewModel或PresentationModel需要检索RegionContext
,则也需要从DependencyObject
基类派生。
创建多个区域实例
范围区域仅适用于视图注入。如果您需要视图拥有自己的区域实例,则应该使用它们。定义具有附加属性的区域的视图会自动继承其父级的RegionManager
。通常是在Shell窗口中注册的全局RegionManager
。如果应用程序创建该视图的多个实例,每个实例都会尝试向父RegionManager
注册其区域。而RegionManager
只允许唯一命名的区域;因此第二次注册会产生错误。
相反使用范围区域将使每个视图都有自己的RegionManager
,并且其区域也将注册到该 RegionManager
而不是父级RegionManager
,如下图所示。
要为视图创建本地的RegionManager
,请指定在将视图添加到区域时创建一个新的RegionManager
,如以下代码示例所示。
IRegion detailsRegion = this.regionManager.Regions["DetailsRegion"];
View view = new View();
bool createRegionManagerScope = true;
IRegionManager detailsRegionManager = detailsRegion.Add(view, null, createRegionManagerScope);
Add
方法将返回新的RegionManager
,视图可以保留该对象以便进一步访问其本地的范围区域对象。