# Shell Navigation / Điều Hướng App Shell Hướng dẫn chi tiết về App Shell và URI-based navigation trong .NET MAUI. ## Core Concepts / Khái Niệm Cốt Lõi 1. **Shell**: Container chính quản lý visual structure và navigation 2. **URI-based Navigation**: Điều hướng thông qua URI routes 3. **Query Parameters**: Truyền dữ liệu giữa pages --- ## AppShell Structure ### Basic Shell Configuration ```xml ``` ### Flyout Navigation ```xml ``` --- ## Route Registration ```csharp // AppShell.xaml.cs public partial class AppShell : Shell { public AppShell() { InitializeComponent(); // Register detail pages (không có trong Shell visual hierarchy) Routing.RegisterRoute(nameof(ProductDetailPage), typeof(ProductDetailPage)); Routing.RegisterRoute(nameof(EditProductPage), typeof(EditProductPage)); Routing.RegisterRoute(nameof(CheckoutPage), typeof(CheckoutPage)); // Nested routes (page within page) Routing.RegisterRoute("products/detail", typeof(ProductDetailPage)); Routing.RegisterRoute("products/detail/edit", typeof(EditProductPage)); } } ``` --- ## Navigation Patterns ### 1. Basic Navigation ```csharp // Absolute navigation (từ root) await Shell.Current.GoToAsync("//products"); // Relative navigation (push onto stack) await Shell.Current.GoToAsync(nameof(ProductDetailPage)); // Navigate back await Shell.Current.GoToAsync(".."); // Navigate back to root await Shell.Current.GoToAsync("//"); ``` ### 2. Navigation with Parameters ```csharp // ✅ RECOMMENDED: Object-based parameters (NativeAOT safe) await Shell.Current.GoToAsync( nameof(ProductDetailPage), new Dictionary { ["Product"] = selectedProduct, ["IsEditing"] = true }); // Simple string parameters await Shell.Current.GoToAsync($"{nameof(ProductDetailPage)}?productId={product.Id}"); ``` ### 3. Receiving Parameters (IQueryAttributable) ```csharp // ✅ RECOMMENDED: IQueryAttributable (works with complex objects) public partial class ProductDetailViewModel : ObservableObject, IQueryAttributable { [ObservableProperty] private Product? _product; [ObservableProperty] private bool _isEditing; public void ApplyQueryAttributes(IDictionary query) { if (query.TryGetValue("Product", out var product)) { Product = product as Product; } if (query.TryGetValue("IsEditing", out var isEditing)) { IsEditing = isEditing is bool editing && editing; } } } ``` ```csharp // Alternative: QueryProperty attribute (simple types only) [QueryProperty(nameof(ProductId), "productId")] public partial class ProductDetailViewModel : ObservableObject { [ObservableProperty] private string? _productId; partial void OnProductIdChanged(string? value) { if (!string.IsNullOrEmpty(value)) { LoadProductAsync(value); } } } ``` --- ## Navigation Service Pattern ```csharp // Interface public interface INavigationService { Task GoToAsync(string route); Task GoToAsync(object? parameters = null) where TViewModel : class; Task GoBackAsync(); Task GoToRootAsync(); } // Implementation public class NavigationService : INavigationService { public async Task GoToAsync(string route) { await Shell.Current.GoToAsync(route); } public async Task GoToAsync(object? parameters = null) where TViewModel : class { var route = typeof(TViewModel).Name.Replace("ViewModel", "Page"); if (parameters != null) { var dict = new Dictionary(); foreach (var prop in parameters.GetType().GetProperties()) { dict[prop.Name] = prop.GetValue(parameters)!; } await Shell.Current.GoToAsync(route, dict); } else { await Shell.Current.GoToAsync(route); } } public async Task GoBackAsync() { await Shell.Current.GoToAsync(".."); } public async Task GoToRootAsync() { await Shell.Current.GoToAsync("//"); } } // Usage in ViewModel public partial class ProductListViewModel : ObservableObject { private readonly INavigationService _navigation; public ProductListViewModel(INavigationService navigation) { _navigation = navigation; } [RelayCommand] private async Task ViewProductAsync(Product product) { await _navigation.GoToAsync(new { Product = product }); } } ``` --- ## Modal Navigation ```csharp // Push modal page await Shell.Current.GoToAsync(nameof(EditProductPage), animate: true); // Or using Navigation property await Shell.Current.Navigation.PushModalAsync(new EditProductPage(viewModel)); // Close modal await Shell.Current.Navigation.PopModalAsync(); ``` --- ## Common Mistakes / Lỗi Thường Gặp ### 1. Route Not Registered ```csharp // ❌ SAI: Quên đăng ký route await Shell.Current.GoToAsync(nameof(ProductDetailPage)); // Exception! // ✅ ĐÚNG: Đăng ký trong AppShell constructor Routing.RegisterRoute(nameof(ProductDetailPage), typeof(ProductDetailPage)); ``` ### 2. Wrong Route Syntax ```csharp // ❌ SAI: Double slash cho relative await Shell.Current.GoToAsync("//ProductDetailPage"); // ✅ ĐÚNG // Relative (push): không có // await Shell.Current.GoToAsync(nameof(ProductDetailPage)); // Absolute (to tab): có // await Shell.Current.GoToAsync("//products"); ``` ### 3. Complex Objects with QueryProperty ```csharp // ❌ SAI: QueryProperty không hỗ trợ complex objects [QueryProperty(nameof(Product), "product")] // Won't work! public Product Product { get; set; } // ✅ ĐÚNG: Dùng IQueryAttributable public void ApplyQueryAttributes(IDictionary query) { if (query.TryGetValue("Product", out var product)) Product = product as Product; } ``` --- ## Resources - [.NET MAUI Shell Navigation](https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/shell/navigation) - [MVVM Rules](./mvvm-rules.md) - [DI Setup](./di-setup.md)