如何在WPF / MVVM应用程序中处理dependency injection
我正在开始一个新的桌面应用程序,我想用MVVM和WPF来构build它。
我也打算使用TDD。
问题是,我不知道如何使用IoC容器将我的dependency injection到我的生产代码中。
假设我有以下的类和接口:
public interface IStorage { bool SaveFile(string content); } public class Storage : IStorage { public bool SaveFile(string content){ // Saves the file using StreamWriter } }
然后我有另一个有IStorage
作为依赖的类,假设这个类是一个ViewModel或一个业务类…
public class SomeViewModel { private IStorage _storage; public SomeViewModel(IStorage storage){ _storage = storage; } }
有了这个,我可以轻松地编写unit testing,以确保它们正常工作,使用模拟等。
问题是在真正的应用程序中使用它。 我知道我必须有一个链接IStorage
接口的默认实现的IoC容器,但我该怎么做呢?
例如,如果我有以下xaml,将会如何呢?
<Window ... xmlns definitions ... > <Window.DataContext> <local:SomeViewModel /> </Window.DataContext> </Window>
如何正确“告诉”WPF在这种情况下注入依赖关系?
另外,假设我需要从我的cs
代码SomeViewModel
一个实例,我应该怎么做呢?
我觉得我完全失去了这一点,我将不胜感激任何例子或指导如何是最好的办法来处理它。
我熟悉StructureMap,但我不是专家。 另外,如果有一个更好/更容易/现成的框架,请让我知道。
提前致谢。
我一直在使用Ninject,发现很高兴与之合作。 一切都在代码中设置,语法是相当简单的,它有一个很好的文档(和大量的答案)。
所以基本上是这样的:
创build视图模型,并将IStorage接口作为构造器参数:
class UserControlViewModel { public UserControlViewModel(IStorage storage) { } }
使用视图模型的get属性创build一个ViewModelLocator,该视图模型从Ninject加载视图模型:
class ViewModelLocator { public UserControlViewModel UserControlViewModel { get { return IocKernel.Get<UserControlViewModel>();} // Loading UserControlViewModel will automatically load the binding for IStorage } }
使ViewModelLocator成为App.xaml中的应用程序范围资源:
<Application ...> <Application.Resources> <local:ViewModelLocator x:Key="ViewModelLocator"/> </Application.Resources> </Application>
将UserControl的DataContext绑定到ViewModelLocator中的相应属性。
<UserControl ... DataContext="{Binding UserControlViewModel, Source={StaticResource ViewModelLocator}}"> <Grid> </Grid> </UserControl>
创build一个inheritanceNinjectModule的类,它将设置必要的绑定(IStorage和viewmodel):
class IocConfiguration : NinjectModule { public override void Load() { Bind<IStorage>().To<Storage>().InSingletonScope(); // Reuse same storage every time Bind<UserControlViewModel>().ToSelf().InTransientScope(); // Create new instance every time } }
在应用程序启动时使用必要的Ninject模块初始化IoC内核(现在是上面的模块):
public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { IocKernel.Initialize(new IocConfiguration()); base.OnStartup(e); } }
我使用了一个静态IocKernel类来保存IoC内核的应用程序范围的实例,所以我可以在需要的时候轻松地访问它:
public static class IocKernel { private static StandardKernel _kernel; public static T Get<T>() { return _kernel.Get<T>(); } public static void Initialize(params INinjectModule[] modules) { if (_kernel == null) { _kernel = new StandardKernel(modules); } } }
这个解决scheme确实使用了一个静态的ServiceLocator(IocKernel),它通常被认为是反模式,因为它隐藏了类的依赖关系。 但是,要避免对UI类进行某种手动服务查找是非常困难的,因为它们必须具有无参数的构造函数,并且无论如何您都无法控制实例化,因此无法注入VM。 至less通过这种方式,您可以孤立地testing虚拟机,这就是所有业务逻辑所在的位置。
如果有人有更好的方法,请分享。
编辑:Lucky Likey通过让Ninject实例化UI类提供了一个解决静态服务定位器的答案。 答案的细节可以在这里看到
在你的问题中,你可以在XAML中设置视图的DataContext
属性的值。 这就要求你的视图模型有一个默认的构造函数。 但是,正如你所指出的那样,在dependency injection的地方,你想在构造函数中注入依赖关系时,这不起作用。
所以你不能在XAML中设置DataContext
属性 。 相反,你有其他的select。
如果应用程序基于简单的分层视图模型,则可以在应用程序启动时构build整个视图模型层次结构(您将不得不从App.xaml
文件中删除StartupUri
属性):
public partial class App { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var container = CreateContainer(); var viewModel = container.Resolve<RootViewModel>(); var window = new MainWindow { DataContext = viewModel }; window.Show(); } }
这是基于RootViewModel
的视图模型的对象graphics,但是您可以将一些视图模型工厂注入到父视图模型中,以允许它们创build新的子视图模型,因此对象graphics不必修复。 这也希望能够回答你的问题, 假设我需要一个来自我的cs
代码的SomeViewModel
的实例,我应该怎么做?
class ParentViewModel { public ParentViewModel(ChildViewModelFactory childViewModelFactory) { _childViewModelFactory = childViewModelFactory; } public void AddChild() { Children.Add(_childViewModelFactory.Create()); } ObservableCollection<ChildViewModel> Children { get; private set; } } class ChildViewModelFactory { public ChildViewModelFactory(/* ChildViewModel dependencies */) { // Store dependencies. } public ChildViewModel Create() { return new ChildViewModel(/* Use stored dependencies */); } }
如果您的应用程序本质上更具dynamic性,并且可能基于导航,则必须挂钩执行导航的代码。 每次你浏览一个新的视图,你需要创build一个视图模型(从DI容器),视图本身,并将视图的DataContext
设置为视图模型。 您可以首先在基于视图select视图模型的位置执行此视图 ,或者您可以首先在视图模型确定要使用哪个视图的位置执行视图模型。 MVVM框架以某种方式为您提供了这个关键function,可以将DI容器挂接到视图模型的创build中,但您也可以自己实现它。 我在这里有点模糊,因为根据您的需要,这个function可能会变得相当复杂。 这是您从MVVM框架获得的核心function之一,但是在一个简单的应用程序中进行自己的翻译会让您很好地理解MVVM框架提供了什么。
由于无法在XAML中声明DataContext
,您将失去一些devise时支持。 如果您的视图模型包含一些数据,它将在devise时出现,这可能非常有用。 幸运的是,您也可以在WPF中使用devise时属性 。 一种方法是将以下属性添加到XAML中的<Window>
元素或<UserControl>
:
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DataContext="{d:DesignInstance Type=local:MyViewModel, IsDesignTimeCreatable=True}"
视图模型types应该有两个构造函数,devise时数据的默认值和dependency injection的默认值:
class MyViewModel : INotifyPropertyChanged { public MyViewModel() { // Create some design-time data. } public MyViewModel(/* Dependencies */) { // Store dependencies. } }
通过这样做,您可以使用dependency injection并保留良好的devise时支持。
我采用“先查看”的方法,将视图模型传递给视图的构造函数(在其代码后面),将其分配给数据上下文,例如
public class SomeView { public SomeView(SomeViewModel viewModel) { InitializeComponent(); DataContext = viewModel; } }
这取代了你的基于XAML的方法。
我使用Prism框架来处理导航 – 当一些代码要求显示一个特定的视图(通过“导航”),Prism将parsing该视图(内部使用应用程序的DI框架)。 DI框架将依次解决视图所具有的任何依赖关系(我的示例中的视图模型),然后parsing其依赖关系等等。
DI框架的select几乎是无关紧要的,因为它们都做基本相同的事情,即注册一个接口(或一个types)以及您希望框架在发现对该接口的依赖时实例化的具体types。 为了logging我使用温莎城堡。
棱镜导航需要一些习惯,但是一旦你了解它,它就会变得非常好,从而允许你使用不同的视图来编写你的应用程序。 例如,您可以在主窗口上创build棱镜“区域”,然后使用棱镜导航,您可以在该区域内从一个视图切换到另一个视图,例如当用户select菜单项或任何其他视图时。
或者看一下MVVM框架,比如MVVM Light。 我没有这些经验,所以不能评论他们喜欢用什么。
安装MVVM Light。
部分安装是创build视图模型定位器。 这是一个暴露你的视图模型属性的类。 这些属性的getter可以从你的IOC引擎返回实例。 幸运的是,MVVM light还包含了SimpleIOC框架,但是如果你愿意的话,你也可以在其他networking中连线。
用简单的IOC你注册一个types的实现…
SimpleIOC.Default.Register<MyViewModel>(()=> new MyViewModel(new ServiceProvider()), true);
在这个例子中,你的视图模型被创build,并按照其构造函数传递一个服务提供者对象。
然后创build一个从IOC返回实例的属性。
public MyViewModel { get { return SimpleIOC.Default.GetInstance<MyViewModel>; } }
聪明的部分是视图模型定位器然后在app.xaml中创build或作为数据源等效。
<local:ViewModelLocator x:key="Vml" />
你现在可以绑定到它的“MyViewModel”属性来获得一个注入服务的视图模型。
希望有所帮助。 对于从iPad上的内存编码的任何代码不准确的道歉。
我在这里发布的是对sondergard的答案的改进,因为我要告诉不符合评论:)
事实上,我引入了一个简洁的解决scheme,它避免了需要一个ServiceLocator和一个StandardKernel
-Instance的包装,它在sondergard的解决scheme中被称为IocContainer
。 为什么? 如上所述,这些是反模式。
使StandardKernel
无处不在
Ninject的魔法的关键是使用.Get<T>()
方法所需的StandardKernel
-Instance。
或者对sondergard的IocContainer
你可以在IocContainer
中创buildStandardKernel
。
只需从App.xaml中删除StartUpUri即可
<Application x:Class="Namespace.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> ... </Application>
这是App.xaml.cs中的App的CodeBehind
public partial class App { private IKernel _iocKernel; protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); _iocKernel = new StandardKernel(); _iocKernel.Load(new YourModule()); Current.MainWindow = _iocKernel.Get<MainWindow>(); Current.MainWindow.Show(); } }
从现在起,Ninject还活着,准备战斗:)
注入你的DataContext
由于Ninject还活着,您可以执行各种注射,例如Property Setter Injection或最常见的构造注入 。
这是如何将您的ViewModel注入到您的Window
的DataContext
public partial class MainWindow : Window { public MainWindow(MainWindowViewModel vm) { DataContext = vm; InitializeComponent(); } }
当然,如果你做了正确的绑定,你也可以注入一个IViewModel
,但这不是这个答案的一部分。
直接访问内核
如果你需要直接调用Kernel的方法(例如.Get<T>()
Method),你可以让Kernel注入自己。
private void DoStuffWithKernel(IKernel kernel) { kernel.Get<Something>(); kernel.Whatever(); }
如果你需要一个本地的内核实例,你可以注入它的属性。
[Inject] public IKernel Kernel { private get; set; }
虽然这可能是非常有用的,我不会build议你这样做。 只要注意以这种方式注入的对象在构造函数中不可用,因为它是在稍后注入的。
根据这个链接,你应该使用factory-extension而不是注入IKernel
(DI容器)。
在软件系统中使用DI容器的推荐方法是,应用程序的Composition Root是直接与容器接触的单个地方。
如何使用Ninject.Extensions.Factory也可以在这里是红色的。
使用托pipe扩展性框架 。
[Export(typeof(IViewModel)] public class SomeViewModel : IViewModel { private IStorage _storage; [ImportingConstructor] public SomeViewModel(IStorage storage){ _storage = storage; } public bool ProperlyInitialized { get { return _storage != null; } } } [Export(typeof(IStorage)] public class Storage : IStorage { public bool SaveFile(string content){ // Saves the file using StreamWriter } } //Somewhere in your application bootstrapping... public GetViewModel() { //Search all assemblies in the same directory where our dll/exe is string currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); var catalog = new DirectoryCatalog(currentPath); var container = new CompositionContainer(catalog); var viewModel = container.GetExport<IViewModel>(); //Assert that MEF did as advertised Debug.Assert(viewModel is SomViewModel); Debug.Assert(viewModel.ProperlyInitialized); }
一般来说,你要做的是有一个静态类,并使用工厂模式为您提供一个全局容器(caching,natch)。
至于如何注入视图模型,你注入他们注入其他所有相同的方式。 在XAML文件的代码后面创build一个导入构造函数(或者在属性/字段中放置一个导入语句),并告诉它导入视图模型。 然后将你的Window
的DataContext
绑定到那个属性上。 您实际上从容器中拉出的根对象通常是由Window
对象组成的。 只需将接口添加到窗口类,然后导出它们,然后从上面的目录中抓取(在App.xaml.cs中,这是WPF引导程序文件)。
我会build议使用ViewModel – 第一种方法https://github.com/Caliburn-Micro/Caliburn.Micro
请参阅: https : //caliburnmicro.codeplex.com/wikipage?title=All%20About%20Conventions
使用Castle Windsor
作为IOC容器。
所有关于公约
Caliburn.Micro的主要特点之一是通过采取一系列的惯例来消除对锅炉板代码的需求。 有些人喜欢公约,有些人讨厌他们。 这就是为什么CM的惯例是完全可定制的,甚至可以完全closures,如果不需要的话。 如果你打算使用约定,并且默认情况下它们是ON,那么知道这些约定是什么以及它们是如何工作是很好的。 这是本文的主题。 查看分辨率(ViewModel-First)
基本
您使用CM时可能遇到的第一个约定与查看分辨率有关。 这个约定会影响应用程序的任何ViewModel-First区域。 在ViewModel中 – 首先,我们有一个现有的ViewModel,我们需要渲染到屏幕上。 要做到这一点,CM使用一个简单的命名模式来find一个UserControl1,它应该绑定到ViewModel并显示。 那么,这是什么模式? 我们来看看ViewLocator.LocateForModelType来找出:
public static Func<Type, DependencyObject, object, UIElement> LocateForModelType = (modelType, displayLocation, context) =>{ var viewTypeName = modelType.FullName.Replace("Model", string.Empty); if(context != null) { viewTypeName = viewTypeName.Remove(viewTypeName.Length - 4, 4); viewTypeName = viewTypeName + "." + context; } var viewType = (from assmebly in AssemblySource.Instance from type in assmebly.GetExportedTypes() where type.FullName == viewTypeName select type).FirstOrDefault(); return viewType == null ? new TextBlock { Text = string.Format("{0} not found.", viewTypeName) } : GetOrCreateViewType(viewType); };
我们首先忽略“context”variables。 为了推导出这个观点,我们假定你在虚拟机的命名中使用了“ViewModel”这个文本,所以我们只是通过删除“Model”这个单词,把它改变为“View”。 这具有改变types名称和名称空间的效果。 所以ViewModels.CustomerViewModel将成为Views.CustomerView。 或者,如果您按function组织您的应用程序:CustomerManagement.CustomerViewModel成为CustomerManagement.CustomerView。 希望这很简单。 一旦我们有了这个名字,我们就可以search这个名字的types。 我们通过AssemblySource.Instance.2search任何已经暴露给CM的程序集,如果我们find了types,我们创build一个实例(或者如果注册的话从IoC容器获得一个实例)并将其返回给调用者。 如果我们没有find这个types,我们就会生成一个适当的“找不到”消息的视图。
现在回到那个“上下文”的价值。 这是CM如何支持同一个ViewModel的多个视图。 如果提供了一个上下文(通常是一个string或一个枚举),我们会根据该值进一步转换该名称。 这个转换实际上假定你有一个不同视图的文件夹(命名空间),通过从结尾删除单词“视图”并附加上下文。 所以,鉴于“主”的上下文我们ViewModels.CustomerViewModel将成为Views.Customer.Master。
从app.xaml中删除启动uri。
App.xaml.cs
public partial class App { protected override void OnStartup(StartupEventArgs e) { IoC.Configure(true); StartupUri = new Uri("Views/MainWindowView.xaml", UriKind.Relative); base.OnStartup(e); } }
现在你可以使用你的IoC类来构造实例。
MainWindowView.xaml.cs
public partial class MainWindowView { public MainWindowView() { var mainWindowViewModel = IoC.GetInstance<IMainWindowViewModel>(); //Do other configuration DataContext = mainWindowViewModel; InitializeComponent(); } }