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

309 lines
7.8 KiB
Markdown

# 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
<!-- 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
```xml
<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
```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<string, object>
{
["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<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;
}
}
}
```
```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<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
```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<string, object> 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)