在 Prism 中通过 TreeView 导航不同的模块。






4.38/5 (4投票s)
描述了在标准 PRISM 中使用 TreeView 进行导航。
动机
当我们需要在复合 PRISM 的概念中利用模式和实践时,我们可以利用示例中显示的其中一个假设。不幸的是,在 TreeView 中没有包含分层导航系统的示例。
在本文中,我想展示我如何解决导航问题,无论应用程序中添加了多少模块,无论模块的层次结构如何,也无论模块中包含什么。通过 TreeView 深入(Shell 与模块协同工作)并导航基础结构。
要求
- 为了更好地理解以下内容
- 了解 C#。
- 了解 PRISM 是什么并能够实现它。
- Visual C# 2010 Express(最低)和 SharpDevelop 4.1(我使用的是 Visual Studio 2010)。
工作原理
我使用的基础项目

来自 http://dphill.members.winisp.net/Templates.html 的模板,感谢 David Hill。

重新生成项目“NavShelll”根目录中的“ModuleCatalog.xml”文件
// <prism: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:prism="clr-namespace:Microsoft.Practices.Prism.Modularity;assembly=Microsoft.Practices.Prism"> <prism:ModuleInfo Ref="NavModule_One.dll" ModuleName="NavModule_One.ModuleInit" ModuleType="NavModule_One.ModuleInit, NavModule_One, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" InitializationMode="WhenAvailable"/> <prism:ModuleInfo Ref="NavModule_Two.dll" ModuleName="NavModule_Two.ModuleInit" ModuleType="NavModule_Two.ModuleInit, NavModule_Two, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" InitializationMode="WhenAvailable"> <prism:ModuleInfo.DependsOn> <sys:String>NavModule_One.ModuleInit</sys:String> </prism:ModuleInfo.DependsOn> </prism:ModuleInfo> </prism:ModuleCatalog>
项目“Module_One
”、“Module_Two
”和“Independent_Module
”,双击“属性”,然后单击“生成”,在“输出”部分,并指明应用程序所在的目录 NavShell,即生成的文件,例如……/bin/Debug/ 或 /bin/Release/

重新生成解决方案。
我们将添加一个名为“NavInfrastructure
”的类库项目,使其成为独立模块,但不会创建许多重复的基础类和接口。我们将向您的 NavShell 项目添加对此库的引用。
项目 NavInfrastructure
添加类 NameRegions
,这将帮助我们正确识别区域并在一个地方进行因子化(当然,您可以坚持使用专用代码)。
using System;
namespace NavInfrastructure
public static class NameRegions
{
public static string NavigationTreeViewRegion = "NavigationTreeViewRegion"
}
项目“NavShell”中的“Views”找到一个名为“ShellView.xaml”的文件,该文件负责显示区域及其内容。当然,区域本身是由帮助类使用 MappAdapter
的 ManagerRegion
创建的,具体取决于用于控制区域的基础类型。
我们在“NavShell”的“View”中的“ShellView.xaml”文件中找到区域,我们在这里使用 TreeView,这是本文的主题。部分
<local:RegionBorderControl Grid.Row="1" Grid.Column="0" RegionName="TopLeftRegion"
Style="{StaticResource RegionBorderControlStyle}">
<!--<span class="code-comment"> TopLeft Region : A simple content control -- > <ContentControl prism:RegionManager.RegionName="TopLeftRegion" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" />
</local:RegionBorderControl >
在文件中补充 ShellView.xaml 引用
xmlns:infra="clr-namespace:NavInfrastructure;assembly=NavInfrastructure"
我们将区域名称更改为
<local:RegionBorderControl Grid.Row="1" Grid.Column="0" RegionName="Navigation"
Style="{StaticResource RegionBorderControlStyle}">
<!--NavigationTreeViewRegion Region : A TreeView control --></span>
<TreeView prism:RegionManager.RegionName="{x:Static infra:NameRegions.NavigationTreeViewRegion}"
VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"/>
</local:RegionBorderControl>
完全重新生成解决方案 - 重新生成解决方案 -> F6。

模块本身的导航
在本节中,我们创建自动加载到 TreeView 控件的类,并设置单个模块中导航的根部或主分支。关于模块导航将在另一部分介绍。
展示了两个版本。第一个是 XAML。第二个是过程代码,C#。
XAML 代码
我们在项目“NavModule_One
”中使用 XAML 代码。在“Views”中,右键单击并选择“添加”>“新建项”,然后在模式选择窗口中选择 UserControl.xaml,将其命名为 NavigationModuleOne.xaml。打开新创建的文件。我将对其进行因子化处理
<TreeViewItem x:Class="NavModule_One.Views.NavigationModuleOneView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:infra="clr-namespace:NavInfrastructure;assembly=NavInfrastructure" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" mc:Ignorable="d"> <TreeViewItem.Resources> <!-- HierarchicalDataTemplates --></span> <HierarchicalDataTemplate x:Key="categoriesEntry" DataType="{x:Type infra:EntityBase}" ItemsSource="{Binding Path=SubEntity}"> <StackPanel Orientation="Horizontal" ToolTip="{Binding}"> <ContentControl Margin="0 0 4 0" Content="{Binding Icon, Converter={x:Static infra:StringToImage.Default}}" /> <TextBlock FontWeight="Bold" VerticalAlignment="Center" Text="{Binding Path=Title}" /> </StackPanel> </HierarchicalDataTemplate> <DataTemplate x:Key="headerDataTemplate" DataType="{x:Type infra:Catalog}"> <StackPanel Orientation="Horizontal" ToolTip="{Binding}"> <ContentPresenter Margin="0 0 4 0" Content="{Binding Icon, Converter={x:Static infra:StringToImage.Default}}" /> <TextBlock FontWeight="Bold" Text="{Binding Title, Mode=TwoWay}" VerticalAlignment="Center" /> </StackPanel> </DataTemplate> </TreeViewItem.Resources> <TreeViewItem ItemsSource="{Binding Path=Categories}" Header="{Binding Root}" HeaderTemplate="{StaticResource headerDataTemplate}" ItemTemplate="{StaticResource categoriesEntry}"> </TreeViewItem> <i:Interaction.Triggers> <!--<i:EventTrigger EventName="Selected">--></span> <i:EventTrigger EventName="MouseDoubleClick"> <i:InvokeCommandAction Command="{Binding SelectedCommand}" CommandParameter="{Binding Path=SelectedItem, RelativeSource={RelativeSource AncestorType={x:Type TreeView}}}" /> </i:EventTrigger> </i:Interaction.Triggers> </TreeViewItem>
以及表单的代码文件
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel.Composition;
using NavModule_One.ViewModels;
namespace NavModule_One.Views
{
/// <summary>
/// Interaction logic for NavigationModuleOne.xaml
/// </summary>
[Export]
[PartCreationPolicy(CreationPolicy.Shared)]
public partial class NavigationModuleOneView : TreeViewItem
{
public NavigationModuleOneView()
{
InitializeComponent();
}
static NavigationModuleOneView()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(NavigationModuleOneView), new FrameworkPropertyMetadata(typeof(ItemsControl)));
}
[Import(AllowRecomposition = false)]
public NavigationModuleOneViewModel ViewModel
{
get { return this.DataContext as NavigationModuleOneViewModel; }
set { this.DataContext = value; }
}
}
}
最重要的改变是将 UserControl 中的控件类型改为 TreeViewItem 控件。HierarchicalDataTemplate
允许使用 TreeViewItem,这在我们的情况下至关重要。我使用了另一个子 TreeViewItem,其中主根键替换了 ItemsControl
的默认样式,它将第一级作为容器处理,并附加了适当的模板 HierarchicalDataTemplate
和 HeaderTemplate
。覆盖 ItemsControl
类型的样式功能将帮助您在列表中删除负责标题的部分(Header_Part)。这些结果也表明所有内容都在项目中使用的“主题”以及主应用程序(本例中为“NavShell”)中。我在静态构造函数中实现了这些,该构造函数仅在创建对象时调用一次。应用标准模型一级,我们使用过程代码模块“NavModule_Two
”。我们始终记住用导出装饰类属性以及导入构造函数,在需要某个属性构造函数([ImportingConstructor])的类中。现在我们将导航控件报告给区域“Navigation”。我们在项目 ModuleInit NavModule_One
类中执行此操作,该类实现了接口 IModule
。我们添加对项目“NavInfrastructure
”的引用,其中包含已声明区域的名称。
using NavInfrastructure;
并重新生成 ModulInit.Initialize
#region IModule Members public void Initialize() { // Use View Discovery to automatically display the MasterView when the TopLeft region is displayed. _regionManager.RegisterViewWithRegion(NameRegions.NavigationTreeViewRegion, () => _serviceLocator.GetInstance<NavigationModuleOneView>()); } #endregion
是时候实现导航逻辑和包含导航节点的基础了。
我们将需要
数据模型类:EntityBase
和 Catalog
,它们允许方便地在应用程序的 NavInfrastructure.dll 库中的其他模块中使用它们。项目 NavInfrastructure
添加 EntityBase
和一个从 EntityBase
类继承的 Catalog
类。
EntityBase.cs
namespace NavInfrastructure
{
public class EntityBase
{
public int IDEntity { get; set; }
public string Title { get; set; }
public string Icon { get; set; }
public string Description { get; set; }
public EntityBase Parent { get; set; }
public override string ToString()
{
if (string.IsNullOrEmpty(Description))
{
return Title;
}
else
{
return Description;
}
}
}
}
Catalog.cs
using System.Collections.Generic;
namespace NavInfrastructure
{
public class Catalog : EntityBase
{
public IEnumerable<EntityBase> SubEntity { get; set; }
}
}
我将添加一个文件夹,用于放置默认节点图标的图像,我现在已经在库中

QueryStringBuilder.cs
类有助于构建地址,并从 Karl Shifflett 的项目 http://karlshifflett.wordpress.com/ 下载,StringToImage.cs
类负责将地址转换为图像图标。
我们回到 NavigationModuleOneView.xaml 文件。如前所述,控件的主级别被用作提供 HierarchicalDataTemplate 和 HeaderTemplate 的容器。我们使用 HierarchicalDataTemplate
模板,该模板将数据类指定为 EntityBase
,Catalog
类继承自 EntityBase
,达到导航所需的程度,在 Catalog
中,数据绑定引擎会忽略对没有 EntityBase
类的属性的绑定。

Catalog
和 EntityBase
类是用于通过 id 和类型进行正确组合导航所需的基本信息的简单集合,这需要 id 的唯一性。
MVVM 加缓存模式
在本例中,我们使用 MVVM(模式)表示模式。实际上,这是很自然的。如 NavigationModuleOneView.xaml 文件所示,我们将相关类的特征与 VM(ViewModel)上下文结合起来,VM 将与视图(View)相关联。

上图显示了 NavModule_One
模块的核心架构。用户交互由 NavigationModuleOneView
实例处理,通过 Prism 实现的日志记录和映射机制,它被附加到主应用程序 NavShell 的 Navigation 区域。当 NavigationModuleOneView
实例被构建时,NavigationModuleOneViewModel
实例会通过 MEF 机制被构建。WPF 或 Silverlight 引擎将这两个实例的属性连接起来。NavigationModuleOneViewModel
的构造函数本身启动了创建存储缓存数据的类实例的机制。然后,它通过 DocumentService
服务启动数据收集方法。NavigationModuleOneViewModel
类是所有导航逻辑发生的主要场所。
这是完整的类
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Linq;
using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Practices.Prism.ViewModel;
using NavInfrastructure;
using NavModule_One.Models;
using NavModule_One.Properties;
namespace NavModule_One.ViewModels
{
/// <summary>
/// Main navigation class of the Module_One.
/// </summary>
[Export]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class NavigationModuleOneViewModel : NotificationObject
{
#region Declaration field class
CacheManager _cacheManager;
IRegionManager _regionManager;
#endregion
#region .ctor
/// <summary>
/// Default Mef .ctor. Called when creating an object constructor.
/// </summary>
/// <param name="cacheManager">The data cache manager.</param>
/// <param name="regionManager">The Regions manager.</param>
[ImportingConstructor] // Called when creating an object constructor
public NavigationModuleOneViewModel(
CacheManager cacheManager, // Initialization of the data cache manager
IRegionManager regionManager // Initialization of the Regions manager
)
{
if (cacheManager == null) throw new ArgumentNullException("cacheManager");
if (regionManager == null) throw new ArgumentNullException("regionManager");
this._cacheManager = cacheManager;
this._regionManager = regionManager;
_categories = new ObservableCollection<EntityBase>();
// Get the data model from the Cache but wait how will be populated.
((NotificationObject)_cacheManager).PropertyChanged += new PropertyChangedEventHandler(NavigationDocumentsViewModel_PropertyChanged);
// Initialize this ViewModel's commands.
// The command occurs upon request to an item
SelectedCommand = new DelegateCommand<object>(SelectedExecute, CanExecuteSelected);
}
#endregion
#region Property CurrentCategory and ...
public EntityBase CurrentCategory { get; private set; }
#endregion
#region Property Root and helper methods
private EntityBase _root;
public EntityBase Root // Populate in NavigationDocumentsViewModel_PropertyChanged
{
get { return _root ?? new Catalog() { Title = Strings.LoadingModuleMessage }; }
set
{
if (value != _root)
{
_root = value;
this.RaisePropertyChanged("Root");
}
}
}
#endregion
#region Property Categories and helper methods
private ObservableCollection<EntityBase> _categories;
public ObservableCollection<EntityBase> Categories { get { return _categories; } }
#endregion
#region Helper methods
void NavigationDocumentsViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "CacheDocuments")
{
Populate();
// Po każdym resecie Documents (Przypisanie nowej kolekcji usuwa również subskrybcje)
// przypisujemy Subskrybcje na zdarzenia zmian w kolekcji.
_cacheManager.CacheDocuments.CollectionChanged += new NotifyCollectionChangedEventHandler(CacheDocuments_CollectionChanged);
}
}
void CacheDocuments_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null || e.OldItems != null)
Populate();
}
void Populate()
{
PopulateRoot();
this.Categories.Clear();
foreach (var i in PopulateItemCategories(_root.IDEntity))
{
Categories.Add(i);
}
}
void PopulateRoot()
{
this.Root = this._cacheManager.CacheDocuments.Single(i => i.Parent == null);
if (string.IsNullOrEmpty(Root.Icon)) this.Root.Icon = Settings.Default.RootIcon;
}
IEnumerable<EntityBase> PopulateItemCategories(int idParent)
{
var tempItem = this._cacheManager.CacheDocuments.Where(i => i.Parent != null && i.Parent.IDEntity == idParent);
foreach (var e in tempItem)
{
Catalog cat = e as Catalog;
if (cat != null)
{
cat.SubEntity = PopulateItemCategories(e.IDEntity);
}
}
return tempItem;
}
#endregion
#region SelectedCommand
public DelegateCommand<object> SelectedCommand { get; private set; }
private void SelectedExecute(object commandParameter)
{
if (commandParameter != null)
{
EntityBase obj = commandParameter as EntityBase;
if (obj != null)
{
Catalog cat = obj as Catalog;
if (cat != null)
{
string addressView = QueryStringBuilder.Construct(ViewsName.CatalogView, new[,] { { ViewsName.ParentId, obj.IDEntity.ToString() } });
_regionManager.RequestNavigate(NameRegions.MainRegion, addressView, Callback);
}
else
{
_regionManager.RequestNavigate(NameRegions.MainRegion, QueryStringBuilder.Construct(ViewsName.DocumentView, new[,] { { ViewsName.ParentId, obj.IDEntity.ToString() } }), Callback);
}
}
else
{
// First level.
if (_root != null)
{
string addressView = QueryStringBuilder.Construct(ViewsName.CatalogView, new[,] { { ViewsName.ParentId, _root.IDEntity.ToString() } });
_regionManager.RequestNavigate(NameRegions.MainRegion, addressView, Callback);
}
}
}
}
private bool CanExecuteSelected(object commandParameter)
{
return true;
}
#endregion
#region Navigation helper
void Callback(NavigationResult result)
{
// Todo: To do log.
Debug.WriteLine("NavigationResult: {0}", result.Result);
}
#endregion
}
}
该类可以分为三个功能部分:第一部分 - 处理数据,第二部分 - 用户发起的命令操作;第三部分 - 在视图中正确呈现数据。对于数据处理,有负责初始化检索的构造函数、事件处理程序 method void NavigationDocuments_PropertyChanged (object sender,
和
PropertyChangedEventArgs e)void CacheDocuments_CollectionChanged (object sender,
以及辅助方法
NotifyCollectionChangedEventArgs e)void Populate ()
, void PopulateRoot ()
, IEnumerable <EntityBase> PopulateItemCategories (int idParent)
。对于处理用户发起的事件,有包含在 public DelegateCommand <object> SelectedCommand {get; private set;}
,
private void SelectedExecute (object CommandParameter)private bool CanExecuteSelected (object CommandParameter)
, void Callback (NavigationResult result)
中的功能组,以及通过相关的 CurrentCategory, Root, Categories 属性呈现数据。为了更好地说明数据加载过程,类中应用了 MockDokumentsService
来模拟 5 秒的数据传输延迟。由于加载方法应用在此处,异步加载,应用程序的其他组件同时进行。在等待数据加载完成时,在导航模块的框中显示效果。要接收用户驱动的导航需求,使用了通过双击选定项触发事件的启动方法。项目应包含相应的库 C:\Program Files (x86)\Microsoft SDKs\Expression\Blend\.NETFramework\v4.0\Libraries\System.Windows.Interactivity.dll,它放置在 SDKBlend.NetFramework4.0 中,并添加对文件和视图 System.Windows.Interactivity 的引用
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity".
呈现导航视图
现在我们只需要创建类视图并在区域中创建它们的呈现。当我们回到 NavigationModuleOneViewModel
类 SelectedCommand
部分的 private void SelectedExecute
(object CommandParameter) 方法时,我们会发现一段代码(如果有人重写或粘贴了文本,下划线肯定表示错误)
_regionManager.RequestNavigate(
NameRegions.WorkbenchRegion,
QueryStringBuilder.Construct(ViewsName.
CatalogView, new[,] { { ViewsName.ParentId, obj.IDEntity.ToString() } }),
Callback);
这段代码是导航到模块中特定地址所必需的。然而,我们将详细介绍这一点,我们需要创建一个区域来显示相应的视图、类视图、模型视图以及所有相关的基础结构。
区域 MainRegion
在“NavShell”项目中打开文件,并在 ViewShell.xaml 中找到
<!--Style to display an icon and the current item's name in the tab header--></span> <Style x:Key="TabHeaderStyle" TargetType="{x:Type TabItem}"> <Setter Property="HeaderTemplate"> <Setter.Value> <DataTemplate> <StackPanel Orientation="Horizontal"> <Image Height="20" Width="20" Margin="0,0,2,0" Source="/NavigateTreeView;component/Images/ItemIcon.png"/> <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type TabItem} }, Path=Content.DataContext.CurrentItem.Name}" VerticalAlignment="Center"/> </StackPanel> </DataTemplate> </Setter.Value> </Setter> </Style>
我重新构建了以下内容
<!--Style to display an icon and the current item's name in the tab header--> <Style x:Key="TabHeaderStyle" TargetType="{x:Type TabItem}"> <Setter Property="HeaderTemplate"> <Setter.Value> <DataTemplate> <DataTemplate.Resources> <Style x:Key="ToolButtonStyle" BasedOn="{x:Null}" TargetType="{x:Type Button}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Button}"> <Grid> <Rectangle Opacity="0" Fill="#66FFFFFF" Stroke="{x:Null}" StrokeThickness="0.5" HorizontalAlignment="Stretch" Margin="2,0,2,0" x:Name="rectangle" VerticalAlignment="Stretch" /> <Path Fill="{x:Null}" x:Name="leftLine" Stretch="Fill" Stroke="LightGray" StrokeThickness="1" HorizontalAlignment="Left" Margin="0.5" Width="1" Height="18" Grid.RowSpan="1" Data="M-87.28,4 L-87.28,17" /> <Path Fill="{x:Null}" x:Name="rightLine" Stretch="Fill" Stroke="LightGray" StrokeThickness="1" HorizontalAlignment="Right" Margin="0.5" Height="18" Width="1" Grid.RowSpan="1" Data="M-87.28,4 L-87.28,17" /> <ContentPresenter x:Name="presenter" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="10,0,10,0" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" TextElement.Foreground="LightGray" RecognizesAccessKey="True" /> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Opacity" TargetName="rectangle" Value="0.5" /> <Setter Property="TextElement.Foreground" TargetName="presenter" Value="Red" /> <Setter Property="Path.Stroke" TargetName="leftLine" Value="Red" /> <Setter Property="Path.Stroke" TargetName="rightLine" Value="Red" /> <Setter Property="Rectangle.Fill" TargetName="rectangle" Value="LightGray" /> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter Property="Opacity" TargetName="rectangle" Value="1" /> <Setter Property="Fill" TargetName="rectangle" Value="DarkGray" /> </Trigger> <Trigger Property="IsEnabled" Value="False" /> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </DataTemplate.Resources> <StackPanel Orientation="Horizontal"> <Image Height="16" Width="16" Margin="0,0,2,0" Source="{Binding RelativeSource={RelativeSource AncestorType={x:Type ContentControl} }, Path=Content.DataContext.CurrentItem.Icon}" /> <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type ContentControl} }, Path=Content.DataContext.CurrentItem.Title}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" /> <Button Content="x" Style="{StaticResource ToolButtonStyle}" Margin="5,0,0,0" Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type ContentControl}}, Path=Content.DataContext.KillingCommand}" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type ContentControl}},Path=Content}" /> </StackPanel> </DataTemplate> </Setter.Value> </Setter> </Style>
尽管代码量很大,但这些更改都是表面上的,唯一必需的更改是绑定属性和将特定的关闭命令绑定到视图头视图中的关闭按钮。
视图呈现
在项目“NavModule_One
”的 Views 目录中,向名为 CatalogView.xaml 的 UserControl 添加一个新的用户控件,并将其重构为表单
<UserControl x:Class="NavModule_One.Views.CatalogView" 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"> <Grid> <ListBox ItemsSource="{Binding CatalogItems}"> <ListBox.ItemTemplate> <DataTemplate> <!-- Utworzyć styl HeaderItemsControl z możliwością pokazywaia niższych kategorii --> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Path=Title}" /> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </UserControl>
DocumentView.xaml
<UserControl x:Class="NavModule_One.Views.DocumentView" 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"> <Grid x:Name="LayoutRoot" Background="White"> <ScrollViewer> <RichTextBox> <FlowDocument> <Paragraph>Text testowy do próby</Paragraph> </FlowDocument> </RichTextBox> </ScrollViewer> </Grid> </UserControl>
ViewModelBase
每个具体的 ViewModel
类都继承自辅助项目 NavInfrastructure.dll 中的 ViewModelBase
基类。它只有一个正确的功能,即与视图关闭相关的子项。它将连接到 Prism 和 ServiceLocation 库的项目链接。KillView
函数会搜索特定视图的列表并将其从列表中删除。没有实现检测文件中更改的机制(这将在另一篇论文中介绍)。值得注意的是,与视图关闭相关的属性是将函数传递给 CatalogView.xaml 文件中关联属性的委托。
using System.Linq;
using Microsoft.Practices.Prism.ViewModel;
using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.Regions;
using Microsoft.Practices.ServiceLocation;
namespace NavInfrastructure
{
public abstract class ViewModelBase : NotificationObject
{
#region Killing behavior
public virtual DelegateCommand<object> KillingCommand
{
get { return _killingCommand ?? (_killingCommand = new DelegateCommand<object>(KillView)); }
set { _killingCommand = value; }
}
private DelegateCommand<object> _killingCommand;
public virtual void KillView(object view)
{
// Arbitrary Container.
IRegionManager manager = ServiceLocator.Current.GetInstance<IRegionManager>();
// find and remove view.
foreach (IRegion region in manager.Regions)
{
// Find current view
// Ustalamy obiekt na liście widoków
object removeView = region.Views.SingleOrDefault(v => v == view);
if (removeView != null)
// Remove finding view.
// Usuwamy ustalony widok.
manager.Regions[region.Name].Remove(view);
}
}
#endregion
}
}
具体的 ViewModel 类
DocumentViewModel
和 CatalogViewModel
这两个类都继承自 ViewModelBase
。
CatalogViewModel 类
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Linq;
using Microsoft.Practices.Prism.Regions;
using NavInfrastructure;
using NavModule_One.Models;
namespace NavModule_One.ViewModels
{
[Export]
public class CatalogViewModel : ViewModelBase, INavigationAware
{
#region Private Field
CacheManager _cacheManager;
#endregion
#region .ctor
[ImportingConstructor]
public CatalogViewModel(CacheManager cacheManager)
{
_cacheManager = cacheManager;
}
#endregion
#region Property CurrentItem
/// <summary>
/// Current base item.
/// </summary>
public EntityBase CurrentItem
{
get { return _currentItem; }
private set
{
if (value != _currentItem)
{
_currentItem = value;
this.RaisePropertyChanged("CurrentItem");
}
}
}
private EntityBase _currentItem;
#endregion
#region Property CatalogItems
private ObservableCollection<EntityBase> _catalogItems;
public ObservableCollection<EntityBase> CatalogItems //
{
get { return _catalogItems; }
set
{
_catalogItems = value;
RaisePropertyChanged("CatalogItems");
}
}
#endregion
#region INavigationAware Members
public bool IsNavigationTarget(NavigationContext navigationContext)
{
// Only one view.
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
var idEntity = int.Parse(navigationContext.Parameters["ParentId"]);
this.CurrentItem = _cacheManager.CacheDocuments.Single(i => i.IDEntity == idEntity);
PopulateViewModel();
}
#endregion
#region Help method
private void PopulateViewModel()
{
CatalogItems.Clear();
foreach (var i in _cacheManager.CacheDocuments.Where(i => i.Parent != null && i.Parent.IDEntity == _currentItem.IDEntity))
{
CatalogItems.Add(i);
}
}
#endregion
}
}
CatalogViewModel
类因为它不打算更改内容,不需要确认 INavigationAware,因此实现了负责导航到此类行为的接口。我建议您阅读 Prism 文档 http://compositewpf.codeplex.com/。
DocumentViewModel 类
using System;
using System.ComponentModel.Composition;
using System.Linq;
using Microsoft.Practices.Prism.Regions;
using NavInfrastructure;
using NavModule_One.Models;
namespace NavModule_One.ViewModels
{
/// <summary>
/// ViewModel class's Document.
/// </summary>
[Export]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class DocumentViewModel : ViewModelBase, IConfirmNavigationRequest
{
#region Private Field
CacheManager _cacheManager;
#endregion
#region .ctor
/// <summary>
/// Importing Constructor.
/// </summary>
/// <param name="cacheManager"> Manager data cache.</param>
[ImportingConstructor]
public DocumentViewModel(CacheManager cacheManager)
{
if (cacheManager == null) throw new ArgumentNullException("cacheManager");
this._cacheManager = cacheManager;
}
#endregion
#region Property CurrentItem
/// <summary>
/// Current base item.
/// </summary>
public EntityBase CurrentItem
{
get { return _currentItem; }
private set
{
if (value != _currentItem)
{
_currentItem = value;
this.RaisePropertyChanged("CurrentItem");
}
}
}
private EntityBase _currentItem;
#endregion
#region IConfirmNavigationRequest Members
/// <summary>
/// Implementation confirm.
/// </summary>
/// <param name="navigationContext"></param>
/// <param name="continuationCallback"></param>
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
continuationCallback(true);
}
#endregion
#region INavigationAware Members
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return CurrentItem.IDEntity.ToString() == navigationContext.Parameters["ParentId"];
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
var idEntity = int.Parse(navigationContext.Parameters["ParentId"]);
this.CurrentItem = _cacheManager.CacheDocuments.Single(i => i.IDEntity == idEntity);
}
#endregion
}
}
DokumentViewModel
类有两个重要的特性值得关注。第一个是与应用程序的协作方法,它指示容器每次我们想要创建指定文档自己的实例。PartCreationPolicyAttrybute
获取 NonShared
值。这是 CatalogViewModel
实例被共享的情况。第二个最重要的项目是从我们的角度来看,它有两个 INavigationAware
接口方法的实现 - IsNavigationTarget
和 OnNavigationTo
。第一个在导航指向同一类类型时被调用,根据结果,它被分配给实例数据,或者创建一个新的(区域检查不再适用于指向的实例)。第二个当下一个导航指向给定实例时被调用,并且应该开始进行输入。这里简要描述了 NavModuleOne
。

另一种方式,但不完全是
实际上,这只会是 NavModule_One
导航的过程版本。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using NavInfrastructure;
using System.Windows.Interactivity;
using System.ComponentModel.Composition;
using NavModule_Two.ViewModels;
namespace NavModule_Two.Views
{
[Export]
[PartCreationPolicy(CreationPolicy.Shared)]
public class NavigationModuleTwoView : TreeViewItem
{
#region .ctor's
public NavigationModuleTwoView()
{
InitializeComponent();
}
#endregion
#region SetComponent
void InitializeComponent()
{
this.SetBinding(ItemsSourceProperty, new Binding() { Path = new PropertyPath("ContentTable") });
this.HeaderTemplate = GetHeaderTemplate();
this.SetBinding(HeaderProperty, new Binding() { Path = new PropertyPath("Root") });
this.ItemTemplate = this.GetHierarchicalTemplate();
this.Resources.Add("hierarchicalTemplate", this.ItemTemplate);
}
private DataTemplate GetHeaderTemplate()
{
DataTemplate dataTemplate = new DataTemplate(typeof(EntityBase));
//create stack pane;
FrameworkElementFactory stackPanel = new FrameworkElementFactory(typeof(StackPanel));
stackPanel.Name = "headerStackPanel";
stackPanel.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
stackPanel.SetBinding(StackPanel.ToolTipProperty, new Binding());
// Create Image
FrameworkElementFactory icon = new FrameworkElementFactory(typeof(ContentPresenter));
icon.SetValue(ContentPresenter.MarginProperty, new Thickness(2));
icon.SetBinding(ContentPresenter.ContentProperty, new Binding() { Path = new PropertyPath("Icon"), Converter = StringToImage.Default });
stackPanel.AppendChild(icon);
// create text
FrameworkElementFactory titleLabel = new FrameworkElementFactory(typeof(TextBlock));
titleLabel.SetValue(TextBlock.TextProperty, new Binding("Title"));
titleLabel.SetValue(TextBlock.FontWeightProperty, FontWeights.Bold);
titleLabel.SetValue(TextBlock.VerticalAlignmentProperty, VerticalAlignment.Center);
stackPanel.AppendChild(titleLabel);
//set the visual tree of the data template
dataTemplate.VisualTree = stackPanel;
return dataTemplate;
}
private HierarchicalDataTemplate GetHierarchicalTemplate()
{
//create the data template
HierarchicalDataTemplate dataTemplate = new HierarchicalDataTemplate(typeof(EntityBase));
//create stack pane;
FrameworkElementFactory stackPanel = new FrameworkElementFactory(typeof(StackPanel));
stackPanel.Name = "parentStackpanel";
stackPanel.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
stackPanel.SetBinding(StackPanel.ToolTipProperty, new Binding());
// Create Image as ContentPresenter
FrameworkElementFactory content = new FrameworkElementFactory(typeof(ContentPresenter));
content.SetValue(ContentPresenter.MarginProperty, new Thickness(2));
content.SetBinding(ContentPresenter.ContentProperty, new Binding() { Path = new PropertyPath("Icon"), Converter = StringToImage.Default });
stackPanel.AppendChild(content);
// create text
FrameworkElementFactory titleLabel = new FrameworkElementFactory(typeof(TextBlock));
titleLabel.SetBinding(TextBlock.TextProperty, new Binding() { Path = new PropertyPath("Title") });
titleLabel.SetValue(TextBlock.VerticalAlignmentProperty, VerticalAlignment.Center);
stackPanel.AppendChild(titleLabel);
//set the visual tree of the data template
dataTemplate.VisualTree = stackPanel;
dataTemplate.ItemsSource = new Binding() { Path = new PropertyPath("SubEntity") };
return dataTemplate;
}
#endregion
#region Setting DataContext
private NavigationModuleTwoViewModel _viewModel;
[Import]
public NavigationModuleTwoViewModel ViewModel //
{
get { return _viewModel; }
set
{
_viewModel = value;
this.DataContext = _viewModel;
// Samemu trzeba zadbać o właściwe dopisanie innych zadań
Populate();
}
}
#endregion
#region Help Method
// Inne zadania
private void Populate()
{
/* In Xaml appears so:
<i:Interaction.Triggers>
<i:EventTrigger EventName="Selected">
<i:InvokeCommandAction Command="{Binding SelectedCommand}"
CommandParameter="{Binding Path=SelectedItem, RelativeSource={RelativeSource AncestorType={x:Type TreeView}}}" />
</i:EventTrigger>
</i:Interaction.Triggers>
*/
// In Procedural Code:
InvokeCommandAction iCASelect = new InvokeCommandAction();
iCASelect.SetValue(InvokeCommandAction.CommandProperty, this._viewModel.SelectedCommand);
BindingOperations.SetBinding(iCASelect,
InvokeCommandAction.CommandParameterProperty,
new Binding()
{
Path = new PropertyPath("SelectedItem"),
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(TreeView), 1)
});
var eventTriggerSelect = new System.Windows.Interactivity.EventTrigger("Selected");//or MouseDubleClick
eventTriggerSelect.Actions.Add(iCASelect);
var triggerColl = Interaction.GetTriggers(this);
triggerColl.Add(eventTriggerSelect);
}
#endregion
}
}
其余部分类似于 NavModule_One
,尚未实现。
摘要
已知问题
缺乏使用箭头键进行正确导航
独立模块必须要么实现自己的 TabControlItem 关闭过程视图,如 DelegatCommand <object>
在 DataContext
中,要么使用 ViewModelBase NavInfrastructure
库的 DataContext
进行导航。另一个通用的解决方案可以是在 NavShell
中实现一个通用方法,并从各个视图收集它们希望确认关闭的信息,例如 IEventAggregator
。
在我看来,这些示例可以作为开发该主题的起点,以便检测和解决此类解决方案中出现的问题。上面示例中提供的一些解决方案似乎是不必要的或错误的,但它们之所以存在,是因为试图统一 WPF 和 Silverlight 的使用。
如果人们发现本文有趣,我将有兴趣进一步发展 PRISM 的主题。本文允许您根据需要为模块或特定视图添加工具栏。
Andrzej Skutnik