7.8 KiB
7.8 KiB
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
- Shell: Container chính quản lý visual structure và navigation
- URI-based Navigation: Điều hướng thông qua URI routes
- Query Parameters: Truyền dữ liệu giữa pages
AppShell Structure
Basic Shell Configuration
<!-- AppShell.xaml -->
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:MyApp.Views"
x:Class="MyApp.AppShell"
FlyoutBehavior="Disabled">
<!-- Tab-based navigation -->
<TabBar>
<ShellContent Title="Home"
Icon="home.svg"
Route="home"
ContentTemplate="{DataTemplate views:HomePage}" />
<ShellContent Title="Products"
Icon="products.svg"
Route="products"
ContentTemplate="{DataTemplate views:ProductListPage}" />
<ShellContent Title="Settings"
Icon="settings.svg"
Route="settings"
ContentTemplate="{DataTemplate views:SettingsPage}" />
</TabBar>
</Shell>
Flyout Navigation
<Shell FlyoutBehavior="Flyout">
<FlyoutItem Title="Dashboard" Icon="dashboard.svg">
<ShellContent ContentTemplate="{DataTemplate views:DashboardPage}" />
</FlyoutItem>
<FlyoutItem Title="Products" Icon="products.svg">
<Tab Title="All Products">
<ShellContent ContentTemplate="{DataTemplate views:ProductListPage}" />
</Tab>
<Tab Title="Categories">
<ShellContent ContentTemplate="{DataTemplate views:CategoryListPage}" />
</Tab>
</FlyoutItem>
<!-- Separator -->
<MenuItem Text="Logout"
IconImageSource="logout.svg"
Command="{Binding LogoutCommand}" />
</Shell>
Route Registration
// 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
// 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
// ✅ RECOMMENDED: Object-based parameters (NativeAOT safe)
await Shell.Current.GoToAsync(
nameof(ProductDetailPage),
new Dictionary<string, object>
{
["Product"] = selectedProduct,
["IsEditing"] = true
});
// Simple string parameters
await Shell.Current.GoToAsync($"{nameof(ProductDetailPage)}?productId={product.Id}");
3. Receiving Parameters (IQueryAttributable)
// ✅ RECOMMENDED: IQueryAttributable (works with complex objects)
public partial class ProductDetailViewModel : ObservableObject, IQueryAttributable
{
[ObservableProperty]
private Product? _product;
[ObservableProperty]
private bool _isEditing;
public void ApplyQueryAttributes(IDictionary<string, object> 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;
}
}
}
// 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
// Interface
public interface INavigationService
{
Task GoToAsync(string route);
Task GoToAsync<TViewModel>(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<TViewModel>(object? parameters = null)
where TViewModel : class
{
var route = typeof(TViewModel).Name.Replace("ViewModel", "Page");
if (parameters != null)
{
var dict = new Dictionary<string, object>();
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<ProductDetailViewModel>(new { Product = product });
}
}
Modal Navigation
// 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
// ❌ 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
// ❌ 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
// ❌ 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<string, object> query)
{
if (query.TryGetValue("Product", out var product))
Product = product as Product;
}