Resolving a class with a custom parameter in Simple Injector

与世无争的帅哥 提交于 2019-12-10 10:22:41

问题


I'm creating a WPF MVVM application using Simple Injector as DI container. Now I'm having some issues when I'm trying to resolve a view from Simple Injector, because I'm in need of passing a parameter into my constructor at construction time (not when registering the view to the container, thus this is not applicable: Simple Injector pass values into constructor).

What I'm after is something like this:

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

I've read several places that this is not possible in Simple Injector because it should not be done (including here: https://simpleinjector.codeplex.com/discussions/397080).

Is this true, and if so, how could I do this instead?


Background information

I have a collection of multiple view models and models which are looked up by a specific key, and the parameter I want to pass into the view is the key for the view model to use. I've found this necessary because the view models and the models are used in multiple locations of the application, and need to stay in sync / be the same instances if they have the same key. I don't think I'm able to use lifetime scope to solve this, and there is no way I know the keys when I'm registering to the container. I've also understood that the ViewModelLocator approach might be the ServiceLocator (anti-?)pattern, however, currently it is the best I've got.

My constructor currently looks like this, and I want the IViewModelLocator to be resolved, while I pass in the key:

public FillPropertiesView(IViewModelLocator vml, object key)
{
    // .. Removed code

    // Assign the view model
    var viewModel = vml.GetViewModel<FillPropertiesViewModel>(key);
    DataContext = viewModel;
}

The IViewModelLocator looks like the following (and a similar interface exists for models).

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

Now I have the following questions:

  • What is the best way to be able to resolve the view with the view model key?
  • Do I have to do some refactoring to enable this?
  • Am I missing out on some of the power of the DI container since I have created my own dictionary based ViewModelLocator?

Extended information

I have shown the ViewModelLocator above, and the reason I'm using this is to keep blendability (basically it just provides me with design time data when opened in Blend). The runtime view model could have been the same for every instance of the view (not dependent on the key), if I did not have to have different windows opened at the same time (see next paragraph). The issue described above, however, is the same when the ViewModel is fetching a model, and the ModelLocator needs a key to fetch an existing model if it exists.

This is part of a VSTO application targeting PowerPoint (which affects some parts of the design). When an object on screen is selected, a task panel is opened (this is basically the FillPropertiesView explained above). The object that is selected has a key that is supplied to the view, in order for the view to extract the correct view model from the ViewModelLocator. The view model will then get a reference to a model by using a IModelLocator (similar to the IViewModelLocator) and the same key. At the same time, a controller will fetch the model from the ModelLocator by using the same key. The controller listens to change events from the model and updates the objects on screen. The same process is replicated whenever a new object is selected, and at the same time multiple windows could be open that interacts with the same or different objects simultaneously (that is, with multiple task panes all with unique view models).

Up until now I have resolved the view with a default, parameterless constructor, and then injected the view model with a method call afterwards:

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

I never had to register the view with the container, but resolved the concrete type like this:

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

This issue surfaced when I tried to refactor this so that I did not have to call the SetViewModel() method every and manually resolve the view models etc. It got very messy when I also had to do this manual initiation within the view model to initiate the model in the same way.


ViewModelLocator

The ViewModelLocator is currently working as a wrapper around the DI container, i.e. the view models are registered in Simple Injector.

The registrations are like the following (in a class called CompositionHost):

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

The implementation looks like this:

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

}

回答1:


Injecting a service locator into your classes is (almost) never the way to go, because this disallows compile time checking of dependencies and runtime dependency analysis. For that reason I can also advice to register ALL your root types (such as your views) since otherwise Simple Injector is left in the dark and is not able to advice you about any possible misconfigurations that you might have.

Since you have View + ViewModel pairs that are always cached together, but might depend on Model instance that are reused by multiple View + ViewModel pairs, I suggest the following design.

Define an abstraction for views and view models:

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

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

Define an abstraction for retrieving/caching view by key.

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

With these abstractions your view can look as follows:

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

    public IViewModel<FillPropertiesModel> ViewModel { get; private set; }
}

And your controllers can depend upon the IViewProvider<TView, TModel> abstraction so they can reload the view when a new key is coming in:

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

The implementation for IViewProvider<TView, TModel> could look like this:

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

This implementation depends on a (previously undefined) IModelProvider<TModel> abstraction. This is basically your old ModelLocator, but by using a generic type you can make the implementation much easier, because we can have one instance of this type per TModel (the same holds for ViewProvider), which saves you from having to do things with storing elements with the { Type + key } combination.

You can register this all as follows:

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

With RegisterManyForOpenGeneric you let Simple Injector search the supplied assemblies to look for implementations of the given open generic abstraction and Simple Injector will batch-register them for you. With RegisterOpenGeneric you specify an open-generic abstraction and tell Simple Injector which implementation to use when a close-generic version of that abstraction is requested. The last line searches through your application to look for all controller types and registers them in the system.



来源:https://stackoverflow.com/questions/25767693/resolving-a-class-with-a-custom-parameter-in-simple-injector

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!