# 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)