Files
pos-system/microservices/.agent/skills/maui-enterprise-architect/guidelines/shell-nav.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

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

  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

<!-- 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;
}

Resources