3

I have an action:

[HttpPost]
public ActionResult(Animal a)
{

}

I'd like a to be a Rabbit or Dog depending on the incoming form data. Is there a simple way to achieve this?
Thank you

LINQ2Vodka
  • 2,996
  • 2
  • 27
  • 47

1 Answers1

4

In order to get this to work, you are looking at setting up your Action to accept a dynamic parameter, that a ModelBinder will convert to the appropriate type, either a Rabbit or Dog:

[HttpPost]
public ActionResult([ModelBinder(typeof(AnimalBinder))] dynamic a)
{

}

Since the Action doesn't know what the object is that it is getting, it needs a way of knowing what to convert that object to. You will need two things to achieve this. First, you have to embed in your View, EditorTemplate, whatever, what the model that you are binding to is:

@Html.Hidden("ModelType", Model.GetType())

Second, the model binder that will create an object of the appropriate type, based on the ModelType field you specified above:

public class AnimalBinder : DefaultModelBinder
    {
        protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
        {
            var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
            var type = Type.GetType((string)typeValue.ConvertTo(typeof(string)), true);
            if (!typeof(Animal).IsAssignableFrom(type))
            {
                throw new InvalidOperationException("Bad Type");
            }
            var model = Activator.CreateInstance(type);
            bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
            return model;
        }
    }

Once this is all in place, if you inspect the dynamic a object that is passed into the Action, you will see that it is of type Rabbit or Dog based on what the page model was.

Pete
  • 3,842
  • 3
  • 31
  • 42
  • Thanks for the detailed answer! Yes, this will work for simple cases. Please, can you explain the more complex thing like following. WHat if i have arrays or even nested objects sent by browser to server? Is there a way to make binder "know" how to translate each separate client objects to model types but not to explain how to work with arrays or nested objects? In my case I have an array of "animals" (that are animal[0], animal[1] etc on the form) and each of them can also have nested objects of types that should also be correctly "parsed" in binder? – LINQ2Vodka Apr 10 '14 at 19:14
  • Is this an API? If so, you may benefit from splitting this complex logic into multiple API actions or resources, so that you don't need to rely so much on the model binder. – NWard Apr 10 '14 at 19:16
  • @LINQ2Vodka - Remember that when you post data, it's just HTML form fields... there's nothing special about it. There's no magic. Something has to be able to tell the model binder what kind of object it is, and there's not enough information provided by Http to do this, so you have to build that information into your posted data. – Erik Funkenbusch Apr 10 '14 at 19:19
  • One thing to be aware of is that this could potentially be dangerous and/or a security violation. Imagine where you post a user object. If you can just change the "type" of user to "Administrator" by changing the hidden field, it would be quite bad.... – Erik Funkenbusch Apr 10 '14 at 19:20
  • @ErikFunkenbusch I just wanted the binder to remember its default skills before parsing my model with my rules :) – LINQ2Vodka Apr 10 '14 at 19:21
  • @ErikFunkenbusch you do have a point, there is a risk with spoofing the model type, but there is code builtin to the ModelBinder to at least verify that the incoming object is of a valid type: `if (!typeof(Animal).IsAssignableFrom(type))`. As far as dealing with complex objects, you will have to come up with some way of rebuilding your VM after the POST by including enough information in the your posted data. But I won't get into that here as it is out of scope of the current question. – Pete Apr 10 '14 at 19:59
  • @Pete - I wasn't referring to the case of Animal, I was referring to the general case of allowing untrusted data to control object creation. In the case of a User object where you have a subclass that's an administrator, setting the type to administrator will make the model binder create the user as an administrator. I'm just suggesting that you have to be careful, this is essentially untrusted code and should be treated no differently than, say, a sql injection or similar issue. – Erik Funkenbusch Apr 10 '14 at 20:05