How to Bind a collection of Abstract Class or Interface to a Model – ASP.NET MVC

Default Model binder is a very neat and slick piece of functionality, that automatically parses the Request.Form fields and maps the Form Fields to our Action parameters, mostly in case of POST.

In most of the scenario’s it will work pretty well,  but it some of the cases it will not (Obviously !!!). So when it works out well we don’t have a problem, but if not then we are in trouble. Remember we are in trouble, but not a dead end. One thing about ASP.NET MVC is that it’s heavily customizable. But then we also need to know what to customize. In this case when the default model binder doesn’t work, we have to write our own ModelBinder.

Problem Statement: Let’s assume that we an Abstract Base class “Animal” and there are three concrete classes, namely Cat, Dog and Tiger which inherits from Animal class.

The Animal Class is defined as

   1: public abstract class Animal
   2:     {
   3:         public Animal()
   4:         {
   5:         }
   6:  
   7:         public string Name { get; set; }
   8:         public string Color { get; set; }
   9:         public int Legs { get; set; }
  10:         public abstract string Shout();
  11:         public abstract string AnimalType { get;}
  12:     }

And the three derived classes are defined as

   1: public abstract class Animal
   2:     {
   3:         public Animal()
   4:         {
   5:         }
   6:  
   7:         public string Name { get; set; }
   8:         public string Color { get; set; }
   9:         public int Legs { get; set; }
  10:         public abstract string Shout();
  11:         public abstract string AnimalType { get;}
  12:     }
  13:  
  14: public class Cat : Animal
  15:     {
  16:         private string m_animalType = "Cat";
  17:  
  18:         public Cat()
  19:         {
  20:             this.Color = "Bluish Gray";
  21:             this.Name = "Tom";
  22:             this.Legs = 4;
  23:         }
  24:  
  25:         public override string Shout()
  26:         {
  27:             return (".");
  28:         }
  29:  
  30:         public override string AnimalType
  31:         {
  32:             get { return (m_animalType); }
  33:         }
  34:     }
  35:  
  36: public class Dog : Animal
  37:    {
  38:        private string m_animalType = "Dog";
  39:  
  40:        public Dog()
  41:        {
  42:            this.Color = "Gray";
  43:            this.Name = "Spike";
  44:            this.Legs = 4;
  45:        }
  46:        public override string Shout()
  47:        {
  48:            return("..");
  49:        }
  50:  
  51:        public override string AnimalType
  52:        {
  53:            get
  54:            {
  55:                return (m_animalType);
  56:            }
  57:        }
  58:    }
  59:  
  60:  
  61: public class Tiger : Animal
  62:     {
  63:         private string m_animalType = "Tiger";
  64:  
  65:         public Tiger()
  66:         {
  67:             this.Color = "White and Black Stripes";
  68:             this.Name = "Hobbes";
  69:             this.Legs = 4;
  70:         }
  71:         public override string Shout()
  72:         {
  73:             return ("....");
  74:         }
  75:  
  76:         public override string AnimalType
  77:         {
  78:             get { return (m_animalType); }
  79:         }
  80:     }

So now we will Bind the view with a List<Animal>. So the Action will look like following:

   1: public ActionResult Index()
   2:         {
   3:             List<Animal> _lst = AnimalFactory.GetMyPets();
   4:             return View(_lst);
   5:         }

Now let’s say our View is a Bulk Edit screen, which looks like…

image

Now if we modify any value and try to Save it, it will call the “Save” controller.

   1: [AcceptVerbs(HttpVerbs.Post)]
   2: public ActionResult Save(IList<Animal> myPets)

But here if we try to run it it will throw an Exception,

image

Which is expected, because after all you can’t instantiate an abstract class. So here we require some mechanism, so that we can create instances of the concrete classes, and recreate the List. A custom Model binder will help us to do that.

First step to create a Model Binder is to create a class which Implements IModelBinder interface.

So my Model Binder will look like…

   1: public class AnimalModelBinder : IModelBinder
   2:     {
   3:         #region IModelBinder Members
   4:  
   5:         object IModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   6:         {
   7:             string typeKey = bindingContext.ModelName + ".Type";
   8:             string animalTypeName = GetValue<string>(bindingContext, typeKey);
   9:             Animal tempAnimal = Activator.CreateInstance(Assembly.GetAssembly(typeof(Animal)).GetName().FullName,animalTypeName).Unwrap() as Animal;
  10:             
  11:             typeKey = bindingContext.ModelName + ".Name";
  12:             tempAnimal.Name = GetValue<string>(bindingContext, typeKey);
  13:  
  14:             typeKey = bindingContext.ModelName + ".Color";
  15:             tempAnimal.Color = GetValue<string>(bindingContext, typeKey);
  16:  
  17:             typeKey = bindingContext.ModelName + ".Legs";
  18:             tempAnimal.Legs = GetValue<int>(bindingContext, typeKey);
  19:             return (tempAnimal);
  20:         }
  21:  
  22:         private T GetValue<T>(ModelBindingContext bindingContext, string key)
  23:         {
  24:             ValueProviderResult valueResult;
  25:             bindingContext.ValueProvider.TryGetValue(key, out valueResult);
  26:             bindingContext.ModelState.SetModelValue(key, valueResult);
  27:             return (T)valueResult.ConvertTo(typeof(T));
  28:         }  
  29:  
  30:         #endregion
  31:     }

 

GetValue method I have copied from this Post.

And to get the concrete type of each of the item of the List<Animal>, I have included and hidden field in the View.

<%
   = Html.TextBox("myPets[" + i + "].Type",item.GetType(),new {style="display:none"}) 
%>

So I am getting the Type from “myPets[i].Type” field. Once I have the Type, I am using Reflection to create object of that class, and assigning the properties.

That’s all !!!

4 Comments

  • Try this:

    public class TestModelBinder : DefaultModelBinder
    {
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType)
    {
    if (modelType.IsInterface || modelType.IsAbstract)
    {
    if (bindingContext.ValueProvider.ContainsKey(bindingContext.ModelName + ".BindingType"))
    {
    modelType = System.Type.GetType(((string[])bindingContext.ValueProvider[bindingContext.ModelName + ".BindingType"].RawValue)[0]);
    }
    }
    return base.CreateModel(controllerContext, bindingContext, modelType);
    }
    }

    in conjunction with this in your view (instituteDetails is the name of my collection IList):



    Which is a more portable solution and you override any existing model binder and add this behaviour.

  • I don't know if this is easier, but I used StructureMap as my IoC container and created a custom modelbinder, where I resolved the interface and passed it to the default model binder....
    /*-----------------------------------*/
    public class SmartModelBinder : DefaultModelBinder
    {
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
    try
    {
    bindingContext.ModelType = (ObjectFactory.Resolve(bindingContext.ModelType)).GetType();
    }
    catch(Exception)
    {

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

    /*-----------------------------------*/

  • I like the solution given by worlspawn; it Seems simpler and more portable.
    Any contra against it?

  • I also like the solution given by worldspawn[]. Looks like it will work if I add an override on GetModelType() (so that BindModel() iterates over the full set of properties on the derived class).

    Here's my version (.NET4/MVC2):

    public class BetterModelBinder : DefaultModelBinder
    {
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
    return base.CreateModel(controllerContext, bindingContext, GetModelType(controllerContext, bindingContext, modelType));
    }

    protected override ICustomTypeDescriptor GetTypeDescriptor(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
    var modelType = GetModelType(controllerContext, bindingContext, bindingContext.ModelType);
    return new AssociatedMetadataTypeTypeDescriptionProvider(modelType).GetTypeDescriptor(modelType);
    }

    private static Type GetModelType(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
    if (modelType.IsInterface || modelType.IsAbstract)
    {
    var key = bindingContext.ModelName + ".AssemblyQualifiedName";
    if (bindingContext.ValueProvider.ContainsPrefix(key))
    {
    modelType = Type.GetType(((string[])bindingContext.ValueProvider.GetValue(key).RawValue)[0]);
    }
    }
    return modelType;
    }
    }

Comments have been disabled for this content.