太长不看

https://github.com/sanshiliuxiao/PrismRegionWithHcTabControl

◭ 软件运行效果

更新

◭ 软件运行效果

吐槽

最近又开始整 WPF 的项目,做 UI 界面的时候想要像 Web 页面那样可以多栏打开,于是踩坑之路开始了。

不用说,本着能用就行的想法, 才有 WPF (.Net 5) 作为基础,配上 Prism 8 和 HandyControl 3.1.0, 想着写起来能很快搞定,然而开头就把自己给掉坑了, 还好最后勉强爬出来了,本文只会有一些思路和简略的代码,具体的代码请查看仓库。

预先知识

如果有用过 Prism 的朋友,肯定知道这个框架的功能有很多,比如 WPF 的 MVVM 模式,依赖注入,路由,事件通知等等。

为了能够划分界面,并填充内容, Prism 有 IRegion 的概念,即将页面分为一个一个的区域,填入的内容称为 View (视图), 它能够由 IRegionManager(的实现类) 统一进行管理。

<Window xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        xmlns:hc="https://handyorg.github.io/handycontrol">
 <hc:TabControl Name="MainTabControl"  prism:RegionManager.RegionName="MainTabControlName" />
</Window>

坑一

其实 Prism 已经实现了关于 WPF 自带的 TabControl 控件的适配器,而 HC: TabControl 继承自 TabControl ,因此也算间接实现了,但要是直接去用的话,就会出问题,查下文档得到的解决办法是自己实现一个新的适配器(照猫画虎)。

坑二

使用 Prism 的时候,官方推荐我们使用 RegionManager.RequestNavigate(raginName, viewName); 的方式去激活我们所需要展示的页面(视图)。他的好处是当视图不存在具体的实例时,它能够自动创造该视图,当实例存在时,只负责去切换到对应的视图。而且应该会添加路由信息,从而实现返回上个视图的效果。

然而在修复坑一的基础上,再使用这个方法去激活视图就会产生问题,会发现当实例存在时,无法切换到对应的视图(但可以点击 TebItem 手动切换,相当于只是单方面的使用了 TabControl 控件的相关功能)。

坑三

HC:TabControl 控件的(HC:TabItem) 子项可以由 Items 属性可以查看,并且每个子项其实是分为 HeaderContent 两个部分, Prism 的 IRegion 的区域信息可以由 Views 属性查看, 并且每一项仅仅只是包括 Content 部分。 所以如何将 IRegion.ViewsHC:TabControl.Items,进行同步其实也是一个问题。

回到起点

首先我们究竟需要去实现怎样的效果。

  1. 当页面实例不存在时,会产生新的实例并进行切换

  2. 当对应页面实例存在时,仅仅进行页面的切换

  3. 当不同的页面实例(模板相同,但展示的内容不同,这种需求是有的,而且很常见)需要被展示时,会产生新的实例并进行切换。

  4. 当存两个以上不同的页面实例(模板不同),如何进行定位

  5. 当存在两个以上不同的页面实例(模板相同,但内容不同),如何进行定位。

Prism 方面我们有 RegionManager.RequestNavigate(raginName, viewName); 方法可以进行视图的切换,而且如果 viewName 不同的话,切换到的视图也就是不同的。

那么我们需要一个 List<string> TabViewNames 来存储 viewName,问题的关键就变成如果根据 viewName 关联到 HC:TabControl.ItemsregionManager[regionName].Views(也即 IRegion.VIews) 里面对应的各个子项。

为了解决这些问题,创建了一个 TabSwitchOrAddHelper.cs 的帮助类。具体的代码可以去仓库看源码。下面之分享几个要点。

  1. 帮助类需要几个依赖,所以我在 MainWindow.OnSourceInitialized 方法中传递进去了。

  2. 切换和添加页面由 SwitchOrAddTab 函数统一包裹,这样就具有统一的入口,但核心功能在另外两个函数里。

  1. 添加页面时,我额外让页面继承了 IViewName 接口,把 传入 viewName 一并写入到 这个页面里,方便之后进行提取(切换和关闭页面时用的到)。
  1. 切换页面时,根据传入的 viewName 去跟 HC:TabControl.items 里各个子项里面的 ViewName 属性比较,然后设置 HC:TabControl.SelecetdItem 属性

  2. 最后,在确保 Region.Views 中已经存在对应页面的前提下,再使用 RegionManager.RequestNavigate 方法,这样会产生路由信息(大概),但不会自己创建新的实例,而是激活这个实例。

  3. 当移除页面时,同理,移除 TabViewNames 里对应的 viewName,移除 HC:TabControl.items 对应项。移除 IRegion.Views 对应项。

存在的问题

  1. 如果要为 MainWindow.xaml 里的 Region 区域设置默认视图时,需要在 MainWindow.xaml.cs 里面的 OnSourceInitialized 方法中去调用 TabSwitchOrAddHelper.SwitchOrAddTab 方法,而不是在 MainWindowViewModel.cs 中去调用。 (TabSwitchOrAddHelper.SwitchOrAddTab 需要依赖 HC:TabControl 实例,而 MainVindowViewModel.cs 在 MainWindow.cs 之前实例化)。

  2. 没有实现带参数的页面跳转,这个很好实现,已经尝试过,但没有写出完整的代码,之后补充。(因为端午放假了。)

  3. 如果我直接点击 HC:TabControl 控件上的子项,路由信息是不会被记录上的,所以页面返回上一页这个功能可能存在问题,不过这个问题聊胜于无。(解决的办法是将 IRegionManager.RequestNavigate(viewName);) 放到 HCTabControl.SelectionChanged 事件中去执行, 但我不想这么做)。

  4. 还有其他未被我发现的问题。

更新

书接上文, 继续实现带参数页面的跳转功能

坑四

要实现才参数跳转,需要视图去实现 INavigationAware 接口, 这个接口提供三个函数: IsNavigationTarget(判断是否允许导航), OnNavigatedFrom (离开视图触发), OnNavigatedTo(进入视图触发)。坑就坑在源码里的视图获取和我需要的功能(需求 3 和 需求 5)相冲突了。

需求 3 可表示为,现在我有两个视图,一个是视图 ViewA ( ViewType = ViewA ViewName = ViewA, UrlSource = ViewA), 另一个视图 View-aaa ( ViewType= ViewA ViewName = View-aaa, UrlSource = View-aaa)

当需要导航到 ViewA 时,会触发 OnNavigatedFromOnNavigatedTo 函数, 当导航到 View-aaa 时,永远只会触发 OnNavigatedFrom 函数,这真的很奇怪啊!!!

最后又是折腾了半天,查看源码,最终才发现这究竟是为什么,关键代码如下:

// Prism.Regions.RegionNavigationContentLoader
private bool ViewIsMatch(object v, string navigationSegment)
{

  var names = new[] { v.GetType().Name, v.GetType().FullName};
  return names.Any(x => x.Equals(navigationSegment, StringComparison.Ordinal));
}
private static bool ViewIsMatch(Type viewType, string navigationSegment)
{
    var names = new[] { viewType.Name, viewType.FullName };
    return names.Any(x => x.Equals(navigationSegment, StringComparison.Ordinal));
}

这是什么意思呢,意思就是说, ViewA or aaa.bbb.ViewA 肯定不会跟 View-aaa 匹配上,最终返回一个 null

故此,原因明了。 因为下面这两行代码将变的无效了。

// Prism.Regions.RegionNavigationService
// The view can be informed of navigation
Action<INavigationAware> action = (n) => n.OnNavigatedTo(navigationContext);
MvvmHelpers.ViewAndViewModelAction(view, action);

解决方案

既然官方不支持,如果我自己稍微改动一下源码,让他能够找到对应的视图不就行了。最后在魔改代码之后,自己生成对应的 dll 文件,然后进行引用,就解决这个问题啦。

不过感觉在 WPF 应用中,这个场景还是比较少见吧,网上也查不到相关的资料。还向官方提 ISSUES 啦,得到如下回复:


There are a lot of issues with your code. You are implementing a lot of poor coding choices that are not recommended by the Prism team. Also, there is no need to have the same view registered with different names. Only register a specific view type once.

大概意思就是这种需求是伪需求!!!一个视图只需要一个名字。

写在最后

为了解决这个问题花了老多时间了,感谢给予我帮助的各位大佬,写这篇博客也老花时间了,一个下午加一个晚上的时间。

端午快乐!!!