309 lines
7.8 KiB
Markdown
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)
|