1

I Have a ViewModel that has an interface as a property. When I submit the page, I got the "Cannot create an instance of Interface" error.

The ViewModel is like this:

public class PlanoPagamentoViewModel
{
     //some properties
     public IPlanoPagamentosParcelas PlanoPagamentosParcelas { get; set; }     
}

There're two classes that implement this Interface. The corresponding ViewModels are dinamically loaded with a PartialView, depending on the option that is selected.

public class PlanoPagamentoCartaoViewModel : IPlanoPagamentosParcelas
{
   //some properties
}

public class PlanoPagamentoCrediarioViewModel : IPlanoPagamentosParcelas
{
   //some properties
}

I did a research and I found that the need of creating a custom model binding, and I did that:

public class PlanoPagamentoParcelasBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        var type = typeof(PlanoPagamentoCartaoViewModel);
        var model = Activator.CreateInstance(type);
        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);

        return model;
    }
}

And add this new custom binding in Global.asax, Application_Start method:

ModelBinders.Binders.Add(typeof(IPlanoPagamentosParcelas), new PlanoPagamentoParcelasBinder());  

It works well for PlanoPagamentoCartaoViewModel, but I would need to have another custom bindings for the PlanoPagamentoCrediarioViewModel, but I can't just add a new ModelBinders.Binders.Add with the same Key (IPlanoPagamentosParcelas) because there's already one key with this type.

So, is there any other approach to create a custom model binding for ViewModels that implement the same interface?

Maturano
  • 951
  • 4
  • 15
  • 42
  • change typeof(IPlanoPagamentosParcelas) to typeof(PlanoPagamentoParcelasBinder) or typeof(DefaultModelBinder) – Vivek Nuna Nov 04 '16 at 16:27
  • @viveknuna I got the same error. I would need to add the IPlanoPagamentosParcelas for the two ViewModels, but it's not possible since it is a dictionary – Maturano Nov 04 '16 at 16:33

1 Answers1

2

I believe that the solution is as follow:

protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
        var typeValueProvider = bindingContext.ValueProvider.GetValue("Type");

        var type = (int)typeValueProvider.ConvertTo(typeof(int));

        Type instanceType = null;

        switch (type)
        {
            case 1:
                instanceType = typeof(PlanoPagamentoCartaoViewModel );
                break;

            case 2:
                instanceType = typeof(PlanoPagamentoCrediarioViewModel );
                break;
        }

        if (!typeof(IPlanoPagamentosParcelas).IsAssignableFrom(instanceType))
        {
            throw new InvalidOperationException("Bad Type");
        }
        var model = Activator.CreateInstance(instanceType);
        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, instanceType);
        return model;
}

In the above code var typeValueProvider = bindingContext.ValueProvider.GetValue("Type"); Type would be something to decide which concrete class to instantiate. I´ve tested this solution and it worked for me. It can be improved to be more extensible (removing the switch for something else maybe) but I´ve just tried to make it work.

Here is from where I based my question (maybe you can get more information from there) Polymorphic model binding and below is the code that I created to simulate this scenario (just in case of doubts):

MODELS:

public class NewVehicleViewModel
{
    public string Type { get; set; }

    public VehicleViewModel Vehicle { get; set; }
}

public interface VehicleViewModel
{
    string Name { get; set; }
    string Color { get; set; }
}

public class CarViewModel : VehicleViewModel
{
    public string Color { get; set; }

    public string Name { get; set; }

    public string Brand { get; set; }
}

public class TankViewModel : VehicleViewModel
{
    public string Color { get; set; }

    public string Name { get; set; }

    public string Weapon { get; set; }
}

BINDER:

public class VehicleBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        var typeValueProvider = bindingContext.ValueProvider.GetValue("Type");

        var type = (int)typeValueProvider.ConvertTo(typeof(int));

        Type instanceType = null;

        switch (type)
        {
            case 1:
                instanceType = typeof(CarViewModel);
                break;

            case 2:
                instanceType = typeof(TankViewModel);
                break;
        }

        if (!typeof(VehicleViewModel).IsAssignableFrom(instanceType))
        {
            throw new InvalidOperationException("Bad Type");
        }
        var model = Activator.CreateInstance(instanceType);
        bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, instanceType);
        return model;
    }
}

CONTROLLER:

public class VehiclesController : Controller
{
    // GET: Vehicles
    public ActionResult Index()
    {
        return View();
    }

    [HttpPost]
    public ActionResult Create(NewVehicleViewModel vm)
    {
        return View();
    }
}

GLOBAL.ASAX:

    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        ModelBinders.Binders.Add(typeof(VehicleViewModel), new VehicleBinder());
    }

VIEW (Index.cshtml):

@{
    ViewBag.Title = "New Vehicle";
}


@using (Html.BeginForm("Create", "Vehicles"))
{
    <label>Type</label>
    <input name="type" type="text" />

    <label >Name</label>
    <input name="Vehicle.Name" type="text"/>

    <label>Color</label>
    <input name="Vehicle.Color" type="text" />

    <label>Weapon</label>
    <input name="Vehicle.Weapon" type="text" />

    <label>Brand</label>
    <input name="Vehicle.Brand" type="text" />

    <input type="submit" />
}

Regards.

Community
  • 1
  • 1
dime2lo
  • 826
  • 8
  • 15
  • very interesting approach, but I'm getting NullRecerenceException on: var typeValueProvider = bindingContext.ValueProvider.GetValue("Type"); I'm sorry for my newbie question, but do I need to change the "Type" for something else? – Maturano Nov 07 '16 at 11:35
  • I tried also to change "Type" to bindingContext.ModelName, but it also returns null – Maturano Nov 07 '16 at 12:03
  • Type is just a property used so can be decided which class to instantiate. It could be something else, but you have to decide, somehow which concrete class you will instantiate. You could have used boolean property, maybe `IsPlanoPagamentoCartao`, place it as a hidden field at the view, and in the modelbinder use this propert `bindingContext.ValueProvider.GetValue("IsPlanoPagamentoCartao");` to decide which instante to create. – dime2lo Nov 07 '16 at 12:51
  • Alright man, that's what I'm going to do then, thank you for helping – Maturano Nov 07 '16 at 12:57