Constructor injection of a View Model instance used as an Action method parameter
Asked Answered
A

1

7

When a view model is created you can populate the options (e.g. used in a dropdown list) into a setter property of the view model. The problem is that when that view model is later passed as a parameter (by the framework!) into an action method, those property values has not become automagically repopulated, so if you need to redisplay the form because of validation errors, you need to repopulate those options again.

One potential solution, which I am asking for specifically in this question, is how to make the MVC framework instantiate the view model with constructor injection, which would provide the view model constructor with an implementation of some kind of data access object (e.g. a repository) that can be used for retrieving the options when they are requested by the view (e.g. in the helper method "DropDownListFor") ?

I think the solution might have something to do with implementations of IModelBinderProvider or IModelBinder but after having experimented with these things from example code snippets here and there on the net, I am still looking for a completely working example, with downloadable executable code without any missing piece of how putting all things together.

If you are looking for some alternative discussion about how to populate a select list, e.g. with "Dependecy Lookup" instead of "Dependecy Injection" you may want to check out the following discussion: Best way to populate SelectList for ViewModel on GET/POST Best way to populate SelectList for ViewModel on GET/POST

Some days ago I wrote the following follow-up-question in that thread about the "Dependecy Injection" I am now looking for in this thread: https://mcmap.net/q/1482700/-best-way-to-populate-selectlist-for-viewmodel-on-get-post (which provides a code example about the problem I am looking for a solution of)

But instead of hoping that someone will find that old thread with a less specific title, I have created this new question with a more specific subject about what I am looking for. And I will also provide a link from that thread into this new question for anyone that want to follow-up regarding this specific solution I am looking for.

Anastasio answered 1/1, 2012 at 12:43 Comment(0)
P
10

I'm assuming you want to have your ViewModels automatically injected with something via their Constructor - for example some kind of configuration object that the View will use to determine what to show. I'm also assuming that this approach is causing a "No parameterless constructor defined for this object" error when MVC tries to automatically create and bind a model instance, from the arguments of your Controller Action. Let's also then assume that we will use a DI framework to inject the SiteConfig object into our Controllers automatically at runtime.

This means that the only problem we have to solve is how to get the injected object from our Controller into its Actions' ViewModels when they are automatically bound.

So let's define a base model for others to inherit from.

BaseViewModel

public class BaseViewModel
{
    public ISiteConfig SiteConfig { get; set; }

    public BaseViewModel(ISiteConfig siteConfig)
    {
        this.SiteConfig = siteConfig;
    }
}

And now let's create a model that inherits from it.

IndexViewModel

public class IndexViewModel : BaseViewModel
{
    public string SomeIndexProperty { get; set; }

    public IndexViewModel (ISiteConfig siteConfig) : base(siteConfig) {}
}

And now let's define a Base Controller that our Controllers will inherit from.

BaseController

public abstract class BaseController : Controller
{
    protected BaseController(ISiteConfig siteConfig)
    {
        _siteConfig = siteConfig;
    }

    private readonly ISiteConfig _siteConfig;

    public ISiteConfig SiteConfig
    {
        get
        {
            return _siteConfig;
        }
    }
}

Now we define our actual controller.

HomeController

public HomeController: BaseController
{
    public HomeController(ISiteConfig siteConfig): base(siteConfig) {}
}

Assuming we're using Ninject for DI, Ninject would be configured to automatically create the Controller and pass a concrete ISiteConfig object into its Constructor at runtime.

Now we add our Action to the Controller.

Index Action

public ActionResult Index(IndexViewModel model)
{
    return View(model);
}

And so this is the point where without doing anything else, MVC will explode with a "Parameterless Constructor" error if you try to call the Index Action, because MVC can't find a ViewModel constructor that takes no arguments.

And so, the answer. We need to override the default ModelBinder.

BaseViewModelBinder

public class BaseViewModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        if (modelType == typeof(BaseViewModel) || modelType.IsSubclassOf(typeof(BaseViewModel)))
        {
            var baseControl = controllerContext.Controller as BaseController;
            if (baseControl == null)
            {
                throw new Exception("The Controller must derive from BaseController");
            }

            var instance = Activator.CreateInstance(modelType, baseControl.SiteConfig);
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => instance, modelType);
            return instance;
        }
        else
        {
            return base.CreateModel(controllerContext, bindingContext, modelType);
        }
    }
}

And we need to set this as the default model binder in global.asax.cs :

protected void Application_Start()
{
    ...
    ModelBinders.Binders.DefaultBinder = new BaseViewModelBinder();
}

That's all. As you can see, when you view the Index Action now, MVC will use our custom model binder. It will realise that the IndexViewModel derives from BaseViewModel, and so will attempt to spin up an IndexViewModel instance using the ISiteConfig it can find in the Action's Controller (because the Controller derives from BaseController).

Perceptible answered 11/6, 2014 at 15:10 Comment(2)
I don't understand exactly what ISiteConfig is. Do I have to implement it?Fanti
ISiteConfig is the interface for the SiteConfig object that you want to be automagically injected into your View. ISiteConfig and SiteConfig refers to a config object of your own making - it's whatever you want to provide to your View, and is just for example.Perceptible

© 2022 - 2024 — McMap. All rights reserved.