WPF Region 导航
大约 10 分钟约 3094 字
WPF Region 导航
简介
Region 导航模式是 WPF 模块化应用的核心架构模式。它将主窗口(或任何容器控件)划分为多个命名区域(Region),每个区域可以动态加载、切换和卸载不同的 View。这种模式在 Prism 框架中被广泛使用,也是企业级 WPF 应用的标准导航方案。
Region 导航解决的核心问题是:如何在多页面应用中实现模块化的页面管理和灵活的导航。传统的多 Window 方案存在窗口管理复杂、资源消耗大等问题;而 Region 导航通过在单个窗口内动态切换 View,提供了更轻量、更灵活的方案。
Region 导航的核心概念
- Region(区域):UI 中的一个命名占位区域,通常由 ContentControl、ItemsControl 或 Panel 承载
- View(视图):加载到 Region 中的 UserControl 或 Page
- Navigation(导航):从一个 View 切换到另一个 View 的过程
- INavigationAware:感知导航生命周期的接口,View/ViewModel 在进入和离开页面时收到通知
- Navigation Parameters:导航时传递的参数
- Navigation Journal:导航历史记录,支持前进/后退
适用场景
- 工控软件的多模块界面(设备监控、报警记录、配方管理、系统设置)
- 企业管理系统的多页面布局(仪表盘、数据管理、报表中心)
- 需要动态加载插件式模块的应用
- 多 Tab 页面的动态管理
基础实现:简易 Region 导航框架
RegionManager 核心
/// <summary>
/// Region 管理器 — 管理区域注册、视图注册和导航
/// </summary>
public class RegionManager
{
private readonly Dictionary<string, ContentControl> _regions = new();
private readonly Dictionary<string, Func<UserControl>> _viewFactories = new();
private readonly Dictionary<string, object?> _currentParameters = new();
private readonly Dictionary<string, WeakReference> _currentViews = new();
/// <summary>
/// 注册 Region(将 ContentControl 关联到 Region 名称)
/// </summary>
public void RegisterRegion(string name, ContentControl control)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name));
if (control == null)
throw new ArgumentNullException(nameof(control));
_regions[name] = control;
Debug.WriteLine($"[Region] 注册区域: {name}");
}
/// <summary>
/// 注册 View(将 View 名称关联到创建工厂)
/// </summary>
public void RegisterView(string regionName, string viewName, Func<UserControl> factory)
{
if (string.IsNullOrEmpty(regionName))
throw new ArgumentNullException(nameof(regionName));
if (string.IsNullOrEmpty(viewName))
throw new ArgumentNullException(nameof(viewName));
if (factory == null)
throw new ArgumentNullException(nameof(factory));
var key = $"{regionName}:{viewName}";
_viewFactories[key] = factory;
Debug.WriteLine($"[Region] 注册视图: {regionName}/{viewName}");
}
/// <summary>
/// 导航到指定 View
/// </summary>
public bool NavigateTo(string regionName, string viewName, object? parameter = null)
{
var key = $"{regionName}:{viewName}";
if (!_viewFactories.TryGetValue(key, out var factory))
{
Debug.WriteLine($"[Region] 错误: View '{viewName}' 未注册到 Region '{regionName}'");
return false;
}
if (!_regions.TryGetValue(regionName, out var control))
{
Debug.WriteLine($"[Region] 错误: Region '{regionName}' 未注册");
return false;
}
// 通知旧 View 离开
if (control.Content is FrameworkElement oldView)
{
if (oldView.DataContext is INavigationAware oldAware)
oldAware.OnNavigatedFrom();
}
// 创建新 View
var newView = factory();
control.Content = newView;
_currentParameters[regionName] = parameter;
_currentViews[regionName] = new WeakReference(newView);
// 通知新 View 进入
if (newView.DataContext is INavigationAware newAware)
newAware.OnNavigatedTo(parameter);
Debug.WriteLine($"[Region] 导航: {regionName} → {viewName}");
return true;
}
/// <summary>
/// 获取当前 Region 中的 View
/// </summary>
public FrameworkElement? GetCurrentView(string regionName)
{
if (_currentViews.TryGetValue(regionName, out var weakRef) && weakRef.IsAlive)
return weakRef.Target as FrameworkElement;
return null;
}
}INavigationAware 接口
/// <summary>
/// 导航感知接口 — ViewModel 实现此接口以感知页面切换
/// </summary>
public interface INavigationAware
{
/// <summary>
/// 导航到当前页面时调用(适合加载数据)
/// </summary>
void OnNavigatedTo(object? parameter);
/// <summary>
/// 从当前页面导航离开时调用(适合保存状态、暂停刷新)
/// </summary>
void OnNavigatedFrom();
}带确认的导航接口
/// <summary>
/// 导航确认接口 — 支持离开前的确认逻辑(如未保存数据提醒)
/// </summary>
public interface INavigationConfirm
{
/// <summary>
/// 询问是否允许离开当前页面
/// </summary>
bool CanNavigateAway();
/// <summary>
/// 离开确认回调(异步,显示对话框后回调)
/// </summary>
void ConfirmNavigation(Action<bool> callback);
}XAML Region 注册与导航
<!-- MainWindow.xaml -->
<Window x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 左侧导航菜单 -->
<Border Grid.Column="0" Background="#F5F5F5" BorderThickness="0,0,1,0"
BorderBrush="#E0E0E0">
<ListBox x:Name="navMenu" BorderThickness="0"
SelectionMode="Single"
SelectedIndex="0"
SelectionChanged="NavMenu_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="8,12">
<TextBlock Text="{Binding Icon}" Margin="0,0,8,0" FontSize="16" />
<TextBlock Text="{Binding Title}" FontSize="14" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<GridSplitter Grid.Column="1" Width="2" Background="#E0E0E0" />
<!-- 右侧内容区域(Region) -->
<ContentControl Grid.Column="2" x:Name="contentRegion" />
</Grid>
</Window>MainWindow 代码
public partial class MainWindow : Window
{
private readonly RegionManager _regionManager;
private readonly IServiceProvider _serviceProvider;
// 导航菜单项
private readonly List<NavItem> _navItems = new()
{
new NavItem { Icon = "📊", Title = "设备监控", ViewName = "DeviceMonitor" },
new NavItem { Icon = "🔔", Title = "报警记录", ViewName = "AlarmHistory" },
new NavItem { Icon = "📋", Title = "配方管理", ViewName = "RecipeManager" },
new NavItem { Icon = "⚙️", Title = "系统设置", ViewName = "Settings" },
};
public MainWindow(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
InitializeComponent();
_regionManager = new RegionManager();
// 注册 Region
_regionManager.RegisterRegion("ContentRegion", contentRegion);
// 注册 View(通过 DI 容器创建 ViewModel)
_regionManager.RegisterView("ContentRegion", "DeviceMonitor",
() => new DeviceMonitorView
{
DataContext = _serviceProvider.GetRequiredService<DeviceMonitorViewModel>()
});
_regionManager.RegisterView("ContentRegion", "AlarmHistory",
() => new AlarmHistoryView
{
DataContext = _serviceProvider.GetRequiredService<AlarmHistoryViewModel>()
});
_regionManager.RegisterView("ContentRegion", "RecipeManager",
() => new RecipeManagerView
{
DataContext = _serviceProvider.GetRequiredService<RecipeManagerViewModel>()
});
_regionManager.RegisterView("ContentRegion", "Settings",
() => new SettingsView
{
DataContext = _serviceProvider.GetRequiredService<SettingsViewModel>()
});
// 绑定导航菜单
navMenu.ItemsSource = _navItems;
// 默认导航到第一个页面
Loaded += (s, e) => NavigateTo("DeviceMonitor");
}
private void NavMenu_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (navMenu.SelectedItem is NavItem item)
{
NavigateTo(item.ViewName);
}
}
public void NavigateTo(string viewName)
{
_regionManager.NavigateTo("ContentRegion", viewName);
}
}
public record NavItem(string Icon, string Title, string ViewName);INavigationAware 的 ViewModel 实现
public class DeviceMonitorViewModel : ObservableObject, INavigationAware
{
private readonly IDeviceService _deviceService;
private ObservableCollection<Device> _devices = new();
private bool _isRefreshing;
private Timer? _refreshTimer;
public ObservableCollection<Device> Devices
{
get => _devices;
set => SetProperty(ref _devices, value);
}
public bool IsRefreshing
{
get => _isRefreshing;
set => SetProperty(ref _isRefreshing, value);
}
public DeviceMonitorViewModel(IDeviceService deviceService)
{
_deviceService = deviceService;
}
/// <summary>
/// 导航到当前页面时调用 — 加载数据并启动自动刷新
/// </summary>
public async void OnNavigatedTo(object? parameter)
{
await LoadDevicesAsync();
// 启动定时刷新(每 5 秒)
_refreshTimer = new Timer(async _ =>
{
await Application.Current.Dispatcher.InvokeAsync(async () =>
{
await RefreshDevicesAsync();
});
}, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
}
/// <summary>
/// 离开当前页面时调用 — 停止自动刷新,释放资源
/// </summary>
public void OnNavigatedFrom()
{
_refreshTimer?.Dispose();
_refreshTimer = null;
Debug.WriteLine("[DeviceMonitor] 离开页面,停止自动刷新");
}
private async Task LoadDevicesAsync()
{
IsRefreshing = true;
try
{
var devices = await _deviceService.GetAllAsync();
Devices.Clear();
foreach (var d in devices)
Devices.Add(d);
}
finally
{
IsRefreshing = false;
}
}
private async Task RefreshDevicesAsync()
{
try
{
var updated = await _deviceService.GetUpdatedAsync();
foreach (var device in updated)
{
var existing = Devices.FirstOrDefault(d => d.Id == device.Id);
if (existing != null)
{
// 更新现有设备的属性
existing.Name = device.Name;
existing.Status = device.Status;
existing.Temperature = device.Temperature;
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"[DeviceMonitor] 刷新失败: {ex.Message}");
}
}
}导航参数传递
基础参数传递
/// <summary>
/// 导航参数封装类
/// </summary>
public class NavigationParameters : Dictionary<string, object>
{
public NavigationParameters() { }
public NavigationParameters(string key, object value)
{
this[key] = value;
}
public T? Get<T>(string key)
{
if (TryGetValue(key, out var value) && value is T typed)
return typed;
return default;
}
public T Get<T>(string key, T defaultValue)
{
return Get<T>(key) ?? defaultValue;
}
}使用强类型参数
// 导航时传递参数
_regionManager.NavigateTo("ContentRegion", "DeviceDetail",
new NavigationParameters
{
{ "DeviceId", "PLC-001" },
{ "Mode", "Edit" }
});
// ViewModel 中接收参数
public class DeviceDetailViewModel : ObservableObject, INavigationAware
{
private string? _deviceId;
private string _mode = "View";
private Device? _device;
public async void OnNavigatedTo(object? parameter)
{
if (parameter is NavigationParameters params_)
{
_deviceId = params_.Get<string>("DeviceId");
_mode = params_.Get("Mode", "View");
}
if (_deviceId != null)
{
_device = await _deviceService.GetByIdAsync(_deviceId);
// 填充 UI
}
}
public void OnNavigatedFrom() { /* 保存状态 */ }
}导航历史记录(Journal)
导航历史栈实现
/// <summary>
/// 导航历史管理器 — 支持前进/后退
/// </summary>
public class NavigationJournal
{
private readonly Stack<JournalEntry> _backStack = new();
private readonly Stack<JournalEntry> _forwardStack = new();
public bool CanGoBack => _backStack.Count > 0;
public bool CanGoForward => _forwardStack.Count > 0;
/// <summary>
/// 记录导航历史
/// </summary>
public void RecordNavigation(string regionName, string viewName, object? parameter)
{
_forwardStack.Clear(); // 新导航清除前进历史
_backStack.Push(new JournalEntry(regionName, viewName, parameter));
}
/// <summary>
/// 后退
/// </summary>
public JournalEntry? GoBack()
{
if (_backStack.Count == 0) return null;
var entry = _backStack.Pop();
_forwardStack.Push(entry);
return entry;
}
/// <summary>
/// 前进
/// </summary>
public JournalEntry? GoForward()
{
if (_forwardStack.Count == 0) return null;
var entry = _forwardStack.Pop();
_backStack.Push(entry);
return entry;
}
public void Clear()
{
_backStack.Clear();
_forwardStack.Clear();
}
}
public record JournalEntry(string RegionName, string ViewName, object? Parameter);集成到 RegionManager
public class RegionManager
{
private readonly NavigationJournal _journal = new();
// ... 其他代码不变
public bool NavigateTo(string regionName, string viewName, object? parameter = null)
{
// 记录当前页面到历史
var currentView = GetCurrentView(regionName);
if (currentView != null)
{
// 查找当前 View 的名称(需要维护反向映射)
_journal.RecordNavigation(regionName, GetCurrentViewName(regionName), null);
}
// ... 执行导航
}
public bool CanGoBack(string regionName) => _journal.CanGoBack;
public bool CanGoForward(string regionName) => _journal.CanGoForward;
public bool GoBack(string regionName)
{
var entry = _journal.GoBack();
if (entry == null) return false;
return NavigateToInternal(entry.RegionName, entry.ViewName, entry.Parameter, recordHistory: false);
}
public bool GoForward(string regionName)
{
var entry = _journal.GoForward();
if (entry == null) return false;
return NavigateToInternal(entry.RegionName, entry.ViewName, entry.Parameter, recordHistory: false);
}
}导航命令
public class NavigationViewModel : ObservableObject
{
private readonly RegionManager _regionManager;
public NavigationViewModel(RegionManager regionManager)
{
_regionManager = regionManager;
GoBackCommand = new RelayCommand(() => _regionManager.GoBack("ContentRegion"),
() => _regionManager.CanGoBack("ContentRegion"));
GoForwardCommand = new RelayCommand(() => _regionManager.GoForward("ContentRegion"),
() => _regionManager.CanGoForward("ContentRegion"));
}
public RelayCommand GoBackCommand { get; }
public RelayCommand GoForwardCommand { get; }
}导航拦截与确认
未保存数据提醒
/// <summary>
/// 支持导航确认的 RegionManager
/// </summary>
public class RegionManager
{
// ... 其他代码
public async Task<bool> NavigateToAsync(string regionName, string viewName,
object? parameter = null)
{
// 检查当前 View 是否允许离开
var currentView = GetCurrentView(regionName);
if (currentView?.DataContext is INavigationConfirm confirm)
{
bool canLeave = false;
confirm.ConfirmNavigation(result =>
{
canLeave = result;
});
// 等待用户确认
while (!confirm.CanNavigateAway())
{
await Task.Delay(100);
}
if (!confirm.CanNavigateAway())
return false;
}
return NavigateTo(regionName, viewName, parameter);
}
}ViewModel 中的导航确认
public class RecipeEditorViewModel : ObservableObject, INavigationAware, INavigationConfirm
{
private bool _hasUnsavedChanges;
private bool _canNavigateAway;
public bool HasUnsavedChanges
{
get => _hasUnsavedChanges;
private set
{
if (SetProperty(ref _hasUnsavedChanges, value))
_canNavigateAway = !value;
}
}
public bool CanNavigateAway() => _canNavigateAway;
public void ConfirmNavigation(Action<bool> callback)
{
if (!_hasUnsavedChanges)
{
callback(true);
return;
}
// 显示确认对话框
var result = MessageBox.Show(
"当前配方有未保存的修改,是否离开?\n离开后修改将丢失。",
"未保存修改",
MessageBoxButton.YesNo,
MessageBoxImage.Warning);
if (result == MessageBoxResult.No)
{
callback(false);
return;
}
_canNavigateAway = true;
callback(true);
}
public void OnNavigatedTo(object? parameter) { /* 加载配方 */ }
public void OnNavigatedFrom() { /* 释放资源 */ }
// 修改配方时标记为未保存
partial void OnRecipeNameChanged(string value) => HasUnsavedChanges = true;
}导航事件通知
导航事件
public class NavigationEventArgs : EventArgs
{
public string RegionName { get; }
public string ViewName { get; }
public object? Parameter { get; }
public FrameworkElement? OldView { get; }
public FrameworkElement? NewView { get; }
public NavigationEventArgs(string regionName, string viewName, object? parameter,
FrameworkElement? oldView, FrameworkElement? newView)
{
RegionName = regionName;
ViewName = viewName;
Parameter = parameter;
OldView = oldView;
NewView = newView;
}
}
public class RegionManager
{
public event EventHandler<NavigationEventArgs>? Navigating;
public event EventHandler<NavigationEventArgs>? Navigated;
// 在 NavigateTo 方法中触发事件
public bool NavigateTo(string regionName, string viewName, object? parameter = null)
{
// 触发 Navigating 事件(导航前)
Navigating?.Invoke(this, new NavigationEventArgs(
regionName, viewName, parameter, oldView: null, newView: null));
// ... 执行导航逻辑
// 触发 Navigated 事件(导航后)
Navigated?.Invoke(this, new NavigationEventArgs(
regionName, viewName, parameter, oldView: oldView, newView: newView));
return true;
}
}导航日志
public class NavigationLogger
{
public NavigationLogger(RegionManager regionManager)
{
regionManager.Navigating += OnNavigating;
regionManager.Navigated += OnNavigated;
}
private void OnNavigating(object sender, NavigationEventArgs e)
{
Debug.WriteLine($"[Navigation] 导航中: {e.RegionName} → {e.ViewName}");
}
private void OnNavigated(object sender, NavigationEventArgs e)
{
Debug.WriteLine($"[Navigation] 导航完成: {e.RegionName} → {e.ViewName}");
}
}多 Region 布局
复杂布局中的多个 Region
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- 顶部导航栏 -->
<RowDefinition Height="*" /> <!-- 主内容区 -->
<RowDefinition Height="Auto" /> <!-- 底部状态栏 -->
</Grid.RowDefinitions>
<!-- 顶部 Region -->
<ContentControl Grid.Row="0" x:Name="headerRegion" />
<!-- 主内容区:左右分栏 -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 左侧列表 Region -->
<ContentControl Grid.Column="0" x:Name="sidebarRegion" />
<!-- 右侧详情 Region -->
<ContentControl Grid.Column="1" x:Name="detailRegion" />
</Grid>
<!-- 底部状态栏 Region -->
<ContentControl Grid.Row="2" x:Name="statusBarRegion" />
</Grid>// 注册多个 Region
_regionManager.RegisterRegion("HeaderRegion", headerRegion);
_regionManager.RegisterRegion("SidebarRegion", sidebarRegion);
_regionManager.RegisterRegion("DetailRegion", detailRegion);
_regionManager.RegisterRegion("StatusBarRegion", statusBarRegion);
// 同时导航多个 Region
_regionManager.NavigateTo("HeaderRegion", "MainHeader");
_regionManager.NavigateTo("SidebarRegion", "DeviceList");
_regionManager.NavigateTo("DetailRegion", "DeviceDetail", new NavigationParameters("DeviceId", "PLC-001"));
_regionManager.NavigateTo("StatusBarRegion", "MainStatusBar");Region 间联动
public class RegionManager
{
public event EventHandler<NavigationEventArgs>? Navigated;
// 当 SidebarRegion 导航完成时,自动导航 DetailRegion
private void OnNavigated(object sender, NavigationEventArgs e)
{
if (e.RegionName == "SidebarRegion" && e.Parameter is NavigationParameters param)
{
var deviceId = param.Get<string>("SelectedDeviceId");
if (deviceId != null)
{
NavigateTo("DetailRegion", "DeviceDetail",
new NavigationParameters("DeviceId", deviceId));
}
}
}
}Prism 框架的 Region 导航
对于大型项目,建议直接使用 Prism 框架的 Region 导航,它提供了更完善的实现:
// Prism 中的 Region 导航
public class AppModule : IModule
{
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<DeviceMonitorView, DeviceMonitorViewModel>();
containerRegistry.RegisterForNavigation<AlarmHistoryView, AlarmHistoryViewModel>();
}
public void OnInitialized(IContainerProvider containerProvider)
{
var regionManager = containerProvider.Resolve<IRegionManager>();
// 注册 Region
regionManager.RegisterViewWithRegion("ContentRegion",
typeof(DeviceMonitorView));
}
}
// Prism 中的 ViewModel 实现 INavigationAware
public class DeviceMonitorViewModel : BindableBase, INavigationAware, IConfirmNavigationRequest
{
public void OnNavigatedTo(NavigationContext navigationContext)
{
var deviceId = navigationContext.Parameters.GetValue<string>("DeviceId");
}
public bool IsNavigationTarget(NavigationContext navigationContext)
{
// 返回 true 表示复用当前实例,false 表示创建新实例
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext) { }
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuation)
{
if (_hasUnsavedChanges)
{
var result = MessageBox.Show("有未保存的修改,是否离开?", "确认",
MessageBoxButton.YesNo);
continuation(result == MessageBoxResult.Yes);
}
else
{
continuation(true);
}
}
}常见问题与排错
Region 名称不匹配
错误: View 'DeviceMonitor' 未注册到 Region 'ContentRegion'检查:
- Region 注册的名称是否与导航时使用的名称一致
- View 注册的 Region 名称是否正确
- 确认 RegisterRegion 和 NavigateTo 的调用顺序
ViewModel 未注入
错误: 无法创建实例,缺少构造函数参数检查:
- ViewModel 的依赖是否在 DI 容器中注册
- View 工厂函数是否正确设置了 DataContext
- 确认 DI 容器的生命周期(Singleton vs Transient)
导航后数据不刷新
如果 ViewModel 实现了 INavigationAware 但 OnNavigatedTo 未被调用:
- 检查 View 的 DataContext 是否正确设置为 ViewModel
- 确认 RegionManager 的导航方法中正确调用了 OnNavigatedTo
- 如果使用 Singleton ViewModel,注意数据可能已缓存
最佳实践总结
- 统一注册入口:在 App 启动时统一注册所有 Region 和 View,避免分散注册
- DI 管理生命周期:通过 DI 容器管理 ViewModel 的创建和依赖注入
- INavigationAware 管理资源:在 OnNavigatedTo 中加载数据,在 OnNavigatedFrom 中释放资源
- 导航日志:为导航添加日志记录,方便调试页面切换问题
- 导航拦截:实现 INavigationConfirm 处理未保存数据
- 弱引用管理:使用 WeakReference 管理当前 View,避免内存泄漏
- 异常处理:导航过程中捕获异常,避免页面空白
