1

I'm using MVC.NET 5.2.3 and try to post a model to a controller where the model contains several interfaces. This causes .NET to throw this exception:

Cannot create an instance of an interface

I understand that it's because I use interfaces in my model (ITelephone). My models are as such:

public class AddContactPersonForm
{
    public ExternalContactDto ExternalContact { get; set; }
    public OrganizationType OrganizationType { get; set; }
}

public class ExternalContactDto
{
    public int? Id { get; set; }
    public string Name { get; set; }
    public string Title { get; set; }
    public IList<ITelephone> TelephoneNumbers { get; set; }
}

public interface ITelephone
{
    string TelephoneNumber { get; set; }
}

public class TelephoneDto : ITelephone
{
    public string TelephoneNumber { get; set; }
}

It works fine if I use the TelephoneDto class instead of the ITelephone interface.

I understand that I need to use a ModelBinder, which is fine. But I really just want to say what kind of instance the modelbinder should create, instead of mapping the entire model manually.

The answer @jonathanconway gave in this question is close to what I want to do.

Custom model binder for a property

But I really would like to combine this with the simplicity of simply telling the defaultbinder what type to use for a specific interface. Sort of the same way you can use the KnownType-attribute. The defaultbinder obviously knows how to map the model as long as it knows which class it should create.

How can I tell the DefaultModelBinder what class it should use to deserialize the interface and then bind it? It currently crashes because the model that is posted (AddContactPersonForm) contains a "complex" model (ExternalContactDto) which has the interface ITelephone.

This is what I got so far.

public class ContactPersonController : Controller
{

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult AddContactPerson([ModelBinder(typeof(InterfaceModelBinder))] AddContactPersonForm addContactPersonForm)
    {
        // Do something with the model.
        return View(addContactPersonForm);
    }
}

public class InterfaceModelBinder : DefaultModelBinder
{
    protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext,
        PropertyDescriptor propertyDescriptor)
    {

        var propertyBinderAttribute = TryFindPropertyBinderAttribute(propertyDescriptor);
        if (propertyBinderAttribute != null)
        {
            // Never occurs since the model is nested.
            var type = propertyBinderAttribute.ActualType;
            var model = Activator.CreateInstance(type);
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);

            base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
            return;
        }

        // Crashed here since because:
        // Cannot create an instance of an interface. Object type 'NR.Delivery.Contract.Models.ITelephone'.
        base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
    }

    private InterfaceBinderAttribute TryFindPropertyBinderAttribute(PropertyDescriptor propertyDescriptor)
    {
        return propertyDescriptor.Attributes
          .OfType<InterfaceBinderAttribute>()
          .FirstOrDefault();
    }
}

public class ExternalContactDto
{
    public int? Id { get; set; }
    public string Name { get; set; }
    public string Title { get; set; }
    [InterfaceBinder(typeof(List<TelephoneDto>))]
    public IList<ITelephone> TelephoneNumbers { get; set; }
}

public class InterfaceBinderAttribute : Attribute
{
    public Type ActualType { get; private set; }

    public InterfaceBinderAttribute(Type actualType)
    {
        ActualType = actualType;
    }
}
Community
  • 1
  • 1
smoksnes
  • 10,509
  • 4
  • 49
  • 74

2 Answers2

0

I think that you need to override a CreateModel method of model binder so you could create a model with correctly instantiated properties. You will have to use some reflection in order to dynamically create property values based on InterfaceBinderAttribute.ActualType property. So something like that should work:

protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{      
    var model = Activator.CreateInstance(modelType);
    var properties = modelType.GetProperties(BindingFlags.Public|BindingFlags.Instance);
    foreach(var property in properties)
    {
       var attribute = property.Attributes.OfType<InterfaceBinderAttribute>().FirstOrDefault();
       if(attribute != null)
       {
           var requiredType = (attribute as InterfaceBinderAttribute).ActualType;
           property.SetValue(model, Activator.CreateInstance(requiredType ), null);
       }
    }

    return model;
}
Alex Art.
  • 8,711
  • 3
  • 29
  • 47
  • I may have misunderstood your answer, but the problem is that when I use this approach it will iterate through the properties of the viewmodel (AddContactPersonForm), but not the properties of the nested model (ExternalContactDto). CreateModel is only called once, and that is for the viewmodel. My interface is in a class that resides in a property of the viewmodel. – smoksnes Jan 11 '16 at 13:30
  • In this model binder you create `AddContactPersonForm` object then loop through all it's properties and those properties that have `InterfaceBinderAttribute` on them will be instantiated with concrete type based on the `InterfaceBinderAttribute`. It is indeed called only once – Alex Art. Jan 11 '16 at 13:34
0

I think you have missed something:

The ViewModel is a non-visual class

All domain type concerns should be mapped to ViewModel classes. there should be no need for interfaces in your ViewModels as they should all be specific to the page/view that you are rendering.

If you plan to have the same data on multiple pages - You can still use classes - just use partial views, or child actions.

If you need more complex Model's then use inheritance or composition techniques .

Just use classes to avoid this issue altogether.

MichaelLake
  • 1,735
  • 14
  • 17
  • This is based on MVVM but the same principles apply - http://stackoverflow.com/questions/13156040/nice-and-simple-definition-of-wpfs-mvvm – MichaelLake Jan 11 '16 at 13:33
  • Yes, in this case the AddContactPersonForm-class is my viewmodel. However the viewmodel contains a reference to a domain-class (ExternalContactDto). One can discuss if this is the best approach or not, but in my project we don't allow third-party solutions like automapper, so I try to avoid using mappers for everything. However, you're point is correct. The problem is solved by using a more concrete viewmodel. – smoksnes Jan 11 '16 at 13:40
  • If you are seriously considering this perhaps this will help, This example creates a custom Property Binder, and uses IOC to inject a component that can provide a value for the property. (This will also help with other properties that you have not yet come across) http://aboutcode.net/2011/03/12/mvc-property-binder.html – MichaelLake Jan 11 '16 at 14:14
  • Thank you, but after futher consideration I decided that you are right. This will come back and bite me later. My viewmodels should not reference the domain-models and thus there's no need for an interface. – smoksnes Jan 11 '16 at 14:34
  • Good call - I must admit it is a shame you cant use a mapper - But non the less you can map them your self. Good Luck! – MichaelLake Jan 11 '16 at 14:36