MAUI新生5.5-导航路由Navigation
MAUI的Shell导航框架,也是以路由方式进行导航,并提供了两套导航方式,一是如前面章节所述的视觉层次结构,会自动建立导航路由,可以进行不同层次页面的导航切换;二是为页面手动注册路由,并执行代码导航。
一、注册路由
1、视觉层次结构页面的路由注册
在视觉层次结构中(Shell > FlyoutItem|TabBar > Tab > ShellContent),框架自动建立导航路由,可以直接进行导航切换。如果要通过代码导航到视觉层次页面,可以在定义视觉层次结构时,显示的定义路由,如:
<!--通过Route属性,显示注册路由--> <Shell ......> <FlyoutItem Route="animals"> <Tab Title="Domestic" Route="domestic"> <ShellContent Title="Cats" ContentTemplate="{DataTemplate view:Cats}" Route="cats" /> <ShellContent Title="Dogs" ContentTemplate="{DataTemplate view:Dogs}" Route="dogs" /> </Tab> <ShellContent Title="Monkeys" ContentTemplate="{DataTemplate view:Monkeys}" Route="monkeys" /> <ShellContent Title="Bears" ContentTemplate="{DataTemplate view:Bears}" Route="bears" /> <ShellContent Title="Elephants" ContentTemplate="{DataTemplate view:Elephants}" Route="elephants" /> </FlyoutItem> <ShellContent Title="About" ContentTemplate="{DataTemplate view:About}" Route="about" /> </Shell> <!--上例的导航路由结构为,如要导航到dogs,则可以通过路由//animals/domestic/dogs;如要导航到about,则可以通过路由//about--> animals domestic cats dogs monkeys bears elephants about
2、非视觉层次结构页面的路由注册
MAUI中还有很多页面,没有在视觉层次结构中定义,这些页面需要进行手动注册路由,一般在AppShell后台代码的构造函数中进行。
public partial class AppShell : Shell { public AppShell() { InitializeComponent(); //注册非视觉层次页面路由,所有路由名称需保持唯一,它们是全局的 //当导航到视觉层次结构中的路由时,并不会创建导航堆栈;但导航到非视觉层次页面路由时,创建导航堆 Routing.RegisterRoute("dogdetail",typeof(DogDetail)); Routing.RegisterRoute("monkeydetail", typeof(MonkeyDetail)); //也可以将路由注册到不同的视觉层次结构上,可以实现跟踪当前路由层次,导航到不同的页面 //如下例中,都是导航到detail,但在dogs路由层次结构中时,将导航到DogDetail;反之,将导航到MonkeyDetail Routing.RegisterRoute("dogs/detail", typeof(DogDetail)); Routing.RegisterRoute("monkeys/detail", typeof(MonkeyDetail)); } }
二、执行导航
1、在视觉层次结构中,可以直接执行导航跳转。注:这些跳转页面,不会进入到导航堆栈中。
2、非视觉层次结构的页面,需要通过编程式导航来实现。
//下例为Monkeys页面,通过按钮点击事件进行导航 public partial class Monkeys : ContentPage { ......
private async void Button_Clicked(object sender, EventArgs e) { //Shell.Current获得对当前Shell对象的引用,等效于((Shell)App.Current.MainPage) //Shell对象包括了GoToAsync导航方法,以及CurrentItem、CurrentPage、CurrentState、BackButtonBehavior等属性 await Shell.Current.GoToAsync("dogdetail"); await ((Shell)App.Current.MainPage).GoToAsync("dogdetail"); await DisplayAlert("显示当前路由", Shell.Current.CurrentState.Location.ToString(), "OK"); } }
3、绝对路由和相对路由
- 视觉层次结构的页面,可以通过绝对路径进行导航,如【Shell.Current.GoToAsync("//animals/domestic/dogs")】
- 非视觉层次结构的页面,可以通过相对路径进行导航,如【Shell.Current.GoToAsync("dogdetail")】
- 上下文导航。注册非视觉层次结构页面时,将路由注册到结构层次页面上,如Routing.RegisterRoute("monkeys/detail"...)。当在Monkeys页面中,导航到detail时,会匹配monkeys/detail。
- 向后导航。向后导航,【await Shell.Current.GoToAsync("..")】;向后导航与路由导航结合,【await Shell.Current.GoToAsync("../route")】;可以多次向后导航,【await Shell.Current.GoToAsync("../../route")】
- 关于路由导航,还有一些概念比较模糊,建议暂时按以上两种方式来导航
三、导航传参
1、传递参数。
1)方法一:查询字符串
//传递字符串:直接通过查询字符串传递参数 //传递一个字符串参数 await Shell.Current.GoToAsync("monkeydetail?name=sun"); //传递多个字符串参数 await Shell.Current.GoToAsync("monkeydetail?name=sun&sex=male"); //也可以使用内插值变量 string name = "sun" await Shell.Current.GoToAsync($"monkeydetail?name={name}");
2)方法一:字典类型参数
//传递对象:通过GoToAsync方法的第二个参数,IDictionary<string,object>字典类型对象传参 //传递一个对象 var p1 = new Person{Name="zs",Age=18}; var navigationParam = new Dictionary<string, object> { { "person1", p1 } }; await Shell.Current.GoToAsync($"persondetail", navigationParame); //传递多个对象 var p1 = new Person{Name="zs",Age=18}; var p2 = new Person{Name="ls",Age=28}; var navigationParam = new Dictionary<string, object> { { "person1", p1 } { "person2", p2 } }; await Shell.Current.GoToAsync($"persondetail", navigationParame);
2、接收参数。
1)方法一:通过QueryProperty特性,接收路由参数
//QueryProperty特性 //QueryProperty特性的第一个参数为接收路由参数的属性,第一个参数为路由参数的键名 //通过查询参数传递的路由参数,会自动转为键值对形式 //注意接收路由参数的属性,必须是可观察属性(MVVM),触发PropertyChanged事件 [QueryProperty(nameof(Name), "name")] [QueryProperty(nameof(Sex), "sex")] public partial class MonkeyDetail : ContentPage { public MonkeyDetail() { InitializeComponent(); BindingContext = this; //将BindingContext设置为当前对象 } //定义可观察属性Name来接收键名为name的路由参数 private string name; public string Name { get { return name; } set { name = value; OnPropertyChanged(); } } //定义可观察属性Sex来接收键名为sex的路由参数 private string sex; public string Sex { get { return sex; } set { sex = value; OnPropertyChanged(); } } } //XAML文件中使用路由参数 <ContentPage ......> <VerticalStackLayout> <Label HorizontalOptions="Center" Text="{Binding Name}" VerticalOptions="Center" /> <Label HorizontalOptions="Center" Text="{Binding Sex}" VerticalOptions="Center" /> </VerticalStackLayout> </ContentPage>
2)方法二:通过实现IQueryAttributable接口,接收路由参数(在ViewModel中使用)
//接收字符串路由参数 public class PersonDetailViewModel : IQueryAttributable, ObservableObject { [ObservableProperty] private string name; [ObservableProperty] private string sex; public void ApplyQueryAttributes(IDictionary<string, object> query) { name = HttpUtility.UrlDecode(query["name"].ToString()); sex = HttpUtility.UrlDecode(query["sex"].ToString()); } } //接收对象类型参数 public class PersonDetailViewModel : IQueryAttributable, INotifyPropertyChanged { [ObservableProperty] private Person person; public void ApplyQueryAttributes(IDictionary<string, object> query) { person = query["p1"] as Person; } ... }
四、路由守卫
1、全局守卫:类似于生命周期函数,Shell类提供OnNavigating(导航发生前)和OnNavigated(导航发生后)可重写方法,通过重写这两个方法,可以对导航进行控制,实现路由守卫的功能。OnNavigating和OnNavigated方法,在AppShell.xaml.cs中定义。
//OnNavigating和OnNavigated方法的常用属性和方法 public partial class AppShell : Shell { public AppShell() { InitializeComponent(); ...... } protected override void OnNavigating(ShellNavigatingEventArgs args) { base.OnNavigating(args); ShellNavigationState current = args.Current;//当前页,可通过Location属性获取URL ShellNavigationState target = args.Target; //目标页,可通过Location属性获取URL ShellNavigationSource source = args.Source; //导航类型,枚举,包括Unknown/Push/Pop/PopToRoot/Insert/Remove/ShellItemChanged/ShellSectionChanged/ShellContentChanged bool canCancle = args.CanCancel; //是否可以取消 bool canclled = args.Cancelled; //是否取消 args.Cancel(); //取消导航 //获取导航令牌,并完成导航 ShellNavigatingDeferral token = args.GetDeferral(); token.Complete(); } protected override void OnNavigated(ShellNavigatedEventArgs args) { base.OnNavigated(args); ShellNavigationState current = args.Current; //当前页 ShellNavigationState previous = args.Previous; //上一页 ShellNavigationSource source = args.Source; //导航类型 } } //案例一:取消各后导航 protected override void OnNavigating(ShellNavigatingEventArgs args) { base.OnNavigating(args); if (args.Source == ShellNavigationSource.Pop) { args.Cancel(); } } //案例二、跟进弹出框的选择按钮,确定是否继续导航 protected override async void OnNavigating(ShellNavigatingEventArgs args) { base.OnNavigating(args); ShellNavigatingDeferral token = args.GetDeferral(); var result = await DisplayActionSheet("Navigate?", "Cancel", "Yes", "No"); if (result != "Yes") { args.Cancel(); } token.Complete(); }
2、局部守卫:Shell层次结构中的Tab,提供了一系列的重写方法。我们可以自定义Tab的派生类,并重新定义这些重写方法。在视觉层次结构中使用自定义的Tab派生类,就可以控制这个Tab下的路由行为,从而实现局部守卫的功能。
public class MyTab:Tab { protected override void OnInsertPageBefore(Page page, Page before) { base.OnInsertPageBefore(page, before); } protected override void OnRemovePage(Page page) { base.OnRemovePage(page); } protected override Task<Page> OnPopAsync(bool animated) { return base.OnPopAsync(animated); } protected override Task OnPushAsync(Page page, bool animated) { return base.OnPushAsync(page, animated); } protected override Task OnPopToRootAsync(bool animated) { return base.OnPopToRootAsync(animated); } }
五、后退按钮
当导航到非结构层次页面时,会创建导航堆栈,并显示后退按钮,可以重新定义后退按钮的样式和行为
<!--Command在按下后退按钮时执行--> <!--其它样式和行为见名知意--> <ContentPage ......> <Shell.BackButtonBehavior> <BackButtonBehavior Command="{Binding BackCommand}" CommandParameter="命令参数" IconOverride="back.png" IsEnabled="True" IsVisible="True" TextOverride="后退" /> </Shell.BackButtonBehavior> ...... </ContentPage>
六、Navigation导航
1、除了Shell导航体系外,MAUI还提供了另外一套导航体系NavigationPage,两者不可以混用。主要通过Navigation类提供的各种导航方法进行导航,Navigation会创建导航堆栈,并显示后退按钮。
3、如果使用MAUI Blazor,不支持Shell导航,必须使用NavigationPage导航体系。
2、使用步骤:
1)创建导航根页面:
//修改App.xaml.cs,创建导航根页面 public partial class App : Application { public App() { InitializeComponent(); MainPage = new NavigationPage(new MainPage()); //区别于MainPage = new AppShell(); } }
2)进行编程式导航(还可以使用):
//导航入栈、导航出栈(后退)、模态导航入栈和出栈、将页面插入到导航栈、从导航栈中删除页面 await Navigation.PushAsync(new DetailsPage()); await Navigation.PopAsync(); await Navigation.PushModalAsync(new DetailsPage()); await Navigation.PopModalAsync();
await Navigation.InsertPageBefore(new DetailsPage());
await Navigation.RemovePage(new DetailsPage());
//导航传参①:通过构造函数,在目标页的构造函数中接收。 Contact contact = new Contact { Name = "Jane Doe", Age = 30, Occupation = "Developer", Country = "USA" }; ... await Navigation.PushModalAsync(new DetailsPage(contact)); //导航传参②:通过BindingContext属性传参,方便XAML页面直接绑定 Contact contact = new Contact { Name = "Jane Doe", Age = 30, Occupation = "Developer", Country = "USA" }; await Navigation.PushAsync(new DetailsPage { BindingContext = contact });