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

9.2 KiB
Raw Blame History

04 - 导航系统示例

演示如何使用 Sharp.Wpf 的导航服务实现多页面 WPF 应用,包括页面跳转、参数传递、前进/后退等功能。

🎯 学习目标

  • 使用 INavigationService 实现页面导航
  • 理解导航历史栈(前进/后退)
  • 页面间参数传递
  • 导航生命周期钩子(INavigationAware)
  • 弹出式导航并获取结果

📦 使用的模块

  • Sharp.Wpf - INavigationService, NavigationService

🚀 快速运行

cd examples/04-Navigation
dotnet run

📝 核心代码讲解

1. MainViewModel.cs - 主导航容器

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 - 带参数的页面

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 - 详情页(带结果返回)

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 - 注册导航服务

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 - 导航容器

<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 实现页面切换动画。

📚 延伸阅读


掌握导航系统,构建流畅的多页面应用! 🧭