在Simple Injector中解析具有自定义参数的类

本文关键字:自定义 参数 Simple Injector | 更新日期: 2023-09-27 18:21:18

我正在使用Simple Injector作为DI容器创建一个WPF MVVM应用程序。现在,当我试图从Simple Injector解析视图时,我遇到了一些问题,因为我需要在构造时将参数传递到构造函数中(而不是在将视图注册到容器时,因此这不适用:Simple Inject将值传递到构造函数)。

我想要的是这样的东西:

var item = container.GetInstance<MyType>(myParameter);

我在Simple Injector中读到过几个地方,这是不可能的,因为不应该这样做(包括这里:https://simpleinjector.codeplex.com/discussions/397080)。

这是真的吗?如果是,我怎么能这样做


背景信息

我有一个多个视图模型和模型的集合,这些模型由一个特定的键查找,我想传递到视图中的参数是视图模型要使用的键。我发现这是必要的,因为视图模型和模型在应用程序的多个位置使用,并且如果它们具有相同的键,则需要保持同步/是相同的实例。我不认为我能够使用生存期作用域来解决这个问题,而且当我注册到容器时,我不可能知道密钥。我也知道ViewModelLocator方法可能是ServiceLocator(反?)模式,但是,目前它是我所拥有的最好的。

我的构造函数目前看起来是这样的,我希望IViewModelLocator得到解析,同时我传递密钥:

public FillPropertiesView(IViewModelLocator vml, object key)
{
    // .. Removed code
    // Assign the view model
    var viewModel = vml.GetViewModel<FillPropertiesViewModel>(key);
    DataContext = viewModel;
}

IViewModelLocator如下所示(模型也有类似的接口)。

public interface IViewModelLocator
{
    // Gets the view model associated with a key, or a default
    // view model if no key is supplied
    T GetViewModel<T>(object key = null) where T : class, IViewModel;
}

现在我有以下问题:

  • 使用视图模型键解析视图的最佳方法是什么
  • 我必须进行一些重构才能实现这一点吗
  • 自从我创建了自己的基于字典的ViewModelLocator以来,我是否错过了DI容器的一些功能

扩展信息

我已经在上面展示了ViewModelLocator,我使用它的原因是为了保持可混合性(基本上,它只是在Blend中打开时为我提供设计时数据)。如果我不必同时打开不同的窗口(见下一段),则视图的每个实例的运行时视图模型都可以是相同的(不依赖于键)。然而,当ViewModel获取模型时,上述问题是相同的,并且ModelLocator需要一个键来获取现有模型(如果存在)。

这是针对PowerPoint的VSTO应用程序的一部分(它会影响设计的某些部分)。当选择屏幕上的对象时,将打开一个任务面板(这基本上是上面解释的FillPropertiesView)。所选对象具有提供给视图的键,以便视图从ViewModelLocator中提取正确的视图模型。然后,视图模型将通过使用IModelLocator(类似于IViewModelLocator)和相同的键来获得对模型的引用。同时,控制器将使用相同的键从ModelLocator获取模型。控制器侦听模型中的更改事件,并更新屏幕上的对象。每当选择新对象时,都会复制相同的过程,同时可以打开多个窗口,这些窗口可以同时与相同或不同的对象交互(也就是说,多个任务窗格都具有唯一的视图模型)。

到目前为止,我已经用一个默认的无参数构造函数解析了视图,然后用一个方法调用注入了视图模型:

// Sets the view model on a view
public void SetViewModel(IViewModelLocator vml, object key)
{
    // Assign the view model
    _viewModel = vml.GetViewModel<FillPropertiesViewModel>(key);
    DataContext = _viewModel;
}

我从未向容器注册过视图,但解决了如下具体类型:

string key = "testkey" // read from the selected object
var view = container.GetInstance<FillPropertiesView>();
var vml = container.GetInstance<IViewModelLocator>();
view.SetViewModel(vml, key);

当我试图重构它时,这个问题浮出水面,这样我就不必每次都调用SetViewModel()方法并手动解析视图模型等。当我还必须在视图模型中手动初始化以同样的方式初始化模型时,这变得非常混乱。


视图模型定位器

ViewModelLocator当前作为DI容器的包装器工作,即视图模型在Simple Injector中注册。

注册如下(在一个名为CompositionHost的类中):

container.RegisterSingle<IViewModelLocator, ViewModelLocator>();
container.RegisterSingle<IModelLocator, ModelLocator>();

实现如下所示:

// Base implementation used by ViewModelLocator and ModelLocator
public class ServiceLocator<TService> where TService : class
{
    private readonly Dictionary<CombinedTypeKey, TService> _instances =
        new Dictionary<CombinedTypeKey, TService>();
    // Gets a service instance based on the type and a key.
    // The key makes it possible to have multiple versions of the same service.
    public T GetInstance<T>(object key = null) where T : class, TService
    {
        var combinedKey = new CombinedTypeKey(typeof(T), key);
        // Check if we already have an instance
        if (_instances.ContainsKey(combinedKey))
        {
            return _instances[combinedKey] as T;
        }
        // Create a new instance
        // CompositionHost is a static reference to the DI container (and 
        // should perhaps be injected, however, that is not the main issue here)
        var instance = CompositionHost.GetInstance<T>();
        _instances.Add(combinedKey, instance);
        return instance;
    }
    // A combined key to ease dictionary operations
    private struct CombinedTypeKey
    {
        private readonly object _key;
        private readonly Type _type;
        public CombinedTypeKey(Type type, object key)
        {
            _type = type;
            _key = key;
        }
        // Equals and GetHashCode() are overridden
    }
}
public class ViewModelLocator : IViewModelLocator
{
    private readonly ServiceLocator<IViewModel> _viewModelLocator; 
    public ViewModelLocator(ServiceLocator<IViewModel> locator)
    {
        _viewModelLocator = locator;
        // Dummy code that registers design time data is removed
    }
    // IViewModel is just an empty interface implemented by the view models
    public T GetViewModel<T>(object key = null) where T : class, IViewModel
    {
        return _viewModelLocator.GetInstance<T>(key);
    }
}

在Simple Injector中解析具有自定义参数的类

在类中注入服务定位器(几乎)永远不会成功,因为这不允许在编译时检查依赖关系和运行时依赖关系分析。出于这个原因,我还可以建议注册所有的根类型(如视图),因为否则Simple Injector将被蒙在鼓里,无法就您可能存在的任何错误配置向您提供建议。

由于您有始终缓存在一起的View+ViewModel对,但可能依赖于多个View+ViewModel对重用的Model实例,因此我建议采用以下设计。

定义视图和视图模型的抽象:

public interface IView<TModel>
{
    IViewModel<TModel> ViewModel { get; }
}
public interface IViewModel<TModel>
{
    TModel Model { get; set; }
}

定义按键检索/缓存视图的抽象。

public interface IViewProvider<TView, TModel> where TView : IView<TModel>
{
    TView GetViewByKey(object key);
}

使用这些抽象,您的视图可以如下所示:

public class FillPropertiesView : IView<FillPropertiesModel>
{
    public FillPropertiesView(FillPropertiesViewModel viewModel)
    {
        this.ViewModel = viewModel;
    }
    public IViewModel<FillPropertiesModel> ViewModel { get; private set; }
}

您的控制器可以依赖于IViewProvider<TView, TModel>抽象,因此当新密钥进入时,它们可以重新加载视图:

public class FillPropertiesController : Controller
{
    IViewProvider<FillPropertiesView, FillPropertiesModel> viewProvider;
    FillPropertiesView view;
    public FillPropertiesController(
        IViewProvider<FillPropertiesView, FillPropertiesModel> provider) {
        this.viewProvider = provider;
    }
    public void Reinitialize(object key) {
        this.view = this.viewProvider.GetViewByKey(key);
    }
}

IViewProvider<TView, TModel>的实现可能如下所示:

public class ViewProvider<TView, TModel> : IViewProvider<TView, TModel> 
    where TView : class, IView<TModel> {
    Dictionary<object, TView> views = new Dictionary<object, TView>();
    Container container;
    IModelProvider<TModel> modelProvider;
    public ViewProvider(Container container,
        IModelProvider<TModel> modelProvider) {
        this.container = container;
        this.modelProvider = modelProvider;
    }
    public TView GetViewByKey(object key) {
        TView view;
        if (!this.views.TryGetValue(key, out view)) {
            this.views[key] = view = this.CreateView(key);
        }
        return view;
    }
    private TView CreateView(object key) {
        TView view = this.container.GetInstance<TView>();
        view.ViewModel.Model = this.modelProvider.GetModelByKey(key);
        return view;
    }
}

此实现依赖于(以前未定义的)IModelProvider<TModel>抽象。这基本上是您的旧ModelLocator,但通过使用泛型类型,您可以使实现更加容易,因为我们可以为每个TModel提供一个这种类型的实例(ViewProvider也是如此),这使您不必使用{type+key}组合存储元素。

您可以按以下方式注册:

Assembly asm = Assembly.GetExecutingAssembly();
container.RegisterManyForOpenGeneric(typeof(IView<>), asm);
container.RegisterManyForOpenGeneric(typeof(IViewModel<>), asm);
container.RegisterOpenGeneric(typeof(IViewProvider<,>), 
    typeof(ViewProvider<,>), Lifestyle.Singleton);
container.RegisterOpenGeneric(typeof(IModelProvider<>), 
    typeof(ModelProvider<>), Lifestyle.Singleton);
var controllers =
    from type in asm.GetTypes()
    where type.IsSubClassOf(typeof(Controller))
    where !type.IsAbstract
    select type;
controllers.ToList().ForEach(t => container.Register(t));
container.Verify();

使用RegisterManyForOpenGeneric,您可以让Simple Injector搜索提供的程序集,以查找给定的开放泛型抽象的实现,Simple Inject将为您批量注册它们。使用RegisterOpenGeneric,您可以指定一个开放的泛型抽象,并在请求该抽象的封闭泛型版本时告诉Simple Injector要使用哪个实现。最后一行搜索应用程序以查找所有控制器类型,并将它们注册到系统中。