4.4 页面导航与对话框
学习目标
你将实现可以切换的“任务”和“设置”页面,并让 ViewModel 通过接口请求确认。前提:已有 WpfBasics WPF 项目,并已安装上一节的 CommunityToolkit.Mvvm 8.4.0。
导航与后端路由的区别
ASP.NET Core 路由根据 URL 选择端点;桌面导航经常只是让同一个窗口显示另一个对象。WPF 不会因为类名带有 Page 就自动建立地址、历史或依赖作用域。可以用 Frame 与 Page 的导航体系,也可以让 ContentControl 显示当前 ViewModel,再由 DataTemplate 选择画面。本节采用后者,适合小型业务客户端。
关键决策是页面实例是否保留。每次新建页面会丢弃未保存输入;复用页面则会保留输入和选择项,也可能保留订阅与后台任务。以下示例只创建两个页面实例,切换时复用;大型应用需要明确页面进入、离开、释放和取消的规则,而非仅改变一个属性。
完整的页面状态与确认服务
新增完整文件。Shell 负责“当前显示谁”,页面负责自身状态;只有界面服务引用 MessageBox。同步确认适用于这个简单模态对话框;自定义异步对话框可把接口改为返回 Task,并约定关闭、取消与宿主窗口行为。
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace WpfBasics;
public interface IConfirmationService
{
bool Confirm(string message);
}
public sealed class WpfConfirmationService : IConfirmationService
{
public bool Confirm(string message)
{
var owner = Application.Current.MainWindow;
var result = owner is null
? MessageBox.Show(message, "确认", MessageBoxButton.YesNo)
: MessageBox.Show(owner, message, "确认", MessageBoxButton.YesNo);
return result == MessageBoxResult.Yes;
}
}
public partial class TasksViewModel : ObservableObject
{
private readonly IConfirmationService _confirmation;
public TasksViewModel(IConfirmationService confirmation)
=> _confirmation = confirmation;
[ObservableProperty]
private string status = "任务页";
[RelayCommand]
private void Clear()
{
if (_confirmation.Confirm("确认清空演示状态?"))
Status = "已清空";
}
}
public partial class SettingsViewModel : ObservableObject
{
[ObservableProperty]
private string displayName = "后端开发者";
}
public partial class ShellViewModel : ObservableObject
{
private readonly TasksViewModel _tasks;
private readonly SettingsViewModel _settings = new();
public ShellViewModel(IConfirmationService confirmation)
{
_tasks = new TasksViewModel(confirmation);
currentViewModel = _tasks;
}
[ObservableProperty]
private ObservableObject currentViewModel;
[RelayCommand]
private void ShowTasks() => CurrentViewModel = _tasks;
[RelayCommand]
private void ShowSettings() => CurrentViewModel = _settings;
}
替换主窗口 XAML。没有 x:Key 的 DataTemplate 以 DataType 作为隐式键,根据内容对象的类型选取模板;内容区收到的是 ViewModel 实例,不是类型名称或字符串地址。
<Window x:Class="WpfBasics.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:WpfBasics"
Title="页面切换" Width="640" Height="400">
<Window.Resources>
<DataTemplate DataType="{x:Type vm:TasksViewModel}">
<StackPanel>
<TextBlock Text="{Binding Status}" FontSize="20" />
<Button Margin="0,12,0,0" Content="清空演示状态"
Command="{Binding ClearCommand}" />
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SettingsViewModel}">
<StackPanel>
<TextBlock Text="显示名称" />
<TextBox Text="{Binding DisplayName, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<DockPanel Margin="24">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,20">
<Button Content="任务" Command="{Binding ShowTasksCommand}" />
<Button Margin="8,0,0,0" Content="设置" Command="{Binding ShowSettingsCommand}" />
</StackPanel>
<ContentControl Content="{Binding CurrentViewModel}" />
</DockPanel>
</Window>
主窗口代码后置替换为完整文件,保留现有 App.xaml 的启动入口:
using System.Windows;
namespace WpfBasics;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ShellViewModel(new WpfConfirmationService());
}
}
常见错误
显式给上述模板设置 x:Key 后,却未设置 ContentTemplate,会失去按类型自动选取的效果。把 CurrentViewModel 换成字符串,界面通常只显示字符串。对话框服务必须在 UI 线程使用,设置 Owner 能让窗口层级与位置更合理;不能从后台线程调用它。复杂导航也要处理正在保存时离开页面、事件退订和多窗口的所有者选择。
练习与验收
在设置页修改名称,切到任务页再切回来。验收:输入仍保留;任务页确认选择“否”时状态不变,选择“是”时改变。用始终返回 false 的假 IConfirmationService 创建 TasksViewModel,直接执行 ClearCommand,确认无需弹窗就能验证取消路径。
官方资料
继续阅读:资源字典与样式。
完成基础与 TaskDesk 后,可在 Prism 进阶 学习由框架提供的 Region 导航、未保存确认与对话框服务,并对照本章的责任划分。