Files
sharp-basic/examples/04-Navigation/README.md
T

316 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 04 - 导航系统示例
演示如何使用 Sharp.Wpf 的导航服务实现多页面 WPF 应用,包括页面跳转、参数传递、前进/后退等功能。
## 🎯 学习目标
- 使用 `INavigationService` 实现页面导航
- 理解导航历史栈(前进/后退)
- 页面间参数传递
- 导航生命周期钩子(INavigationAware)
- 弹出式导航并获取结果
## 📦 使用的模块
- **Sharp.Wpf** - INavigationService, NavigationService
## 🚀 快速运行
```bash
cd examples/04-Navigation
dotnet run
```
## 📝 核心代码讲解
### 1. MainViewModel.cs - 主导航容器
```csharp
using Wpf.Navigation;
public class MainViewModel : ViewModelBase
{
private readonly INavigationService _navigationService;
private ViewModelBase? _currentPage;
public ViewModelBase? CurrentPage
{
get => _currentPage;
set => SetProperty(ref _currentPage, value);
}
public bool CanGoBack => _navigationService.CanGoBack;
public bool CanGoForward => _navigationService.CanGoForward;
public ICommand NavigateToHomeCommand { get; }
public ICommand NavigateToProductsCommand { get; }
public ICommand NavigateToSettingsCommand { get; }
public ICommand GoBackCommand { get; }
public ICommand GoForwardCommand { get; }
public MainViewModel(INavigationService navigationService)
{
_navigationService = navigationService;
// 监听导航事件
_navigationService.Navigated += (s, vm) =>
{
CurrentPage = vm;
OnPropertyChanged(nameof(CanGoBack));
OnPropertyChanged(nameof(CanGoForward));
};
// 导航命令
NavigateToHomeCommand = new AsyncCommand(async (ct) =>
{
await _navigationService.NavigateAsync<HomeViewModel>();
});
NavigateToProductsCommand = new AsyncCommand(async (ct) =>
{
await _navigationService.NavigateAsync<ProductsViewModel>();
});
NavigateToSettingsCommand = new AsyncCommand(async (ct) =>
{
await _navigationService.NavigateAsync<SettingsViewModel>();
});
GoBackCommand = new AsyncCommand(async (ct) =>
{
await _navigationService.GoBackAsync();
}, () => CanGoBack);
GoForwardCommand = new AsyncCommand(async (ct) =>
{
await _navigationService.GoForwardAsync();
}, () => CanGoForward);
// 初始导航到首页
Task.Run(async () => await _navigationService.NavigateAsync<HomeViewModel>());
}
}
```
### 2. ProductsViewModel.cs - 带参数的页面
```csharp
public class ProductsViewModel : ViewModelBase, INavigationAware
{
private readonly INavigationService _navigationService;
private readonly IDialogService _dialogService;
private string _category = "全部";
public string Category
{
get => _category;
set => SetProperty(ref _category, value);
}
public ICommand ViewProductDetailsCommand { get; }
public ProductsViewModel(
INavigationService navigationService,
IDialogService dialogService)
{
_navigationService = navigationService;
_dialogService = dialogService;
ViewProductDetailsCommand = new AsyncCommand<int>(async (productId, ct) =>
{
// 导航到产品详情页,传递产品ID
await _navigationService.NavigateAsync<ProductDetailViewModel>(productId);
});
}
// 导航生命周期:进入页面时
public async Task OnNavigatedToAsync(object? parameter)
{
if (parameter is string category)
{
Category = category;
_dialogService.ShowMessage($"显示分类: {category}", "提示");
}
// 可以在这里加载数据
await LoadProductsAsync();
}
// 导航生命周期:离开页面时
public async Task OnNavigatedFromAsync()
{
// 清理资源、保存状态等
await Task.CompletedTask;
}
private async Task LoadProductsAsync()
{
IsBusy = true;
await Task.Delay(500); // 模拟加载
IsBusy = false;
}
}
```
### 3. ProductDetailViewModel.cs - 详情页(带结果返回)
```csharp
public class ProductDetailViewModel : ViewModelBase,
INavigationAware,
INavigationResult<bool> // 返回 bool(是否购买)
{
private readonly TaskCompletionSource<bool> _resultSource = new();
private int _productId;
private string _productName = "";
public int ProductId
{
get => _productId;
set => SetProperty(ref _productId, value);
}
public string ProductName
{
get => _productName;
set => SetProperty(ref _productName, value);
}
public ICommand BuyCommand { get; }
public ICommand CancelCommand { get; }
public ProductDetailViewModel()
{
BuyCommand = new RelayCommand(() =>
{
// 返回结果:已购买
_resultSource.SetResult(true);
});
CancelCommand = new RelayCommand(() =>
{
// 返回结果:取消
_resultSource.SetResult(false);
});
}
public async Task OnNavigatedToAsync(object? parameter)
{
if (parameter is int productId)
{
ProductId = productId;
// 加载产品详情
ProductName = $"产品 #{productId}";
}
await Task.CompletedTask;
}
public Task OnNavigatedFromAsync() => Task.CompletedTask;
// 实现 INavigationResult<bool>
public Task<bool> GetResultAsync()
{
return _resultSource.Task;
}
}
```
### 4. App.xaml.cs - 注册导航服务
```csharp
protected override void OnStartup(StartupEventArgs e)
{
var services = new ServiceCollection();
services.AddSharpWpf(); // 自动注册 INavigationService
// 注册所有页面 ViewModel
services.AddViewModel<MainViewModel>();
services.AddViewModel<HomeViewModel>();
services.AddViewModel<ProductsViewModel>();
services.AddViewModel<ProductDetailViewModel>();
services.AddViewModel<SettingsViewModel>();
var provider = services.BuildServiceProvider();
var mainWindow = provider.GetRequiredService<MainWindow>();
mainWindow.Show();
}
```
### 5. MainWindow.xaml - 导航容器
```xml
<Window x:Class="NavigationExample.MainWindow"
Title="导航系统示例" Height="600" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 导航栏 -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Background="#f0f0f0" Padding="10">
<Button Content="首页" Command="{Binding NavigateToHomeCommand}" Margin="0,0,10,0"/>
<Button Content="产品列表" Command="{Binding NavigateToProductsCommand}" Margin="0,0,10,0"/>
<Button Content="设置" Command="{Binding NavigateToSettingsCommand}" Margin="0,0,20,0"/>
<Button Content="← 后退" Command="{Binding GoBackCommand}" Margin="0,0,10,0"/>
<Button Content="前进 →" Command="{Binding GoForwardCommand}"/>
</StackPanel>
<!-- 内容区域 - 显示当前页面 -->
<ContentControl Grid.Row="1" Content="{Binding CurrentPage}">
<ContentControl.Resources>
<!-- 为每个 ViewModel 指定对应的 View -->
<DataTemplate DataType="{x:Type local:HomeViewModel}">
<local:HomeView/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ProductsViewModel}">
<local:ProductsView/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:SettingsViewModel}">
<local:SettingsView/>
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</Grid>
</Window>
```
## 🎓 练习建议
### 练习1:面包屑导航
添加面包屑显示当前导航路径(如:首页 > 产品列表 > 产品详情)。
### 练习2:导航守卫
实现 `INavigationGuard` 接口,在离开页面前检查是否有未保存的更改。
### 练习3:嵌套导航
在产品列表页内部实现子导航(如:全部/热销/新品标签页切换)。
### 练习4:导航缓存
实现页面缓存,避免重复创建 ViewModel(保持页面状态)。
## 💡 常见问题
**Q: 导航后如何保持页面状态?**
A: 不要后退时,CurrentViewModel 会保留在前进栈中,前进时会恢复状态。如果需要完全保持状态,可以使用 Singleton ViewModel。
**Q: 如何传递复杂对象作为参数?**
A: 使用 `NavigateAsync(object parameter)`,parameter 可以是任何对象。
**Q: 导航历史栈何时清空?**
A: 调用 `_navigationService.ClearHistory()` 手动清空。
**Q: 能否使用动画过渡?**
A: 可以,在 ContentControl 上添加 Storyboard 实现页面切换动画。
## 📚 延伸阅读
- [WPF 数据模板](https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/data/data-templating-overview)
- [MVVM 导航模式](https://prismlibrary.com/docs/navigation.html)
- 下一个示例:[05-Localization](../05-Localization/README.md)
---
**掌握导航系统,构建流畅的多页面应用!** 🧭