Xamarin.Forms Shell 导航

Download Sample下载示例

Xamarin.Forms Shell 包括基于 URI 的导航体验:使用路由导航到应用程序中的任何页面,而无需遵循设置的导航层次结构。 此外,它还能够向后导航,不必访问导航堆栈上的所有页面。

Shell 类定义以下与导航相关的属性:

BackButtonBehaviorCurrentItemCurrentState 属性由 BindableProperty 对象提供支持,这意味着这些属性可以作为数据绑定的目标。

导航是通过从 Shell 类调用 GoToAsync 方法来执行的。 即将执行导航时,将触发 Navigating 事件,并在导航完成时触发 Navigated 事件。

注意

仍可在 Shell 应用程序中的页面间通过使用 Navigation 属性执行导航。 有关详细信息,请参阅分层导航

Routes

通过指定要导航到的 URI,可以在 Shell 应用程序中执行导航。 导航 URI 可以有三个组件:

  • 一个路由,它定义了作为 Shell 视觉层次结构的一部分存在的内容的路径
  • 一个页。 Shell 视觉层次结构中不存在的页可以从 Shell 应用程序中的任何位置推送到导航堆栈。 例如,不会在 Shell 视觉层次结构中定义详细信息页,但可以根据需要将其推送到导航堆栈。
  • 一个或多个查询参数。 查询参数是可以在导航时传递到目标页的参数。

当导航 URI 包含所有三个组件时,结构为://route/page?queryParameters

注册路由

可以通过 FlyoutItemTabBarTabShellContent 对象的 Route 属性在这些对象上定义路由:

<Shell ...>
    <FlyoutItem ...
                Route="animals">
        <Tab ...
             Route="domestic">
            <ShellContent ...
                          Route="cats" />
            <ShellContent ...
                          Route="dogs" />
        </Tab>
        <ShellContent ...
                      Route="monkeys" />
        <ShellContent ...
                      Route="elephants" />  
        <ShellContent ...
                      Route="bears" />
    </FlyoutItem>
    <ShellContent ...
                  Route="about" />                  
    ...
</Shell>

注意

Shell 层次结构中的所有项都有一个与之关联的路由。 如果未设置路由,则会在运行时生成一个路由。 但是,不能保证生成的路由在不同应用程序会话之间都是一致的。

上述示例创建了以下路由层次结构,可用于编程导航:

animals
  domestic
    cats
    dogs
  monkeys
  elephants
  bears
about

要导航到 dogs 路由的 ShellContent 对象,绝对路由 URI 为 //animals/domestic/dogs。 同样,要导航到 about 路由的 ShellContent 对象,绝对路由 URI 为 //about

警告

如果检测到重复路由,则将在应用程序启动时引发 ArgumentException。 如果层次结构中同一级别的两个或更多路由共享一个路由名称,也会引发此异常。

注册详细信息页路由

Shell 子类构造函数或者在调用路由前运行的任何其他位置中,可为未在 Shell 视觉层次结构中表示的任何详细信息页面显式地注册其他路由。 这是使用 Routing.RegisterRoute 方法完成的:

Routing.RegisterRoute("monkeydetails", typeof(MonkeyDetailPage));
Routing.RegisterRoute("beardetails", typeof(BearDetailPage));
Routing.RegisterRoute("catdetails", typeof(CatDetailPage));
Routing.RegisterRoute("dogdetails", typeof(DogDetailPage));
Routing.RegisterRoute("elephantdetails", typeof(ElephantDetailPage));

此示例将 Shell 子类中未定义的详细信息页注册为路由。 然后可以使用基于 URI 的导航从应用程序中的任何位置导航到这些详细信息页面。 这些页面的路由被称为“全局路由”

警告

如果 Routing.RegisterRoute 方法尝试将同一路由注册到两个或多个不同类型,将引发 ArgumentException

此外,如果需要,页面可在不同的路由层次结构上注册:

Routing.RegisterRoute("monkeys/details", typeof(MonkeyDetailPage));
Routing.RegisterRoute("bears/details", typeof(BearDetailPage));
Routing.RegisterRoute("cats/details", typeof(CatDetailPage));
Routing.RegisterRoute("dogs/details", typeof(DogDetailPage));
Routing.RegisterRoute("elephants/details", typeof(ElephantDetailPage));

此示例启用上下文页面导航,其中从 monkeys 路由的页面导航到 details 路由将显示 MonkeyDetailPage。 同样,从 elephants 路由的页面导航到 details 路由将显示 ElephantDetailPage。 有关详细信息,请参阅上下文导航

注意

如果需要,已经使用 Routing.RegisterRoute 方法注册其路由的页面可以通过 Routing.UnRegisterRoute 方法注销。

执行导航

要执行导航,必须首先获得对 Shell 子类的引用。 通过将 App.Current.MainPage 属性转换为 Shell 对象,或者通过 Shell.Current 属性,可以获得此引用。 然后,可以通过调用 Shell 对象上的 GoToAsync 方法来执行导航。 该方法导航到 ShellNavigationState 并返回 Task,后者将在导航动画完成后完成。 ShellNavigationState 对象是通过 GoToAsync 方法从 stringUri 构造的,并将其 Location 属性设置为 stringUri 参数。

重要说明

当导航到 Shell 视觉层次结构中的路由时,不会创建导航堆栈。 但是,当导航到不在 Shell 视觉层次结构中的页面时,将创建一个导航堆栈。

可以通过 Shell.Current.CurrentState 属性检索 Shell 对象的当前导航状态,该属性包括 Location 属性中显示的路由的 URI。

绝对路由

可以通过将一个有效的绝对 URI 指定为 GoToAsync 方法的参数来执行导航:

await Shell.Current.GoToAsync("//animals/monkeys");

本示例导航到 monkeys 路由的页面,该路由在 ShellContent 对象上定义。 表示 monkeys 路由的 ShellContent 对象是其路由为 animalsFlyoutItem 对象的子对象。

相对路由

还可以通过将一个有效的相对 URI 指定为 GoToAsync 方法的参数来执行导航。 路由系统将尝试将 URI 与 ShellContent 对象进行匹配。 因此,如果应用程序中的所有路由都是唯一的,那么仅可通过将唯一路由名称指定为相对 URI 来执行导航。

支持下列相对路由格式:

格式 描述
路由 将从当前位置向上搜索路由层次结构来获取指定的路由。 匹配的页面将被推送到导航堆栈。
/路由 将在指定路由中从当前位置向下搜索路由层次结构。 匹配的页面将被推送到导航堆栈。
//路由 将从当前位置向上搜索路由层次结构来获取指定的路由。 匹配的页面将替换导航堆栈。
///路由 将从当前位置向下搜索路由层次结构来获取指定的路由。 匹配的页面将替换导航堆栈。

以下示例导航到 monkeydetails 路由的页面:

await Shell.Current.GoToAsync("monkeydetails");

在本例中,在 monkeyDetails 路由中向上搜索层次结构,直到找到匹配的页面。 找到该页面后,会将它推送到导航堆栈。

上下文导航

相对路由支持上下文导航。 以下列路由层次结构为例:

monkeys
  details
bears
  details

当显示 monkeys 路由的注册页时,导航到 details 路由将显示 monkeys/details 路由的注册页。 同样,当显示 bears 路由的注册页时,导航到 details 路由将显示 bears/details 路由的注册页。 有关如何注册本示例中的路由的信息,请参阅注册页面路由

向后导航

向后导航可以通过将“..”指定为 GoToAsync 方法的参数来执行:

await Shell.Current.GoToAsync("..");

通过“..”执行的向后导航还可与路由结合使用:

await Shell.Current.GoToAsync("../route");

在本例中,会执行向后导航,然后导航到指定的路由。

重要说明

仅当向后导航将你置于路由层次结构中的当前位置以导航到指定路由时,才可在向后导航后导航到指定路由。

同样,可以向后导航多次,然后导航到指定路由:

await Shell.Current.GoToAsync("../../route");

在本例中,会执行向后导航两次,然后导航到指定的路由。

此外,在向后导航时,可通过查询属性传递数据:

await Shell.Current.GoToAsync($"..?parameterToPassBack={parameterValueToPassBack}");

在本例中,会执行向后导航,并将查询参数值传递到上一页上的查询参数。

注意

可将查询参数追加到任何向后导航请求。

若要详细了解如何在导航时传递数据,请参阅传递数据

无效路由

以下路由格式无效:

Format 说明
//page 或 ///page 全局路由当前不能是导航堆栈上的唯一页面。 因此,不支持绝对路由到全局路由。

使用这些路由格式会导致引发 Exception

警告

尝试导航到不存在的路由会导致引发 ArgumentException 异常。

调试导航

一些 Shell 类通过 DebuggerDisplayAttribute 修饰,它指定调试程序如何显示类或字段。 这可以通过显示与导航请求相关的数据来帮助调试导航请求。 例如,下面的屏幕截图显示了 Shell.Current 对象的 CurrentItemCurrentState 属性:

Screenshot of debugger

在本例中,类型为 FlyoutItemCurrentItem 属性显示了 FlyoutItem 对象的标题和路由。 同样,类型为 ShellNavigationStateCurrentState 属性显示了 Shell 应用程序中显示的路由的 URI。

Tab 类定义了一个类型为 IReadOnlyList<Page>Stack 属性,它表示 Tab 中的当前导航堆栈。 该类还提供了以下可重写的导航方法:

  • GetNavigationStack,返回 IReadOnlyList<Page>,表示当前导航堆栈。
  • OnInsertPageBefore,调用 INavigation.InsertPageBefore 时会对其进行调用。
  • OnPopAsync,返回 Task<Page>,调用 INavigation.PopAsync 时会对其进行调用。
  • OnPopToRootAsync,返回 Task,调用 INavigation.OnPopToRootAsync 时会对其进行调用。
  • OnPushAsync,返回 Task,调用 INavigation.PushAsync 时会对其进行调用。
  • OnRemovePage,调用 INavigation.RemovePage 时会对其进行调用。

下面的示例演示如何重写 OnRemovePage 方法:

public class MyTab : Tab
{
    protected override void OnRemovePage(Page page)
    {
        base.OnRemovePage(page);

        // Custom logic
    }
}

在此示例中,应在 Shell 视觉对象层次结构中使用 MyTab 对象,而不是使用 Tab 对象。

Shell 类定义 Navigating 事件,该事件在即将执行导航时触发,原因可能是编程导航或用户交互。 Navigating 事件随附的 ShellNavigatingEventArgs 对象提供以下属性:

属性 类型​​ 描述
Current ShellNavigationState 当前页的 URI。
Source ShellNavigationSource 发生的导航类型。
Target ShellNavigationState 表示导航目标位置的 URI。
CanCancel bool 指示是否可以取消导航的值。
Cancelled bool 指示是否已取消导航的值。

此外,ShellNavigatingEventArgs 类还提供 Cancel 方法和 GetDeferral 方法,前者可用于取消导航,后者返回可用于完成导航的 ShellNavigatingDeferral 令牌。 有关导航延迟的详细信息,请参阅导航延迟

Shell 类还定义 Navigated 事件,该事件在导航完成时触发。 Navigated 事件随附的 ShellNavigatedEventArgs 对象提供以下属性:

属性 类型​​ 描述
Current ShellNavigationState 当前页的 URI。
Previous ShellNavigationState 上一页的 URI。
Source ShellNavigationSource 发生的导航类型。

重要说明

触发 Navigating 事件时将调用 OnNavigating 方法。 同样,触发 Navigated 事件时将调用 OnNavigated 方法。 这两种方法都可以在 Shell 子类中被替代,以截获导航请求。

ShellNavigatedEventArgsShellNavigatingEventArgs 类均具有类型为 ShellNavigationSourceSource 属性。 此枚举提供下列值:

  • Unknown
  • Push
  • Pop
  • PopToRoot
  • Insert
  • Remove
  • ShellItemChanged
  • ShellSectionChanged
  • ShellContentChanged

因此,可以在 OnNavigating 替代中截获导航,并可根据导航源执行操作。 例如,下面的代码显示页面数据未保存时如何取消向后导航:

protected override void OnNavigating(ShellNavigatingEventArgs args)
{
    base.OnNavigating(args);

    // Cancel any back navigation.
    if (args.Source == ShellNavigationSource.Pop)
    {
        args.Cancel();
    }
// }

可以根据用户的选择截获并完成或取消 Shell 导航。 此操作可通过以下方式实现:重写 Shell 子类中的 OnNavigating 方法,并对 ShellNavigatingEventArgs 对象调用 GetDeferral 方法。 此方法返回 ShellNavigatingDeferral 令牌,该令牌有一个可用于完成导航请求的 Complete 方法:

public MyShell : Shell
{
    // ...
    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();
    }    
}

在此示例中,将显示一个操作工作表,邀请用户完成导航请求,或取消导航。 可通过对 ShellNavigatingEventArgs 对象调用 Cancel 方法来取消导航。 通过对在 ShellNavigatingEventArgs 对象上使用 GetDeferral 方法检索到的 ShellNavigatingDeferral 令牌调用 Complete 方法来完成导航。

警告

如果用户在存在挂起的导航延迟的情况下尝试导航,GoToAsync 方法将引发 InvalidOperationException

传递数据

执行基于 URI 的编程导航时,可以将数据作为查询参数传递。 这通过以下方式实现:在路由后追加 ?,后跟查询参数 ID = 和一个值。 例如,当用户在 ElephantsPage 上选择大象时,将在示例应用程序中执行以下代码:

async void OnCollectionViewSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    string elephantName = (e.CurrentSelection.FirstOrDefault() as Animal).Name;
    await Shell.Current.GoToAsync($"elephantdetails?name={elephantName}");
}

此代码示例在 CollectionView 中检索当前选中的大象,并导航到 elephantdetails 路由,将 elephantName 作为查询参数传递。

可以通过两种方法接收导航数据:

  1. 对于每个查询参数,可以使用 QueryPropertyAttribute 修饰表示要导航到的页面的类或页面的 BindingContext 的类。 有关更多信息,请参阅使用查询属性的特性处理导航数据
  2. 表示要导航到的页面的类,或页面的类 BindingContext 可以实现 IQueryAttributable 接口。 有关更多信息,请参阅使用单一方法处理导航数据

使用查询属性特性处理导航数据

可以通过使用 QueryPropertyAttribute 针对每个查询参数修饰接收类来接收导航数据:

[QueryProperty(nameof(Name), "name")]
public partial class ElephantDetailPage : ContentPage
{
    public string Name
    {
        set
        {
            LoadAnimal(value);
        }
    }
    ...

    void LoadAnimal(string name)
    {
        try
        {
            Animal animal = ElephantData.Elephants.FirstOrDefault(a => a.Name == name);
            BindingContext = animal;
        }
        catch (Exception)
        {
            Console.WriteLine("Failed to load animal.");
        }
    }    
}

QueryPropertyAttribute 的第一个参数指定将接收数据的属性的名称,第二个参数指定查询参数 ID。因此,上述示例中的 QueryPropertyAttribute 指定 Name 属性将接收从 GoToAsync 方法调用中的 URI 传入 name 查询参数的数据。 Name 属性资源库调用 LoadAnimal 方法来检索 nameAnimal 对象,并将其设置为页面的 BindingContext

注意

通过 QueryPropertyAttribute 接收的查询参数值将自动进行 URL 解码。

使用单一方法处理导航数据

可以通过在接收类上实现 IQueryAttributable 接口来接收导航数据。 IQueryAttributable 接口指定必须由实现类来实现 ApplyQueryAttributes 方法。 此方法具有一个 IDictionary<string, string> 类型的 query 变量,其中包含在导航过程中传递的任何数据。 字典中的每个键都是一个查询参数 id,其值为查询参数值。 使用此方法的优点是可以使用单一方法来处理导航数据,在有多个需要作为整体处理的导航数据项时,这会很有用。

以下示例显示了实现 IQueryAttributable 接口的视图模型类:

public class MonkeyDetailViewModel : IQueryAttributable, INotifyPropertyChanged
{
    public Animal Monkey { get; private set; }

    public void ApplyQueryAttributes(IDictionary<string, string> query)
    {
        // The query parameter requires URL decoding.
        string name = HttpUtility.UrlDecode(query["name"]);
        LoadAnimal(name);
    }

    void LoadAnimal(string name)
    {
        try
        {
            Monkey = MonkeyData.Monkeys.FirstOrDefault(a => a.Name == name);
            OnPropertyChanged("Monkey");
        }
        catch (Exception)
        {
            Console.WriteLine("Failed to load animal.");
        }
    }
    ...
}

在该示例中,ApplyQueryAttributes 方法从 GoToAsync 方法调用中的 URI 检索 name 查询参数的值。 然后,调用 LoadAnimal 方法检索 Animal 对象,此时它被设置为数据将绑定到的 Monkey 属性的值。

重要

通过 IQueryAttributable 接口接收的查询参数值不会自动进行 URL 解码。

传递和处理多个查询参数

可以通过将多个查询参数与 & 连接来传递它们。 例如,以下代码传递两个数据项:

async void OnCollectionViewSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    string elephantName = (e.CurrentSelection.FirstOrDefault() as Animal).Name;
    string elephantLocation = (e.CurrentSelection.FirstOrDefault() as Animal).Location;
    await Shell.Current.GoToAsync($"elephantdetails?name={elephantName}&location={elephantLocation}");
}

此代码示例在 CollectionView 中检索当前选中的大象,并导航到 elephantdetails 路由,将 elephantNameelephantLocation 作为查询参数传递。

要接收多个数据项,对于每个查询参数,可以使用 QueryPropertyAttribute 修饰表示导航到的页面的类或页面的 BindingContext 的类:

[QueryProperty(nameof(Name), "name")]
[QueryProperty(nameof(Location), "location")]
public partial class ElephantDetailPage : ContentPage
{
    public string Name
    {
        set
        {
            // Custom logic
        }
    }

    public string Location
    {
        set
        {
            // Custom logic
        }
    }
    ...    
}

在此示例中,每个查询参数的类使用 QueryPropertyAttribute 进行修饰。 第一个 QueryPropertyAttribute 指定 Name 属性将接收在 name 查询参数中传递的数据,而第二个 QueryPropertyAttribute 指定 Location 属性将接收在 location 查询参数中传递的数据。 在这两种情况下,查询参数值都是在 GoToAsync 方法调用的 URI 中指定的。

或者,在表示导航到的页面的类或页面的 BindingContext 的类上实现 IQueryAttributable 接口,通过单一方法来处理导航数据:

public class ElephantDetailViewModel : IQueryAttributable, INotifyPropertyChanged
{
    public Animal Elephant { get; private set; }

    public void ApplyQueryAttributes(IDictionary<string, string> query)
    {
        string name = HttpUtility.UrlDecode(query["name"]);
        string location = HttpUtility.UrlDecode(query["location"]);
        ...        
    }
    ...
}

在该示例中,ApplyQueryAttributes 方法从 GoToAsync 方法调用中的 URI 检索 namelocation 查询参数的值。

“后退”按钮行为

通过将 BackButtonBehavior 附加属性设置为 BackButtonBehavior 对象,可以重新定义“后退”按钮的外观和行为。 BackButtonBehavior 类定义以下属性:

  • Command 属于 ICommand 类型,在按下“后退”按钮时执行。
  • CommandParameter 属于 object 类型,是传递给 Command 的参数。
  • IconOverride 属于 ImageSource 类型,是用于“后退”按钮的图标。
  • IsEnabled 属于 boolean 类型,指示是否已启用“后退”按钮。 默认值为 true
  • TextOverride 属于 string 类型,是用于“后退”按钮的文本。

所有这些属性都由 BindableProperty 对象提供支持,这意味着这些属性可以作为数据绑定的目标。

下面的代码演示了重新定义“后退”按钮的外观和行为的示例:

<ContentPage ...>    
    <Shell.BackButtonBehavior>
        <BackButtonBehavior Command="{Binding BackCommand}"
                            IconOverride="back.png" />   
    </Shell.BackButtonBehavior>
    ...
</ContentPage>

等效 C# 代码如下:

Shell.SetBackButtonBehavior(this, new BackButtonBehavior
{
    Command = new Command(() =>
    {
        ...
    }),
    IconOverride = "back.png"
});

Command 属性设置为按下“后退”按钮时执行的 ICommand,将 IconOverride 属性设置为用于“后退”按钮的图标:

Screenshot of a Shell back button icon override, on iOS and Android