Loading...
正在加载...
请稍候

第八章:Uno 导航系统:跨平台页面跳转

✨步子哥 (steper) 2026年02月17日 05:28
# 第八章:Uno 导航系统:跨平台页面跳转 > **本章导读**:想象你在逛一家大型博物馆。每个展厅是一件艺术品,而连接它们的走廊和指示牌就是导航系统。没有清晰的导航,你会迷失在迷宫般的建筑中;有了好的导航,你能轻松地在不同展区之间穿梭,随时知道自己在哪、能去哪、怎么回。在应用开发中,导航系统同样至关重要——它定义了用户如何在你的应用中探索,如何从一个任务流向另一个任务。本章将揭示 Uno Platform 导航系统的奥秘。 --- ## 🖼️ 8.1 导航的哲学:从 Web 到原生 在深入技术细节之前,让我们先理解不同平台的导航范式,这将帮助你更好地理解 Uno 的设计选择。 ### 🌐 8.1.1 三种导航范式 **Web 导航**基于超链接的概念。每个页面有一个 URL,点击链接就是加载一个新的 URL。浏览器维护历史记录,用户可以通过"前进"和"后退"按钮导航。这种模式简单直观,但 URL 的存在也带来了一些限制(如敏感信息不应出现在 URL 中)。 **iOS 导航**使用导航控制器(Navigation Controller)的概念。页面被推入(push)和弹出(pop)一个栈,类似于 iOS 的 `UINavigationController`。屏幕顶部有导航栏,显示当前标题和返回按钮。 **Android 导航**历史上使用 Activity 栈,现代 Android 开发则推荐使用 Navigation Component,它基于 Fragment 和 NavHost 的概念,提供了类型安全的导航和深度链接支持。 ### 🎯 8.1.2 Uno 的选择:Frame 模型 Uno Platform 统一了这些不同的范式,采用了一种类似 iOS 的**Frame 模型**。 `Frame` 是一个容器控件,它显示一个 `Page` 并维护一个导航历史栈。当你调用 `Navigate()` 时,新页面被推入栈顶;当你调用 `GoBack()` 时,当前页面被弹出,显示前一个页面。 ``` ┌─────────────────────────────────────┐ │ Frame │ │ ┌─────────────────────────────┐ │ │ │ Page (Current) │ │ │ │ │ │ │ │ [用户看到的界面] │ │ │ │ │ │ │ └─────────────────────────────┘ │ │ │ │ Back Stack: │ │ [MainPage] -> [ListPage] │ └─────────────────────────────────────┘ ``` > **第一性原理**:为什么选择栈结构? > > 栈(后进先出)完美地匹配了用户的导航心理模型:我一步步深入,然后一步步返回。当你在设置页面中进入网络设置,再进入 WiFi 设置,按返回键应该先回到网络设置,再回到设置页面,最后回到主页。这种线性的、可预测的返回路径让用户始终有"家"可归。 --- ## 🚀 8.2 基础导航操作 让我们从最基础的导航操作开始,逐步构建复杂的导航场景。 ### 📄 8.2.1 创建 Frame 和初始页面 在典型的 Uno 应用中,`Window` 的内容被设置为一个 `Frame`,然后导航到初始页面: ```csharp // App.xaml.cs protected override void OnLaunched(LaunchActivatedEventArgs args) { var window = new Window(); // 创建 Frame 作为导航容器 var frame = new Frame(); // 将 Frame 设置为窗口内容 window.Content = frame; // 导航到初始页面 frame.Navigate(typeof(MainPage)); window.Activate(); } ``` 在页面内部,你可以通过 `this.Frame` 属性访问所属的 Frame。 ### ➡️ 8.2.2 前进导航 `Navigate` 方法接受一个页面类型作为参数,创建该页面的新实例并显示: ```csharp // 基础导航 this.Frame.Navigate(typeof(DetailsPage)); // 带参数的导航 this.Frame.Navigate(typeof(DetailsPage), itemId); // 带动画过渡信息的导航(高级用法) this.Frame.Navigate(typeof(DetailsPage), null, new SlideNavigationTransitionInfo()); ``` > **技术细节**:Navigate 做了什么? > > 1. 使用反射创建目标 Page 类型的新实例 > 2. 将当前 Page 推入 Back Stack > 3. 将新 Page 设置为 Frame 的 Content > 4. 触发新 Page 的 `OnNavigatedTo` 生命周期方法 ### ⬅️ 8.2.3 后退导航 在执行后退之前,必须检查 `CanGoBack` 属性,否则在栈顶调用 `GoBack` 会导致异常: ```csharp private void OnBackClick(object sender, RoutedEventArgs e) { if (this.Frame.CanGoBack) { this.Frame.GoBack(); } } ``` 也可以清除整个导航历史: ```csharp // 清除所有历史,用户无法返回 this.Frame.BackStack.Clear(); // 或者导航到主页并清除历史 this.Frame.Navigate(typeof(MainPage)); this.Frame.BackStack.Clear(); ``` ### 📱 8.2.4 处理系统返回键 Uno Platform 自动处理了不同平台的返回机制: **Android**:物理返回键自动触发 `GoBack()`。 **iOS**:屏幕边缘滑动手势触发返回。 **WebAssembly**:浏览器的前进/后退按钮与 Frame 导航同步。 在 Windows 上,你可以通过 `SystemNavigationManager` 自定义返回按钮的行为: ```csharp // 在页面构造函数中 var navManager = SystemNavigationManager.GetForCurrentView(); navManager.BackRequested += OnBackRequested; private void OnBackRequested(object sender, BackRequestedEventArgs e) { if (this.Frame.CanGoBack) { this.Frame.GoBack(); e.Handled = true; // 标记为已处理 } } ``` --- ## 📦 8.3 页面间数据传递 导航很少是简单的页面切换——通常需要将数据从源页面传递到目标页面。 ### 🎁 8.3.1 使用导航参数 `Navigate` 方法的第二个参数是导航参数,可以是任何对象: ```csharp // 传递整数 ID this.Frame.Navigate(typeof(UserProfilePage), userId); // 传递复杂对象(不推荐,详见下文) this.Frame.Navigate(typeof(EditPage), selectedItem); // 传递字典 var parameters = new Dictionary<string, object> { ["UserId"] = userId, ["Source"] = "notification" }; this.Frame.Navigate(typeof(UserProfilePage), parameters); ``` 在目标页面中,重写 `OnNavigatedTo` 方法接收参数: ```csharp public sealed partial class UserProfilePage : Page { private int _userId; protected override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); // 简单类型 if (e.Parameter is int userId) { _userId = userId; LoadUserData(userId); } // 字典类型 if (e.Parameter is Dictionary<string, object> parameters) { if (parameters.TryGetValue("UserId", out var id)) { _userId = (int)id; } } // 导航模式:New(新导航)、Back(返回)、Forward(前进) if (e.NavigationMode == NavigationMode.New) { // 首次进入页面的初始化 } } } ``` ### ⚠️ 8.3.2 参数传递的最佳实践 **不要传递复杂对象**:导航参数在页面挂起(Suspend)和恢复(Resume)时会被序列化。复杂对象可能无法正确序列化,或者占用大量内存。 **推荐做法**:传递 ID 或关键字,在目标页面中重新加载数据。 ```csharp // 不推荐 this.Frame.Navigate(typeof(EditPage), userObject); // 推荐 this.Frame.Navigate(typeof(EditPage), userObject.Id); // 然后在 EditPage 中 protected override void OnNavigatedTo(NavigationEventArgs e) { if (e.Parameter is int userId) { // 从数据库或服务重新加载 _user = await _userService.GetUserAsync(userId); } } ``` > **费曼技巧提问**:为什么不能直接传递对象? > > 想象你传递了一个包含 1MB 图片数据的用户对象。当用户导航到下一页、再下一页时,每个页面的 Back Stack 都保留了这个对象的引用。如果用户浏览了 10 个页面,就有 10MB 的数据被锁定在内存中。更糟糕的是,当应用被系统挂起时,这些数据需要被序列化到磁盘,导致启动恢复变慢。传递 ID 并重新加载虽然多了一次数据库查询,但内存占用更小,应用更稳定。 --- ## 🏗️ 8.4 现代导航:Uno.Extensions.Navigation 随着应用规模增长,直接使用 `Frame.Navigate` 会导致代码高度耦合——ViewModel 需要知道具体页面类型,这违反了 MVVM 的关注点分离原则。 **Uno.Extensions.Navigation** 提供了更现代、更解耦的导航方式。 ### 🔧 8.4.1 配置导航扩展 首先,在应用启动时配置导航: ```csharp // App.xaml.cs 或 Program.cs protected override void OnLaunched(LaunchActivatedEventArgs args) { var window = new Window(); var host = Host.CreateBuilder() .ConfigureServices(services => { // 注册导航 services.AddNavigation(); // 注册页面和 ViewModel services.AddTransient<MainPage, MainViewModel>(); services.AddTransient<DetailsPage, DetailsViewModel>(); }) .Build(); // 初始化导航 var navigator = host.Services.GetService<INavigator>(); window.Content = new Frame(); window.Activate(); } ``` ### 🎯 8.4.2 在 ViewModel 中导航 使用 `INavigator` 接口,ViewModel 完全不需要知道页面类型: ```csharp public partial class MainViewModel { private readonly INavigator _navigator; private readonly IUserService _userService; public MainViewModel(INavigator navigator, IUserService userService) { _navigator = navigator; _userService = userService; } // 简单导航 [RelayCommand] private async Task GoToSettings() { await _navigator.NavigateViewAsync<SettingsPage>(this); } // 带参数导航 [RelayCommand] private async Task ViewUser(int userId) { await _navigator.NavigateViewAsync<UserProfilePage>(this, data: userId); } // 导航并等待结果返回 [RelayCommand] private async Task SelectItem() { var result = await _navigator.NavigateViewForResultAsync<ItemPickerPage, Item>(this); if (result.IsSome) { SelectedItem = result.Some(); } } } ``` ### 📋 8.4.3 声明式路由配置 你可以使用路由映射声明性地定义导航结构: ```csharp public class AppRoutes : RouteMap { public AppRoutes() { new RouteMap { Routes = new[] { new RouteMap { Path = "home", View = typeof(HomePage) }, new RouteMap { Path = "users/{userId}", View = typeof(UserProfilePage), Init = (parameters) => new { UserId = parameters["userId"] } }, new RouteMap { Path = "settings", View = typeof(SettingsPage) } } }; } } ``` 然后使用路径导航: ```csharp await _navigator.NavigateAsync("users/123"); ``` > **设计优势**:这种声明式路由配置使得导航逻辑集中管理,便于维护和理解整个应用的导航结构。它还支持深度链接——外部 URL 可以直接映射到应用内页面。 --- ## 📱 8.5 多区域导航 复杂应用通常有多个导航区域——比如侧边栏和主内容区各自独立的导航。Uno 支持这种场景。 ### 🏠 8.5.1 理解导航区域 ``` ┌──────────────────────────────────────────────────┐ │ Main Frame │ ├────────────┬─────────────────────────────────────┤ │ │ │ │ Sidebar │ Content Frame │ │ Frame │ │ │ │ │ │ - Home │ [MainPage] │ │ - Search │ │ │ - Profile │ │ │ │ │ └────────────┴─────────────────────────────────────┘ ``` ### 🔧 8.5.2 实现多 Frame 导航 ```xml <Page> <Grid ColumnDefinitions="200,*"> <!-- 侧边栏区域 --> <Frame x:Name="SidebarFrame" Grid.Column="0"> <Frame.Content> <local:SidebarPage /> </Frame.Content> </Frame> <!-- 主内容区域 --> <Frame x:Name="ContentFrame" Grid.Column="1" /> </Grid> </Page> ``` ```csharp public sealed partial class ShellPage : Page { public Frame ContentFrame => ContentFrame; public ShellPage() { InitializeComponent(); ContentFrame.Navigate(typeof(HomePage)); } public void NavigateToPage(Type pageType) { ContentFrame.Navigate(pageType); } } ``` --- ## 🔗 8.6 深度链接 深度链接允许用户通过外部链接直接进入应用的特定页面。这在营销、推送通知、跨应用跳转等场景中非常有用。 ### 📲 8.6.1 协议激活 在 `Package.appxmanifest` 中注册自定义协议: ```xml <Package> <Applications> <Application> <Extensions> <uap:Extension Category="windows.protocol"> <uap:Protocol Name="myapp"> <uap:DisplayName>My App</uap:DisplayName> </uap:Protocol> </uap:Extension> </Extensions> </Application> </Applications> </Package> ``` 然后处理协议激活: ```csharp // App.xaml.cs protected override void OnActivated(ActivationActivatedEventArgs args) { if (args.Kind == ActivationKind.Protocol) { var protocolArgs = (ProtocolActivatedEventArgs)args; var uri = protocolArgs.Uri; // 解析 URI 并导航到对应页面 // 例如 myapp://user/123 if (uri.Host == "user") { var userId = uri.PathAndQuery.Trim('/'); var frame = new Frame(); frame.Navigate(typeof(UserProfilePage), int.Parse(userId)); Window.Current.Content = frame; } } } ``` ### 🌐 8.6.2 WebAssembly 路由 在 WebAssembly 中,URL 路由自动与 Frame 导航同步。用户可以直接通过 URL 访问特定页面,刷新页面也能保持当前位置。 ```csharp // 配置 URL 路由 services.AddNavigation() .ConfigureRoutes(routes => { routes.MapRoute("home", typeof(HomePage)); routes.MapRoute("user/{id}", typeof(UserProfilePage)); routes.MapRoute("settings", typeof(SettingsPage)); }); ``` --- ## 📊 8.7 导航生命周期 理解导航生命周期对于正确管理资源和状态至关重要。 ### 🔄 8.7.1 页面导航事件 每个 Page 都有以下导航相关的生命周期方法: ```csharp public sealed partial class DetailsPage : Page { // 导航到此页面时调用 protected override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); // e.NavigationMode 告诉你是新导航还是返回 // - NavigationMode.New: 首次进入 // - NavigationMode.Back: 从下一个页面返回 // - NavigationMode.Forward: 从上一个页面前进 // - NavigationMode.Refresh: 刷新 if (e.Parameter is int id) { LoadData(id); } } // 从此页面离开时调用 protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) { base.OnNavigatingFrom(e); // e.SourcePageType 是目标页面 // e.NavigationMode 是导航方向 // 可以取消导航 if (HasUnsavedChanges && !e.IsCancelable) { e.Cancel = true; ShowSavePrompt(); } } // 已经离开此页面时调用 protected override void OnNavigatedFrom(NavigationEventArgs e) { base.OnNavigatedFrom(e); // 清理资源、取消订阅等 CleanupResources(); } } ``` ### 💾 8.7.2 状态保存与恢复 当应用被系统挂起(如用户切换到其他应用)时,需要保存导航状态: ```csharp // 在 App.xaml.cs 中 private void OnSuspending(object sender, SuspendingEventArgs e) { var deferral = e.SuspendingOperation.GetDeferral(); // 保存 Frame 的导航状态 var frameState = new Dictionary<string, object>(); frameState["BackStack"] = frame.BackStack.ToList(); frameState["CurrentPage"] = frame.Content.GetType().FullName; // 保存到应用设置 ApplicationData.Current.LocalSettings.Values["NavigationState"] = JsonConvert.SerializeObject(frameState); deferral.Complete(); } private void OnResuming(object sender, object e) { // 恢复导航状态(如果需要) // 通常 Frame 会自动恢复 } ``` --- ## 📝 本章小结 导航系统是应用的骨架,它决定了用户如何在功能模块之间穿梭。本章我们从第一性原理出发,理解了 Uno Platform 采用的 Frame 模型——基于栈的导航结构简单、可预测,完美匹配用户的心理模型。 我们学习了基础导航操作:前进、后退、参数传递。我们也探讨了页面间数据传递的最佳实践——传递 ID 而非复杂对象,在目标页面重新加载数据。 对于更复杂的场景,Uno.Extensions.Navigation 提供了现代的、MVVM 友好的导航方式。通过 `INavigator` 接口,ViewModel 完全不需要知道具体的页面类型,实现了真正的关注点分离。 深度链接让应用能够响应外部请求,直接跳转到特定页面。这在推送通知、跨应用跳转等场景中非常重要。 在下一章中,我们将从导航转向数据存储——学习 **Uno 本地存储与文件访问**,掌握如何在跨平台环境中持久化应用数据。 --- > **动手实验**: > 1. 创建一个简单的两页应用:主页有一个列表,点击列表项导航到详情页,详情页显示选中项的信息,有返回按钮回到主页。 > 2. 实现一个导航参数传递场景:在列表页选择一个用户,导航到编辑页,编辑完成后返回列表页并刷新数据。 > 3. 尝试使用 Uno.Extensions.Navigation 重写实验 1,体验声明式导航的优势。 > 4. (进阶)实现一个三栏布局的 Shell:左侧导航栏、中间内容区、右侧详情面板。每个区域有独立的 Frame 和导航历史。

讨论回复

0 条回复

还没有人回复,快来发表你的看法吧!