在传统的WPF开发中,我们经常会遇到一个头疼的问题:界面逻辑和业务代码搅在一起,就像一碗打翻的意大利面,越搅越乱。我接手过一个项目,MainWindow.xaml.cs文件里有2000多行代码,每次改个按钮位置都得小心翼翼,生怕碰坏了其他功能。这种紧耦合的架构让团队吃尽了苦头。
Prism框架给出的解决方案是把应用拆分成独立的模块。想象你的应用是个乐高玩具,每个功能模块就像一块积木,可以单独开发、测试,最后通过导航机制拼装起来。RegionManager就是这个拼装师,它负责在指定区域动态加载不同视图,而ViewModel之间完全不需要知道彼此的存在。
首先打开Visual Studio(建议2019或更高版本),选择"WPF应用(.NET Framework)"模板。这里有个坑要注意:一定要选.NET Framework 4.6.1以上版本,因为Prism 7.2开始需要这个基础运行时。
安装NuGet包时别手抖,这三个是核心依赖:
我习惯用Package Manager Console安装:
powershell复制Install-Package Prism.Wpf -Version 7.2.0.1422
Install-Package Prism.Unity -Version 7.2.0.1422
删除自动生成的MainWindow.xaml后,重点改造App.xaml。把根节点改成这样:
xml复制<prism:PrismApplication x:Class="YourApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/">
</prism:PrismApplication>
后台代码需要重写三个关键方法:
csharp复制protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// 这里注册视图导航映射
containerRegistry.RegisterForNavigation<ViewA>("ViewA");
containerRegistry.RegisterForNavigation<ViewB>("ViewB");
}
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
// 模块化开发时在这里注册模块
}
在MainWindow.xaml中,我们用ContentControl声明一个可动态替换的区域:
xml复制<ContentControl prism:RegionManager.RegionName="MainRegion" />
这就像在页面上挖了个"洞",后续所有视图都会在这个洞里动态显示。实际项目中我常用DockPanel+多个区域实现复杂布局:
xml复制<DockPanel>
<Menu DockPanel.Dock="Top" prism:RegionManager.RegionName="MenuRegion"/>
<StatusBar DockPanel.Dock="Bottom" prism:RegionManager.RegionName="StatusRegion"/>
<Grid>
<ContentControl prism:RegionManager.RegionName="MainRegion"/>
</Grid>
</DockPanel>
命令式导航适合按钮点击场景:
csharp复制public DelegateCommand NavigateCommand => new DelegateCommand(() =>
{
_regionManager.RequestNavigate("MainRegion", "ViewA");
});
声明式导航更简洁,直接在XAML绑定:
xml复制<Button Command="{Binding NavigateCommand}"
CommandParameter="ViewA"
Content="前往A视图"/>
遇到过的一个坑:导航参数传递要用NavigationParameters对象:
csharp复制var parameters = new NavigationParameters();
parameters.Add("id", 123);
_regionManager.RequestNavigate("MainRegion", "ViewA", parameters);
大型项目应该分模块开发,在App.xaml.cs中配置:
csharp复制protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<AModule>();
moduleCatalog.AddModule<BModule>(InitializationMode.OnDemand);
}
OnDemand模式的模块不会立即加载,适合后台管理这类低频功能。触发加载的代码:
csharp复制_moduleManager.LoadModule("BModule");
有时需要在导航前后执行逻辑,比如权限检查:
csharp复制_regionManager.Regions["MainRegion"].NavigationService.Navigated += (sender, args) =>
{
var view = args.Context.Uri.ToString();
Debug.WriteLine($"已导航到:{view}");
};
更复杂的场景可以实现INavigationAware接口:
csharp复制public class ViewAViewModel : INavigationAware
{
public void OnNavigatedTo(NavigationContext context)
{
var id = context.Parameters["id"];
// 初始化数据
}
public bool IsNavigationTarget(NavigationContext context)
{
return true; // 返回false会强制创建新实例
}
public void OnNavigatedFrom(NavigationContext context)
{
// 保存未提交的数据
}
}
默认情况下Prism不会记录导航历史,需要自己实现返回功能:
csharp复制private Stack<string> _navigationStack = new Stack<string>();
private void Navigate(string viewName)
{
_navigationStack.Push(viewName);
_regionManager.RequestNavigate("MainRegion", viewName);
}
public DelegateCommand GoBackCommand => new DelegateCommand(() =>
{
if(_navigationStack.Count > 1)
{
_navigationStack.Pop(); // 移除当前视图
var previousView = _navigationStack.Pop();
Navigate(previousView);
}
});
频繁切换视图时,默认每次都会新建实例。通过RegionMemberLifetimeAttribute可以缓存视图:
csharp复制[RegionMemberLifetime(KeepAlive = true)]
public partial class DashboardView : UserControl
{
// 视图实例会被保留
}
或者全局设置缓存:
csharp复制RegionManager.Regions["MainRegion"].RegionMemberLifetime = KeepAlive;
结合权限系统实现动态菜单:
csharp复制protected override void InitializeShell(Window shell)
{
var menuRegion = _regionManager.Regions["MenuRegion"];
foreach(var item in _permissionService.GetAllowedMenus())
{
menuRegion.Add(new MenuItem {
Header = item.Name,
Command = new DelegateCommand(() =>
_regionManager.RequestNavigate("MainRegion", item.ViewName))
});
}
}
导航失败时,先检查这几个地方:
containerRegistry.RegisterForNavigation<View>()建议在App启动时添加日志:
csharp复制protected override void OnInitialized()
{
base.OnInitialized();
Logger.Log("应用初始化完成", Category.Info);
}
使用Prism的EventAggregator发布导航事件:
csharp复制_eventAggregator.GetEvent<NavigationEvent>().Publish(new NavigationPayload{
ViewName = "ViewA",
Timestamp = DateTime.Now
});
在性能关键路径添加计时:
csharp复制var watch = Stopwatch.StartNew();
_regionManager.RequestNavigate("MainRegion", "ComplexView");
watch.Stop();
Debug.WriteLine($"导航耗时:{watch.ElapsedMilliseconds}ms");
测试ViewModel的导航逻辑时,需要Mock IRegionManager:
csharp复制[TestMethod]
public void Should_Navigate_To_ViewA()
{
var mockRegion = new Mock<IRegion>();
var mockRegionManager = new Mock<IRegionManager>();
mockRegionManager.Setup(x => x.Regions["MainRegion"]).Returns(mockRegion.Object);
var vm = new MainViewModel(mockRegionManager.Object);
vm.NavigateCommand.Execute();
mockRegion.Verify(x => x.RequestNavigate("ViewA", null), Times.Once);
}
用TestStack.White模拟点击导航:
csharp复制var mainWindow = application.GetWindow("MainWindow");
var button = mainWindow.Get<Button>("NavigateButton");
button.Click();
var contentControl = mainWindow.Get<ContentControl>("MainRegion");
Assert.IsTrue(contentControl.Content is ViewA);
新版本主要变化在依赖注入容器,以Unity为例:
csharp复制// 旧版
container.RegisterType<IService, MyService>();
// 新版
containerRegistry.Register<IService, MyService>();
区域管理器API变得更简洁:
csharp复制// 旧版
_regionManager.Regions["MainRegion"].Add(view);
// 新版
_regionManager.RegisterViewWithRegion("MainRegion", typeof(ViewA));
升级时特别注意:Prism 8默认使用DryIoc,如果项目原来用Unity,需要显式安装Prism.Unity.Extensions包。