views:

231

answers:

3

I'm not sure if this behavior is expected or not, but it seems that custom model binding doesn't work when the binding is assigned to an interface type. Has anyone experimented with this?

public interface ISomeModel {}
public class SomeModel : ISomeModel {}

public class MvcApplication : HttpApplication {
    protected void Application_Start(object sender, EventArgs e) {
        ModelBinders.Binders[typeof(ISomeModel)] = new MyCustomModelBinder();
    }
}

With the above code when I bind to a model of type SomeModel, MyCustomModelBinder is never hit; however, if I change the above code and substitute typeof(ISomeModel) for typeof(SomeModel) and post the exact same form MyCustomModelBinder is called as expected. Does that seem right?

+1  A: 

I'm not sure if its directly related but yes there are things that you need to think about when using model binding and interfaces... I ran into similar problems with the default model binder, but it may not be directly related depending on how you are doing things...

Have a look at the following: ASP.net MVC v2 - Debugging Model Binding Issues - BUG? http://stackoverflow.com/questions/1676731/asp-net-mvc-v2-debugging-model-binding-issues-bug

anthonyv
My inheritance structure is a little different than yours, but it does seem like this may be a related problem. I should think that the model binding system could infer that a type which implements an interface should also be subject to the model binder assigned to that interface so long as there is not another type binding to a more concrete type than the interface (same class, or base class). I guess I can kind of see why that would be a gray area.
Nathan Taylor
That was my feeling too... I expected it just to work but it turns out when working with interfaces in this way in general with asp.net MVC (even though its strictly not an MVC limitation they could allow for it if they wanted to) you are probably going to run into problems/unexpected behavior if your not thinking about these interface issues... hence the gray area...
anthonyv
A: 

I was experimenting with this issue and I came up with a solution of sorts. I made a class called InterfaceModelBinder:

public class InterfaceModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        ModelBindingContext context = new ModelBindingContext(bindingContext);
        var item = Activator.CreateInstance(
            Type.GetType(controllerContext.RequestContext.HttpContext.Request.Form["AssemblyQualifiedName"]));

        Func<object> modelAccessor = () => item;
        context.ModelMetadata = new ModelMetadata(new DataAnnotationsModelMetadataProvider(),
            bindingContext.ModelMetadata.ContainerType, modelAccessor, item.GetType(), bindingContext.ModelName);

        return base.BindModel(controllerContext, context);
    }
}

Which I registered in my Application_Start as so:

ModelBinders.Binders.Add(typeof(IFormSubmission), new InterfaceModelBinder.Models.InterfaceModelBinder());

The interface and a concrete implementation look like this:

public interface IFormSubmission
{
}

public class ContactForm : IFormSubmission
{
    public string Name
    {
        get;
        set;
    }

    public string Email
    {
        get;
        set;
    }

    public string Comments
    {
        get;
        set;
    }
}

The only downside to this whole approach (as you might have gathered already) is that I need to get the AssemblyQualifiedName from somewhere, and in this example it is being stored as a hidden field on the client side, like so:

<%=Html.HiddenFor(m => m.GetType().AssemblyQualifiedName) %>

I'm not certain though that the downsides of exposing the Type name to the client are worth losing the benefits of this approach. An Action like this can handle all my form submissions:

[HttpPost]
public ActionResult Process(IFormSubmission form)
{
    if (ModelState.IsValid)
    {
        FormManager manager = new FormManager();
        manager.Process(form);
    }

    //do whatever you want
}

Any thoughts on this approach?

clonked
Seems like it would do the trick, but as you said, the assembly name in your type seems like you'd be asking for trouble. I'm not presently sure of a good use case of how it could be used maliciously, but it smells funny to me.
Nathan Taylor
Perhaps a solution to this would be hashing or encrypting the type name on the client, and then making sence of that in the model binder.
clonked
A: 

Excellent post Nathan!!! You rule!

wolfsoul13