一. 目标
- 了解和使用Prism框架的区域(
Region
)- 了解区域管理的实现方式(通过
RegionAdapter
)以及如何实现自定义的RegionAdapter
- 如何通过区域进行导航
- 区域导航的时候如何传递参数
- 区域导航的时候如何添加导航日志(用来上一步,下一步导航操作)
二. 技能介绍
① 什么Region?
Prism框架中
Region
可以理解为一个UI
容器,这些容器可以动态加载和卸载不同的视图和组件.Region
区域通常是一个控件,比如ContentControl
,ItemsControl
,TabControl
. 可以这么去理解Region
,它就像是一个占位符,可以动态地向这个占位符中注入或者移除视图.
② 如何使用Region
- 方式1: 在XAML中声明定义,通过RegionName去定义标识一个区域
<ContentControl prism:RegionManager.RegionName="MainContentRegion" />
我们定义完RegionName
之后,这个Region
代表的是哪个视图呢?通过什么方式将Region
和视图关联起来呢?使用RegisterViewWithRegion()
方法.
注意:
- 容器的内容也就是注册的
View
官方默认实现的支持的是UserControl
,如果你想要支持Window
和Page
控件视图,你需要自定义一个区域适配器.- 在将
View
和Region
关联起来,在哪里做呢.理论上来说只要能够获取到IRegionManager
对象的地方都可以做这件事,但是我们一般推荐在Module
的初始化配置中去完成,如果说你不使用Module
这个功能,尽量就是在App.cs
中完成,为什么不推荐在ViewModel
中完成呢,因为这样更符合MVVM
准则,视图操作的部分,不放到ViewModel
中去.下面再App.cs
中实现View
和Region
的关联
RegisterViewWithRegion()
有四种方式,我会一一列举出来个各个方式的用法以及这种方法有什么好处.
第一种方式:通过字符串ViewName
IRegionManager RegisterViewWithRegion(string regionName, string viewName);
- 介绍: 通过
viewName
字符串的方式来关联Region
和View
. - 要求:
viewName
已经通过其他方式注册到容器中,显示注册,如果没有注册,这里不会主动创建viewName
关联的那个View
对象,如果注册了,这里可以获取到对应的View
对象,并和对应的Region
关联. - 举例:
首先是这个View要注册到容器中,并且使用的是View的默认注册名称
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// 将ViewA注册到容器中,并且是单例模式,每次请求都是同一个实例对象
containerRegistry.RegisterSingleton<ViewA>();
// 瞬态模式,每次请求都是一个新的实例对象
containerRegistry.Register<ViewA>();
// 作用域模式,不同的应用,作用域的界定不太一样,ASP.NET应用充一个作用域通常被定义为一个HTTP请求的生命周期
// 在桌面或者是移动应用中,作用域可能与视图的呈现周期或者用户会话相关联.
containerRegistry.RegisterScoped<ViewA>();
}
第二就是在初始化流程中进行关联
private void RegisterViewForRegion()
{
var regionManager = Container.Resolve<IRegionManager>();
regionManager.RegisterViewWithRegion("MainContentRegion", nameof(ViewA));
}
第二种方式:通过View类型
IRegionManager RegisterViewWithRegion(string regionName, Type viewType);
- 介绍: 通过
View
的类型来和Region
进行关联. - 要求: 这里一般也是要求在依赖注入容器中先注册,如果没有注册,这里会创建一个新的视图作为关联
Region
的对象. - 例子:
private void RegisterViewForRegion()
{
var regionManager = Container.Resolve<IRegionManager>();
//regionManager.RegisterViewWithRegion("MainContentRegion", nameof(ViewA));
regionManager.RegisterViewWithRegion("MainContentRegion", typeof(ViewB));
}
这里我们不需要注册ViewB
依然可以正常的显示ViewB
,但是不建议这么去做.
第三种方式:通过委托的方式
IRegionManager RegisterViewWithRegion(string regionName, Func<object> getContentDelegate);
- 介绍: 通过委托的方式来获取
View
,然后实现View
和Region
的注册 - 特点: 不依赖于注入容器,但是这种方式需要手动去创建一个
View
视图对象 - 例子:
regionManager.RegisterViewWithRegion("MainContentRegion", () => new ViewB());
第四种方式:通过泛型的方式
public static IRegionManager RegisterViewWithRegion<T>(this IRegionManager regionManager, string regionName)
{
return regionManager.RegisterViewWithRegion(regionName, typeof(T));
}
- 介绍: 通过泛型的方式来注册
View
- 特点: 本质上和第二种通过类型是相同的,只是通过泛型这种方式更直观,也是需要依赖注入容器中去注册,如果没有注册,会自动创建一个新的
View
实例,根据这个类型. - 例子:
regionManager.RegisterViewWithRegion<ViewB>("MainContentRegion");
- 方式二:后台代码中定义
RegionManager.SetRegionName(ElementName,"RegionName");
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
/*
* 注意这里RegionManager.SetRegionName(DependencyName,RegionName)一定要在InitializeComponent()之后调用.
* 因为InitializeComponent()方法是WPF应用程序中自动生成的方法,用于加载窗口或者控件的布局和资源.
* 具体来说,会加载XAML文件,并创建定义在XAML中的控件实例.
* 而下面这行代码的作用就是将名为mainCtr的控件注册为一个区域,区域名称为MainContentRegionBackend,如果在
* InitializeComponent()之前调用的话,因为mainCtr控件可能还么有创建,因此会引发异常,
* 会提示注册一个不存在的控件,null Object.
*/
RegionManager.SetRegionName(mainCtr, "MainContentRegionBackend");
}
}
③ RegionManager的功能
- 定义区域
prism:RegionManager.RegionName="SomeRegionName" (XAml文件中)
RegionManager.SetRegionName(ctrName,RegionName)(Backend后台代码中)
- 注册视图
var regionManager = Container.Resolve<IRegionManager>();
regionManager.RegisterViewWithRegion("MainContentRegion", nameof(ViewA));
regionManager.RegisterViewWithRegion("MainContentRegion", typeof(ViewB));
regionManager.RegisterViewWithRegion("MainContentRegion", () => new ViewB());
regionManager.RegisterViewWithRegion<ViewB>("MainContentRegion");
- 导航视图
regionManager.Regions["SomeRegion"].RequestNavigate(nameof(ViewA));
regionManager.RequestNavigate("MainContentRegion", nameof(ViewA));
- 添加或者移除视图
你可以通过编程方式向区域添加视图,或者从区域中移除视图.注意这里添加视图和移除视图
public void AddView()
{
var regionManager = Container.Resolve<IRegionManager>();
var view = Container.Resolve<ViewA>();
regionManager.Regions["MainContentRegion"].Add(view);
}
public void RemoveView()
{
var regionManager = Container.Resolve<IRegionManager>();
var view = regionManager.Regions["MainContentRegion"].GetView("ViewA");
regionManager.Regions["MainContentRegion"].Remove(view);
}
- 维护区域集合Regions以及提供对区域的访问
Regions
集合通过字符串Key
的方式可以获取指定区域,然后对区域进行操作,比如添加区域视图,获取区域视图,以及查看区域视图的个数. 之前我们说过,对于区域的操作尽量不要放到视图模型(也就是ViewModel
)里面去,下面演示一下,放到ViewModel
里面会出现的一个异常事件,比如下面的代码,你肯定意想不到会出现什么问题.
namespace PrismRegionContent.ViewModels
{
public class MainViewModel : BindableBase
{
private readonly IRegionManager _regionManager;
public MainViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
// 这里已经在MainContentRegion中添加了视图,为什么MainCntentRegion还不存在呢,然后下面
// 打印的区域的集合中的个数居然是0.
_regionManager.RegisterViewWithRegion("MainContentRegion", typeof(ViewA));
System.Diagnostics.Debug.WriteLine($"此时的区域集合中的区域个数为: {_regionManager.Regions.Count()}");
}
}
}
注意:
上面的程序是一个错误的示范,但是这个错误的示范,可以让我们学到一些知识.这里为什么获取到的
Regions.Count()
为0呢?
_regionManage.RegisterViewWithRegion
这一行代码并不会直接把视图放到区域中,比如本例子,本例中到这里区域还没有创建呢,区域集合中也没有区域,这行代码的真正作用是什么呢,这里只是设置一个指令,告诉Prism
框架,一旦相应的区域控件被加载且区域创建后,就应该将视图注入到该区域中,如果这个时候区域还不存在,Prism
内部会记住这个指定,然后在区域变得可用时执行它 .
所以,我们可以得出结果,代码执行到这里的时候,区域还没有创建,那么区域又是在什么时候创建的呢?
区域加载的时机:
- 首先是加载视图的构造方法,然后是视图模型构造方法,建立对应上下文环境.
- 视图继续加载,如果有任务其他初始化代码或者是资源需要加载,这个过程会继续,包括区域加载.
- 一旦视图和其所有的子元素加载完毕,Loaded 事件会被触发,所以可以在
Loaded
事件中去做区域的处理,但是也不建议这么去做,尽量还是在模块加载和初始化的地方或者是在App.cs
的OnInitialize()
方法中去做这件事,注意一件事情,OnInitialize()
方法中的base.OnInitialized()
方法执行的时候,居然会先执行CreateShell()
方法也就是说这个时候会先创建区域,以为MainWindow()
多创建了,所以在其后面执行区域视图相关操作也是合理的,比如下面的程序就是很好的实践,并且可以得到自己预期的结果.
namespace PrismRegionContent
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : PrismApplication
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void OnInitialized()
{
base.OnInitialized();
// 配置导航视图
RegisterViewForRegion();
}
private void RegisterViewForRegion()
{
var regionManager = Container.Resolve<IRegionManager>();
regionManager.RegisterViewWithRegion<ViewA>("MainContentRegion");
var regions = regionManager.Regions;
System.Diagnostics.Debug.WriteLine($"regionCount = {regionManager.Regions.Count()}");
}
}
}
- 激活和停用视图
激活视图意味着在区域中使某个视图成为当前可见的视图,而停用则是使视图不再可见.这对于使用ItemsControl
或者TabControl
等控件非常有用,你可以动态地控制哪些视图是活动的.
public void ActivateView()
{
var region = regionManager.Regions["MainContentRegion"];
var view = region.GetView("ViewA");
region.Activate(view);
}
public void DeactivateView()
{
var region = regionManager.Regions["MainContentRegion"];
var view = region.GetView("ViewA");
region.Deactivate(view);
}
④ 区域适配器
为什么ContentControl可以使用RegionManager功能呢?
假如我们使用StackPanel
指定它的附加属性RegionName
是不是可行的呢?例如下面的代码
<StackPanel prism:RegionManager.RegionName="StackPanelRegionSimple">
这里虽然没有报错,但是在运行的时候就会报错了,原因就是StackPanel
没有实现区域适配器,我们看源码的时候可以看到他们实现了对应的区域适配器.
Prism框架中默认的区域适配器有以下几种:
- ContentControlRegionAdapter: 用于单内容控件如
ContentControl
和UserControl
. - ItemsControlRegionAdapter: 适用于可以包含多个子项的控件,如
ItemsControl
. - SelectorRegionAdapter: 用于具有选择功能的控件,如
TabControl
,ComboBox
,ListBox
,Ribbon
如何自定义适配器类:
-
1.创建自定义适配器类
继承RegionAdapterBase<T>
类,其中T
是你要适配的XAML
控件类型.你需要实现CreateREgion()
方法以确定如何处理区域,并且可能需要重写Adapt()
方法来定义如何将视图添加到控件中 -
2. 注册适配器
在应用程序启动时,通过覆盖ConfigureRegionAdapterMappings()
方法来向RegionAdapterMappings
注册你的自定义适配器.这个过程通常在你的Bootstrapper
或者相似的启动配置类中完成.
protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
var mappings = base.ConfigureRegionAdapterMappings();
mappings.RegisterMapping(typeof(MyCustomControl), Container.Resolve<MyCustomRegionAdapter>());
return mappings;
}
- 实现适配器逻辑
在你的适配器类中,定义控件如何响应区域内的变化,例如如何添加或删除视图。
通过这种方式,你可以为几乎任何XAML控件创建自定义的区域适配器,使其能够与Prism框架无缝集成,并支持复杂的动态视图注入和管理。这样做可以大大提高应用程序的可维护性和扩展性。
namespace MyDefineRegionAdapter
{
public class StackPanelRegionAdapter : RegionAdapterBase<StackPanel>
{
public StackPanelRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory) : base(regionBehaviorFactory)
{
}
protected override void Adapt(IRegion region, StackPanel regionTarget)
{
if (regionTarget == null)
{
throw new ArgumentNullException(nameof(regionTarget));
}
/*
* 这里因为是StackPanel,所以新添加项可能有几项,所以要遍历所有的新添加项,添加到
* 区域集合中去
*/
region.Views.CollectionChanged += (s, e) =>
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (FrameworkElement item in e.NewItems!)
{
region.Add(item);
}
}
};
}
protected override IRegion CreateRegion()
{
return new Region();
}
}
}
区域适配器的注册和关联
protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings)
{
base.ConfigureRegionAdapterMappings(regionAdapterMappings);
regionAdapterMappings.RegisterMapping(typeof(StackPanel), Container.Resolve<StackPanelRegionAdapter>());
}
regionAdapterMappings.RegisterMapping(typeof(StackPanel), Container.Resolve<StackPanelRegionAdapter>());
这一行代码的作用是干嘛的?
告诉
Prism
的区域适配器映射的时候,当你想要一个StackPanel
作为区域的时候,应该使用StackPanelAdapter
类型的实例来完成相应的匹配工作.但是这里有一个问题就是我们程序中没有注册StackPanelRegionAdapter
为单例模式,这里每次Resolve
是不是都会有一个新的实例呢,如果每次都是用新的实例,会不会有影响呢?Prism中注册区域适配器通常是在应用程序启动的配置截断完成的,区域适配器映射通常只会设置一次.所以即使StackPanelRegionAdapter
不是单例,它也只会在注册过程中被解析一次,然后这个实例就会用于后续所有StackPanel
类型的区域适配工作,所以不是单例影响也不大.
⑤ 区域导航的使用步骤
1.创建导航区域
这里有一个技巧,就是一般建议将区域名称字符串设置为一个变量,而不是采用硬编码,然后通过
x:static
的方式引入到XAML
文件中,在WPF
中,x:Static
允许你在XAML
中访问静态成员(包括常量,静态字段和静态属性),在使用x:Static
的时候,目标资源不一定要在静态类中定义,也就是说常量,静态字段和静态属性所属的类不一定非要是静态类,但是被引用的成员必须是静态的(静态属性,静态字段,或者是常量)
namespace RegionNavigateSimple.Common
{
public class RegionNameManager
{
public static string MainViewRegion = nameof(MainViewRegion);
}
}
在前面xaml中引用:
<ContentControl Grid.Row="1" prism:RegionManager.RegionName="{x:Static common:RegionNameManager.MainViewRegion}" />
注册视图和视图模型到导航系统中
namespace RegionNavigateSimple
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : PrismApplication
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// RegisterForNavigation的目的就是这个视图可以后续通过regionContainer进行导航到
containerRegistry.RegisterForNavigation<MainWindow, MainViewModel>();
containerRegistry.RegisterForNavigation<ViewA, ViewAViewModel>();
containerRegistry.RegisterForNavigation<ViewB, ViewBViewModel>();
}
}
}
实现对应的导航请求,在MainViewModel中实现其点击命令,在执行命令的过程中实现导航
namespace RegionNavigateSimple.ViewModels
{
public class MainViewModel : BindableBase
{
private readonly IRegionManager _regionManager;
public MainViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
OpenViewACommand = new DelegateCommand(DoOpenViewA);
OpenViewBCommand = new DelegateCommand(DoOpenViewB);
}
#region 1. 导航命令
public DelegateCommand OpenViewACommand { get; private set; }
public DelegateCommand OpenViewBCommand { get; private set; }
private void DoOpenViewA()
{
_regionManager.Regions[RegionNameManager.MainViewRegion].RequestNavigate(nameof(ViewA));
}
private void DoOpenViewB()
{
_regionManager.Regions[RegionNameManager.MainViewRegion].RequestNavigate(nameof(ViewB));
}
#endregion
}
}
导航请求的时候也有另外一种写法,使用regionManager.RequestNavigate(regionName,ViewName)
private void DoOpenViewA()
{
_regionManager.RequestNavigate(RegionNameManager.MainViewRegion, nameof(ViewA));
}
private void DoOpenViewB()
{
_regionManager.RequestNavigate(RegionNameManager.MainViewRegion, nameof(ViewB));
}
⑥ 区域导航参数
在请求导航的时候,我们可以使用NavigationParameters
来实现参数的传递:
var parameters = new NavigationParameters();
// parameters是键值对的形式存储的
parameters.Add("key","value");
_regionManager.RequstNavigate("MainRegion","TargetView",parameters);
那么这个参数,目标视图,也就是导航的目标视图是怎么接收的呢,需要通过实现INavigationAware
接口来实现,一般在目标视图的视图模型继承自INavigaionAware
接口来实现其对应的方法.
我们以导航到ViewB
的过程中,传递一个字符串,比如一个暗号123456
namespace RegionNavigateSimple.ViewModels
{
public class ViewBViewModel : BindableBase, INavigationAware
{
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
if (navigationContext.Parameters.ContainsKey("cipher"))
{
var cipherVal = navigationContext.Parameters["cipher"] as string;
if (!string.IsNullOrEmpty(cipherVal))
{
System.Diagnostics.Debug.WriteLine($"ViewB导航接收到暗语: {cipherVal}");
}
}
}
}
}
从上面可以看出来在实现INavigateAware接口的时候,它里面有三个方法,这三个方法分别代表不同的含义:
- OnNavigateTo(NavigationContext navigationContext)方法
调用时机:
当导航到某个视图或者是视图模型的时候,该方法会被调用,它会先调用,作用就是允许你在视图或者是视图模型成为导航目标后执行一些初始化的工作,比如接收参数,处理参数.
- OnNavigatonFrom(NavigaionContext navigationContext)
调用时机:
当你从当前视图或者视图模型导航离开时,该方法会被调用.就是比如你从
ViewA
导航到ViewB
的时候,
如果导航成功,就是要打开ViewB
以及要离开ViewA
的时候,ViewA
对应的ViewModel
中的OnNivagateFrom
方法会被调用.
作用就是允许你再视图或者视图模型不再是导航目标后执行清理或者是保存状态的代码.这里是保存当前视图状态或者是取消订阅事件的好地方.
- IsNivagationTarget(NavigationContext navigationContext)
调用时机和作用:
当你导航到一个视图或者视图模型的时候,这个方法会被调用,它的作用是什么呢?比如,假如之前导航的视图为
ViewA
,如果接下来还有导航到ViewA
的请求,如果这个函数返回true
,那么就会重用之前的ViewA
和其对应的ViewModel
,如果返回False
,就会宠幸创建一个新的ViewA
和ViewModel
.如果有状态的改变和更新这种,就要返回false,每次都返回新的视图,如果没有状态的改变和更新,可以返回true
,可以提高程序的响应速度和资源利用率.
⑦ 区域导航确认框
如果你需要在导航发生之前提醒用户是否要进行导航的操作的时候怎么做呢?
要实现
IConfirmNavigationRequest
接口,它在INavigationAware
接口基础上又进行了一层封装,新增了ConfirmNavigationRequest()方法
这个方法的作用是什么呢?调用的时机?
namespace RegionNavigateSimple.ViewModels
{
public class ViewBViewModel : BindableBase, IConfirmNavigationRequest
{
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
{
if (MessageBox.Show("是否要离开", "离开提示框", MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
continuationCallback(true);
}
else
{
continuationCallback(false);
}
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
System.Diagnostics.Debug.WriteLine("in IsNavigationTarget()");
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
System.Diagnostics.Debug.WriteLine("in OnNavigatedFrom()");
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
System.Diagnostics.Debug.WriteLine("in OnNavigatedTo()");
}
}
}
continuationCallback(true): 继续后面的导航,允许导航
continuationCallback(false): 中断导航,导航取消
这几个函数的执行顺序是什么样的呢?
情况1: 导航离开视图时:
比如从ViewA
导航到ViewB
的时候,ViewA
的ConfirmNavigationRequest()
方法被调用,来判断是否要进行导航,如果确认导航,继续调用OnNavigatedFrom
,导航离开前的一些状态保存或者是一些取消订阅操作.
情况2: 导航到达时:
比如还是从ViewA
导航到ViewB
,ViewB
的IsNavigationTarget
先调用,用来判定是否重用之前的视图和视图模型,然后是OnNavigatedTo()
方法被调用,对参数进行一些处理,并且做一些视图的初始化工作.
⑧ 导航日志
什么是导航日志?我们在导航操作的时候,比如打开
ViewA
或者是打开ViewB
,可以把这些操作记录起来,然后使用上一步和下一步,回到之前的操作.其实就是一个导航的历史记录影像,然后可以通过巡航按钮进行上一步和下一步操作的一个功能.实现方式如下:
private void DoGoForward()
{
var journal = _regionManager.Regions[RegionNameManager.MainViewRegion].NavigationService.Journal;
if (journal.CanGoForward)
{
journal.GoForward();
}
}
private void DoGoBack()
{
var journal = _regionManager.Regions[RegionNameManager.MainViewRegion].NavigationService.Journal;
if (journal.CanGoBack)
{
journal.GoBack();
}
}
private void DoOpenViewA()
{
_regionManager.RequestNavigate(RegionNameManager.MainViewRegion, nameof(ViewA), NavigationComplete);
}
private void NavigationComplete(NavigationResult result)
{
if (result.Result == true)
{
System.Diagnostics.Debug.WriteLine($"导航到{result.Context.Uri}视图成功");
}
else
{
System.Diagnostics.Debug.WriteLine($"导航到{result.Context.Uri}视图失败");
}
}
private void DoOpenViewB()
{
var parameters = new NavigationParameters()
{
{"cipher","123456" },
};
_regionManager.RequestNavigate(RegionNameManager.MainViewRegion, nameof(ViewB),
NavigationComplete, parameters);
}
上面的代码我们要明白两个知识点:
Prism
的导航日志INavigationJournal
的管理是自动运行的,其中包括记录成功的导航日志.而NavigationCallback
是用来检测导航结果检查的,可以在这里根据导航成功或者失败做一些特殊处理.导航日志对象INavigationJournal
是通过NavigationService
获取到的. 导航日志的创建也可以手动参与维护,如下面这种导航日志记录的方式也是可以的,并且自己还可以自定义导航日志,比如有些操作自己可以自定义过滤掉.
private IRegionNavigationJournal? _navigationJournal;
private void DoGoForward()
{
if (_navigationJournal != null && _navigationJournal.CanGoForward)
{
_navigationJournal?.GoForward();
}
}
private void DoGoBack()
{
if (_navigationJournal != null && _navigationJournal.CanGoBack)
{
_navigationJournal?.GoBack();
}
}
private void DoOpenViewA()
{
_regionManager.RequestNavigate(RegionNameManager.MainViewRegion, nameof(ViewA), NavigationComplete);
}
private void NavigationComplete(NavigationResult result)
{
if (result.Result == true)
{
// 这里添加导航日志
_navigationJournal = result.Context.NavigationService.Journal;
System.Diagnostics.Debug.WriteLine($"导航到{result.Context.Uri}视图成功");
}
else
{
System.Diagnostics.Debug.WriteLine($"导航到{result.Context.Uri}视图失败");
}
}