【问题标题】:Resolving a class with a custom parameter in Simple Injector在 Simple Injector 中使用自定义参数解析类
【发布时间】:2014-11-04 05:40:42
【问题描述】:

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

我所追求的是这样的:

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 Em>)。选择的对象具有提供给视图的键,以便视图从 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

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);
    }

}

【问题讨论】:

  • 你能补充一些信息吗?我很难获得完整的图片。您如何记录您的观点以及如何解决它们?你能举一些使用相同键的视图/视图模型的例子吗?你能详细说明让这些视图模型和模型同步吗?我不清楚其中的原因。
  • 这有点相关(或至少很有趣):stackoverflow.com/questions/15123515/…
  • 感谢史蒂文的快速回复。请参阅更新的问题。希望它包含您要求的信息。
  • 您的IViewModelLocator 实现如何?这些视图模型是在哪里创建的?他们在 Simple Injector 中注册了吗?
  • @Steven:是的,视图模型已在 Simple Injector 中注册。我已经通过实施更新了问题。 ModelLocator 几乎是 ViewModelLocator 的精确副本。

标签: c# wpf dependency-injection service-locator simple-injector


【解决方案1】:

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

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

为视图和视图模型定义一个抽象:

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&lt;TView, TModel&gt; 抽象,因此当有新键进入时,它们可以重新加载视图:

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&lt;TView, TModel&gt; 的实现可能如下所示:

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&lt;TModel&gt; 抽象。这基本上是您的旧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 Injector 将为您批量注册它们。使用RegisterOpenGeneric,您可以指定一个开放通用抽象,并告诉 Simple Injector 在请求该抽象的封闭通用版本时使用哪个实现。最后一行搜索您的应用程序以查找所有控制器类型并将它们注册到系统中。

【讨论】:

  • 非常感谢您的出色回答。这应该有效。正在尝试立即实施!
猜你喜欢
  • 2018-05-08
  • 2016-06-07
  • 1970-01-01
  • 1970-01-01
  • 2014-11-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多