基于MVVM与Ninject,Core与WPF应用依赖注入结合页面切换的实现问询
嘿,我来帮你理顺这个问题——你现在的核心需求是在页面切换时,既能通过Ninject给页面注入对应的ViewModel,又避开ServiceLocator反模式对吧?之前用枚举转页面的方式确实会卡在硬编码new Page()的环节,没法利用DI,我给你两个符合DI原则的可行方案:
方案一:用ViewModel基类+WPF数据模板匹配(最推荐的常规做法)
这个方案完全贴合WPF的MVVM设计思想,不需要自定义转换器,还能完美利用DI:
定义ViewModel基类
先给所有页面的ViewModel创建一个基类(或者接口),这样CurrentPage属性就可以接收任意子类型的ViewModel:public abstract class BaseViewModel : INotifyPropertyChanged { // 实现INotifyPropertyChanged的通用逻辑 public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null) { if (EqualityComparer<T>.Default.Equals(field, value)) return false; field = value; OnPropertyChanged(propertyName); return true; } }让你的
PageOneViewModel、PageTwoViewModel都继承这个基类。在ApplicationViewModel中注入所需ViewModel
通过构造函数注入需要的ViewModel,然后切换页面时直接赋值给CurrentPage:public class ApplicationViewModel : BaseViewModel { private BaseViewModel _currentPage; public BaseViewModel CurrentPage { get => _currentPage; set => SetProperty(ref _currentPage, value); } // 构造函数注入页面ViewModel public ApplicationViewModel(PageOneViewModel pageOneVm, PageTwoViewModel pageTwoVm) { PageOneVm = pageOneVm; PageTwoVm = pageTwoVm; // 默认显示第一个页面 CurrentPage = PageOneVm; } public PageOneViewModel PageOneVm { get; } public PageTwoViewModel PageTwoVm { get; } // 切换页面的命令(假设你用RelayCommand实现ICommand) public ICommand GoToPageOneCommand => new RelayCommand(() => CurrentPage = PageOneVm); public ICommand GoToPageTwoCommand => new RelayCommand(() => CurrentPage = PageTwoVm); }用DataTemplate关联ViewModel和页面
在App.xaml(或者专用的资源字典)中添加数据模板,WPF会自动根据CurrentPage的ViewModel类型渲染对应的页面:<Application.Resources> <!-- 确保已引入ViewModel和View的命名空间 --> <xmlns:views="clr-namespace:YourApp.Views" /> <xmlns:viewModels="clr-namespace:YourApp.ViewModels" /> <DataTemplate DataType="{x:Type viewModels:PageOneViewModel}"> <views:PageOne /> </DataTemplate> <DataTemplate DataType="{x:Type viewModels:PageTwoViewModel}"> <views:PageTwo /> </DataTemplate> </Application.Resources>页面构造函数接收ViewModel
让页面的构造函数直接注入对应的ViewModel,Ninject会自动解析依赖:public partial class PageOne : Page { public PageOne(PageOneViewModel viewModel) { InitializeComponent(); DataContext = viewModel; } }Ninject绑定配置
别忘了在Ninject模块中注册ViewModel和页面:public class AppModule : NinjectModule { public override void Load() { // 注册ViewModel Bind<PageOneViewModel>().ToSelf(); Bind<PageTwoViewModel>().ToSelf(); // 注册页面 Bind<PageOne>().ToSelf(); Bind<PageTwo>().ToSelf(); // 注册ApplicationViewModel Bind<ApplicationViewModel>().ToSelf(); } }
方案二:用ViewModel工厂(适合页面多或需要动态创建ViewModel的场景)
如果你的应用页面很多,或者ViewModel需要动态参数创建,直接在ApplicationViewModel中注入所有ViewModel会导致类臃肿,这时候用工厂模式更合适:
定义ViewModel工厂接口
public interface IViewModelFactory { T Create<T>() where T : BaseViewModel; BaseViewModel Create(Type viewModelType); }用Ninject实现工厂
public class NinjectViewModelFactory : IViewModelFactory { private readonly IKernel _kernel; public NinjectViewModelFactory(IKernel kernel) { _kernel = kernel; } public T Create<T>() where T : BaseViewModel { return _kernel.Get<T>(); } public BaseViewModel Create(Type viewModelType) { return _kernel.Get(viewModelType) as BaseViewModel; } }ApplicationViewModel中使用工厂切换页面
public class ApplicationViewModel : BaseViewModel { private readonly IViewModelFactory _vmFactory; private BaseViewModel _currentPage; public BaseViewModel CurrentPage { get => _currentPage; set => SetProperty(ref _currentPage, value); } public ApplicationViewModel(IViewModelFactory vmFactory) { _vmFactory = vmFactory; CurrentPage = _vmFactory.Create<PageOneViewModel>(); } public ICommand GoToPageOneCommand => new RelayCommand(() => CurrentPage = _vmFactory.Create<PageOneViewModel>()); public ICommand GoToPageTwoCommand => new RelayCommand(() => CurrentPage = _vmFactory.Create<PageTwoViewModel>()); }绑定工厂和其他配置
在Ninject模块中注册工厂:Bind<IViewModelFactory>().To<NinjectViewModelFactory>();DataTemplate和页面的配置和方案一完全一致。
为什么这两个方案避开了ServiceLocator反模式?
- 这两个方案都是通过构造函数注入获取依赖(ViewModel或工厂),而不是在代码中直接调用Ninject的
Get()方法(这就是ServiceLocator的核心问题:依赖查找而非依赖注入)。 - 用ViewModel作为
CurrentPage的类型,结合WPF的DataTemplate机制,完全遵循了“数据驱动UI”的MVVM原则,不需要硬编码页面切换逻辑。
针对你之前的疑问:
- 把
CurrentPage设为ViewModel基类/接口是完全可行的,这样就能接收所有子类型的ViewModel,解决了类型限制的问题。 - 不需要把ViewModel传入转换器,因为DataTemplate已经帮你完成了ViewModel到页面的匹配渲染,自定义转换器的方式反而绕远路了。
内容的提问来源于stack exchange,提问作者Tristan Trainer

