最近在学习 Prsim 和WPF,在油管上找到了一个 PrsimOutlook 项目,作者是 Brian Lagunas ,是当年 Prsim 搬家到GitHub时交给社区的三位贡献者之一.
照着视频学习加上有源代码,视频作者就是Prsim的作者之一,对Prsim相当的了解,视频思路清晰,代码水平高,值得初学习.
以往文章链接
C# WPF Prsim PrsimOutlook学习记录(一)
相关链接
- PrsimOutlook 油管视频链接
- Prsim官网文档
- Prsim官方提供的一些例程代码
- PrsimOutlook 油管视频配套代码 Github
- PrsimOutlook视频中用到的UI框架,可以在Nuget中使用试用版
- Brian Lagunas的个人博客,里面有很多文章
视频二 Prism Navigation 学习记录
注:学习记录只是思路的整理,并没有把所有源码都贴在这里,具体源码可以看上方的链接下载
这节视频主要讲了导航相关的东西,主要是点击 XamOutlookBar 中的内容,将右边 ContentRegion中View内容,切换成对应的内容.
首先让我们先看 MailGroup, 这里面主要是想做邮件的发送,删除,接收功能. Outlook在这个地方做的是树形的结构.
所以首先在 MailGroup.xaml 建立树节点
<ig:XamDataTree x:Name="_dataTree" ItemsSource="{Binding Items}">
<ig:XamDataTree.GlobalNodeLayouts>
<ig:NodeLayout Key="GlobalLayout" TargetTypeName="NavigationItem" DisplayMemberPath="Caption"></ig:NodeLayout>
</ig:XamDataTree.GlobalNodeLayouts>
</ig:XamDataTree>
这里需要指定 TargetTypeName 树节点的类名和 DisplayMemberPath 显示的名称.
这里是业务相关的信息,所以创建了 .NET 类库项目
创建 NavigationItem.cs类,一个标准的树型结构
namespace PrismOutlook.Business
{
public class NavigationItem
{
public string Caption { get; set; }
public string NavigationPath { get; set; }
public ObservableCollection<NavigationItem> Items { get; set; }
public NavigationItem()
{
Items = new ObservableCollection<NavigationItem>();
}
}
}
再创建一个 MainGroupViewModel.cs类, 由于 MainGroup.xaml是放在Menus目录下的,不是放在Prsim默认的 关联路径,所以需要在 MailModule.cs中使用 ViewModelLocationProvider 注册一下
public void RegisterTypes(IContainerRegistry containerRegistry)
{
ViewModelLocationProvider.Register<MailGroup, MainGroupViewModel>();
}
MailGroup 里面有一个binding Items,这里在 ViewModel里写一下
private ObservableCollection<NavigationItem> _items;
public ObservableCollection<NavigationItem> Items
{
get { return _items; }
set { SetProperty(ref _items, value); }
}
然后将树赋值给Items,Load到界面上
public MainGroupViewModel()
{
GenerateMenu();
}
void GenerateMenu()
{
Items = new ObservableCollection<NavigationItem>();
var root = new NavigationItem() { Caption = "Personal Folder", NavigationPath = "MailList" };
root.Items.Add(new NavigationItem() { Caption = "Inbox", NavigationPath = "" });
root.Items.Add(new NavigationItem() { Caption = "Deleted", NavigationPath = "" });
root.Items.Add(new NavigationItem() { Caption = "Sent", NavigationPath = "" });
Items.Add(root);
}
现在,树结构已经创建好了。
下面要做的是点击左边对应的结构,右边的视图需要做对应的切换。
下面先做 点击Mail 和Contacts,导航到对应的View
在MainWindow.xaml中
<ig:XamOutlookBar
prism:RegionManager.RegionName="{x:Static core:RegionNames.OutlookGroupRegion}"
SelectedGroupChanged="XamOutlookBar_SelectedGroupChanged"
DockPanel.Dock="Left" Width="200"></ig:XamOutlookBar>
这里使用Code Behind的方式,建立 XamOutlookBar_SelectedGroupChanged事件.
这里也告诉我们不要什么都用MVVM模式
在 MainWindowViewModel.cs中 写入以下代码
再次感慨 Prsim 很方便, 从容器中拿到 IRegionManager实例
调用 RequestNavigate,导航到指定的View
private readonly IRegionManager _regionManager;
public MainWindow(IRegionManager regionManager)
{
InitializeComponent();
Infragistics.Themes.ThemeManager.ApplicationTheme = new Office2013Theme();
this._regionManager = regionManager;
}
private void XamOutlookBar_SelectedGroupChanged(object sender, RoutedEventArgs e)
{
var group = ((XamOutlookBar)sender).SelectedGroup as IOutlookBarGroup;
if (group != null)
{
// TODO navigate to group.DefaultNavigationPath
_regionManager.RequestNavigate(RegionNames.ContentRegion, group.DefaultNavigationPath);
}
}
当然,调用这个请求导航前,必须要注册一下.
ContactsModule.cs 中
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<ViewA, ViewAViewModel>();
}
MailModule.cs 中
public void RegisterTypes(IContainerRegistry containerRegistry)
{
ViewModelLocationProvider.Register<MailGroup, MainGroupViewModel>();
containerRegistry.RegisterForNavigation<MailList, MailListViewModel>();
}
这里面有好多类其实之前就加了,但是考虑到思考的逻辑顺序,之前没加.现在挨个加上.
MailList.xaml 和 MailListViewModel.cs
这个界面是 点击Mail按钮时,ContentRegion 要导航到的View
<UserControl x:Class="PrsimOutlook.Modules.Mail.Views.MailList"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True">
<Grid>
<TextBlock Text="{Binding Title}"></TextBlock>
</Grid>
</UserControl>
namespace PrsimOutlook.Modules.Mail.ViewModels
{
public class MailListViewModel : BindableBase
{
public MailListViewModel()
{
}
private string _title = "Default";
public string Title
{
get { return _title; }
set { SetProperty(ref _title, value); }
}
}
}
IOutlookBarGroup 接口,给继承 OutlookBarGroup 的窗体 使用
namespace PrismOutlook.Core
{
public interface IOutlookBarGroup
{
string DefaultNavigationPath { get; }
}
}
这个接口用来记录 IOutlookBarGroup中各个内容中的默认导航路径
在 MailGroup.xaml.cs 中
public partial class MailGroup : OutlookBarGroup,IOutlookBarGroup
{
public MailGroup()
{
InitializeComponent();
}
public string DefaultNavigationPath => "MailList";
}
注意 这个 DefaultNavigationPath 写的路径要和Module 使用 RegisterForNavigation 注册的View类名一致,否则找不到.这是Prsim在后台提供的按名称匹配的一种模式
在 ContactsGroup.xaml.cs 中
namespace PrsimOutlook.Modules.Contacts.Menus
{
/// <summary>
/// ContactsGroup.xaml 的交互逻辑
/// </summary>
public partial class ContactsGroup : OutlookBarGroup,IOutlookBarGroup
{
public ContactsGroup()
{
InitializeComponent();
}
public string DefaultNavigationPath => "ViewA";
}
}
让我们来整理一下这个点击 Mali,Contacts按钮,右边ContentControl切换 View 的逻辑
- 点击按钮,进入 XamOutlookBar_SelectedGroupChanged 事件
- 将 sender as成 IOutlookBarGroup接口
- 调用
_regionManager.RequestNavigate(RegionNames.ContentRegion, group.DefaultNavigationPath);
进行导航. - 这个导航调用的前提是注册了导航,注册导航的View 类名要与字符串
group.DefaultNavigationPath一样,本例之前已经注册了 Navigation
containerRegistry.RegisterForNavigation<MailList, MailListViewModel>();
containerRegistry.RegisterForNavigation<ViewA, ViewAViewModel>();
- 所以点击按钮时,可以匹配到对应的视图,导航成功.
由于后面还要对 Mail中的 Inbox,Deleted,Send按钮进行导航,所以觉得这种方式不太好,下面要把这个导航改成 Prsim 中的 CompositeCommand , CompositeCommand 可以执行一个命令的同时,执行这个命令所绑定的所有子命令.
首先创建 IApplicationCommands接口,ApplicationCommands 继承 IApplicationCommands
namespace PrismOutlook.Core
{
public interface IApplicationCommands
{
CompositeCommand NavigateCommand { get; }
}
public class ApplicationCommands : IApplicationCommands
{
public CompositeCommand NavigateCommand { get; } = new CompositeCommand();
}
}
然后需要在 App.xaml.cs里通过单例的方式注册一下.
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterSingleton<IApplicationCommands, ApplicationCommands>();
}
然后给 CompositeCommand 注册一下子命令
首先在MainWindowViewModel.cs中 先创建这个子命令
private DelegateCommand<string> _navigateCommand;
public DelegateCommand<string> NavigateCommand =>
_navigateCommand ?? (_navigateCommand = new DelegateCommand<string>(ExecuteNavigateCommand));
void ExecuteNavigateCommand(string navigationPath)
{
if (string.IsNullOrEmpty(navigationPath))
throw new ArgumentNullException();
_regionManager.RequestNavigate(RegionNames.ContentRegion, navigationPath);
}
然后注册一下子命令
public MainWindowViewModel(IRegionManager regionManager, IApplicationCommands applicationCommands)
{
_regionManager = regionManager;
applicationCommands.NavigateCommand.RegisterCommand(NavigateCommand);
}
再次感叹一下 容器的好处, Prsim用起来真的很舒服.
在MainWindow.xaml.cs
public partial class MainWindow : XamRibbonWindow
{
private readonly IApplicationCommands _applicationCommands;
public MainWindow(IApplicationCommands applicationCommands)
{
InitializeComponent();
Infragistics.Themes.ThemeManager.ApplicationTheme = new Office2013Theme();
this._applicationCommands = applicationCommands;
}
private void XamOutlookBar_SelectedGroupChanged(object sender, RoutedEventArgs e)
{
var group = ((XamOutlookBar)sender).SelectedGroup as IOutlookBarGroup;
if (group != null)
{
_applicationCommands.NavigateCommand.Execute(group.DefaultNavigationPath);
}
}
}
将原来的 XamOutlookBar_SelectedGroupChanged函数写法换成这样的写法,完成了同样的功能.
现在是把点击 MailGroup 和Contacts的导航做好了,还有 MailGroup中的 Inbox,Send,Deleted的导航.
首先需要在 MainGroup.xaml ig:XamDataTree 节点中
<i:Interaction.Triggers>
<i:EventTrigger EventName="ActiveNodeChanged">
<prism:InvokeCommandAction Command="{Binding SelectedCommand}" TriggerParameterPath="NewActiveTreeNode.Data" />
</i:EventTrigger>
</i:Interaction.Triggers>
然后在 MainGroupViewModel.cs 写一下 SelectedCommand,这个Command其实是要做导航功能的,所以直接用这个Command调用之前写的 CompositeCommand,代码如下
private DelegateCommand<NavigationItem> _selectedCommand;
private readonly IApplicationCommands _applicationCommands;
public DelegateCommand<NavigationItem> SelectedCommand =>
_selectedCommand ?? (_selectedCommand = new DelegateCommand<NavigationItem>(ExecuteSelectCommand));
public MainGroupViewModel(IApplicationCommands applicationCommands)
{
GenerateMenu();
this._applicationCommands = applicationCommands;
}
void ExecuteSelectCommand(NavigationItem parameter)
{
//"MailList?Folder=Deleted"
//"MailList?id=Inbox"
if (parameter != null)
_applicationCommands.NavigateCommand.Execute(parameter.NavigationPath);
}
void GenerateMenu()
{
Items = new ObservableCollection<NavigationItem>();
var root = new NavigationItem() { Caption = "Personal Folder", NavigationPath = "MailList?id=Default" };
root.Items.Add(new NavigationItem() { Caption = "Inbox", NavigationPath = "MailList?id=Inbox" });
root.Items.Add(new NavigationItem() { Caption = "Deleted", NavigationPath = "MailList?id=Deleted" });
root.Items.Add(new NavigationItem() { Caption = "Sent", NavigationPath = "MailList?id=Sent" });
Items.Add(root);
}
其中还要注意一下,我们把 GenerateMenu 函数中的 NavigationPath 分别加上了对应的参数.现在的情况下还都是显示Default.想要实现一个点击 Inbox, MailList 中的 TextBlock 显示 Inbox.点击Sent,显示Sent.
这个就需要在 MailList.xaml.cs 中继承一下 INavigationAware,实现里面的接口
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
Title = navigationContext.Parameters.GetValue<string>("id");
}
OnNavigatedTo 函数的介绍如下
//
// 摘要:
// Called when the implementer has been navigated to.
// 执行器被导航到时调用
// 参数:
// navigationContext:
// The navigation context.
导航上下文
void OnNavigatedTo(NavigationContext navigationContext);
然后在点击 Mail 和 Contacts时发现 从 Contacts点回到Mail 时,没有把上一次 Contacts选中的View记录住.在MailGroup.xaml.cs里改一下这段代码
public string DefaultNavigationPath
{
get
{
var item = _dataTree.SelectionSettings.SelectedNodes[0] as XamDataTreeNode;
if(item != null)
{
return ((NavigationItem)item.Data).NavigationPath;
}
return "MailList";
}
}
这样就实现了 文章最开始的图片的效果.
后面老哥稍微重构了一下.
在Core中先创建了一个 ViewModelBase.cs,里面继承了BindableBase和IConfirmNavigationRequest
把方法设为虚方法
public class ViewModelBase : BindableBase,IConfirmNavigationRequest
{
public virtual void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
continuationCallback(true);
}
public virtual bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public virtual void OnNavigatedFrom(NavigationContext navigationContext)
{
}
public virtual void OnNavigatedTo(NavigationContext navigationContext)
{
}
}
然后把各个ViewModel都改为继承 ViewModelBase
后面还有一些内容,但是功能还没实现,只是搭了基础设施,我会放到下篇去写.这节视频这天是这位老哥的结婚18周年纪念日,迫不及待得去接老婆下班了.
总结
这节主要就是实现了点击Mail和点击Contacts,右边的ContentRegion进行视图的切换.
首先先实现了Mail和Contacts的切换,分别显示 MailList 和 ViewA的视图.
在MailGroup中创建了 Inbox,Sent,Deleted子节点
然后实现了点击 Inbox,Sent,Deleted时,右边 MailList 中的TextBlock显示对应的内容.
在实现中使用了CompositeCommand,RequestNavigate,使用了 IOutlookBarGroup 接口解耦.