#TECH

ASP.NET MVC Custom Model Binder with JQuery Collection

Ever faced a situation in ASP.NET MVC where you need to dynamically create multiple instances of derived class objects ( where collection of base class is used as a property in another class), and save the main class in just a single click ?? (This could do wonders for you and save lot of time !!).

One solution could be that each time you make an ajax call and save each instance individually !! Could work !! But you need to have the instance already in hand !! (Time consuming).

Better solution could be that you create instances of the derived classes using JQuery templates and get the derived class type while templating and post the whole form in just a single click !! No ajax call required !!

Here is the whole idea :

I had a class (ApplicationBacker) with collection of other classes as its properties (Asset, Liability, etc.)

ClassDiagram1

Code Snippet
  1. public class ApplicationBacker
  2.   {
  3.       public virtual Int32 Id { get; set; }
  4.       public virtual String FirstName { get; set; }
  5.       public virtual String LastName { get; set; }
  6.       .
  7.       .
  8.       public virtual IList<Asset> Assets { get; set; }
  9.       public virtual IList<Income> Incomes { get; set; }
  10.       public virtual IList<Liability> Liabilities { get; set; }
  11.       public virtual IList<Expense> Expenses { get; set; }
  12.       .
  13.       .
  14.   }
Code Snippet
  1. public class Property : Asset
  2.     {
  3.         // Properties of the class
  4.     }
Code Snippet
  1. public class Mortgage : Liability
  2.     {
  3.         // Properties of the class
  4.     }

 

There was a requirement where I had to create multiple instances of Asset, Liability etc (without making an ajax call) and save the whole form in a single click. So I created a partial view (“_Property”) of these classes:

Code Snippet
  1. @model SimpleMoney.Contracts.Entities.Property
  2. @using SimpleMoney.Main.Utility.CollectionHelper
  3. @using (Html.BeginCollectionItem(“Assets”))
  4. {
  5.    @Html.Hidden(“ModelType”, Model.GetType()) // To get the Derived class type (Property here)
  6.     // Content of View
  7. }

 
Make sure you use the same name in the BeginCollectionItem as your property name in the main class (Assets here). And used these partial view as templates in the main View.

Code Snippet
  1. @model SimpleMoney.Contracts.Entities.ApplicationBacker
  2. .
  3. .
  4. <script type=”text/x-jquery-tmpl” id=”propertyTemplate”>
  5.     @Html.CollectionItemJQueryTemplate(“_Property”, new SimpleMoney.Contracts.Entities.Property())
  6. </script>

 

Now create a CollectionHelper :

Code Snippet
  1. public static class CollectionEditingHtmlExtensions
  2.     {
  3.         ///<summary>
  4.         /// Begins a collection item by inserting either a previously used .Index hidden field value for it or a new one.
  5.         ///</summary>
  6.         ///<typeparam name=”TModel”></typeparam>
  7.         ///<param name=”html”></param>
  8.         ///<param name=”collectionName”>The name of the collection property from the Model that owns this item.</param>
  9.         ///<returns></returns>
  10.         public static IDisposable BeginCollectionItem<TModel>(this HtmlHelper<TModel> html, string collectionName)
  11.         {
  12.             if (String.IsNullOrEmpty(collectionName))
  13.                 throw new ArgumentException(“collectionName is null or empty.”, “collectionName”);
  14.             string collectionIndexFieldName = String.Format(“{0}.Index”, collectionName);
  15.             string itemIndex = null;
  16.             if (html.ViewData.ContainsKey(JQueryTemplatingEnabledKeyDynamic))
  17.             {
  18.                 itemIndex = “${index}”;
  19.             }
  20.             else
  21.             {
  22.                 itemIndex = GetCollectionItemIndex(collectionIndexFieldName);
  23.             }
  24.             string collectionItemName = String.Format(“{0}[{1}]”, collectionName, itemIndex);
  25.             TagBuilder indexField = newTagBuilder(“input”);
  26.             indexField.MergeAttributes(newDictionary<string, string>() {
  27.                 { “name”, collectionIndexFieldName },
  28.                 { “value”, itemIndex },
  29.                 { “type”, “hidden” },
  30.                 { “autocomplete”, “off” }
  31.             });
  32.             html.ViewContext.Writer.WriteLine(indexField.ToString(TagRenderMode.SelfClosing));
  33.             return new CollectionItemNamePrefixScope(html.ViewData.TemplateInfo, collectionItemName);
  34.         }
  35.         private const string JQueryTemplatingEnabledKey = “__BeginCollectionItem_jQuery”;
  36.         private static int counter = 1;
  37.         private static string JQueryTemplatingEnabledKeyDynamic = String.Empty;
  38.         public static MvcHtmlString CollectionItemJQueryTemplate<TModel, TCollectionItem>(thisHtmlHelper<TModel> html, string partialViewName, TCollectionItem modelDefaultValues)
  39.         {
  40.             JQueryTemplatingEnabledKeyDynamic = JQueryTemplatingEnabledKey + counter++;
  41.             ViewDataDictionary<TCollectionItem> viewData = newViewDataDictionary<TCollectionItem>(modelDefaultValues);
  42.             viewData.Add(JQueryTemplatingEnabledKeyDynamic, true);
  43.             MvcHtmlString mvcHtmlString = html.Partial(partialViewName, modelDefaultValues, viewData);
  44.             return mvcHtmlString;
  45.         }
  46.         public static MvcHtmlString ItemJQueryTemplate<TModel , TCollectionItem> ( thisHtmlHelper<TModel> html , string partialViewName , TCollectionItem modelDefaultValues )
  47.             {
  48.             ViewDataDictionary<TCollectionItem> viewData = newViewDataDictionary<TCollectionItem>(modelDefaultValues);
  49.            // viewData.Add(JQueryTemplatingEnabledKeyDynamic , true);
  50.             MvcHtmlString mvcHtmlString = html.Partial(partialViewName , modelDefaultValues , viewData);
  51.             return mvcHtmlString;
  52.             }
  53.         ///<summary>
  54.         /// Tries to reuse old .Index values from the HttpRequest in order to keep the ModelState consistent
  55.         /// across requests. If none are left returns a new one.
  56.         ///</summary>
  57.         ///<param name=”collectionIndexFieldName”></param>
  58.         ///<returns>a GUID string</returns>
  59.         private static string GetCollectionItemIndex(string collectionIndexFieldName)
  60.         {
  61.             Queue<string> previousIndices = (Queue<string>)HttpContext.Current.Items[collectionIndexFieldName];
  62.             if (previousIndices == null)
  63.             {
  64.                 HttpContext.Current.Items[collectionIndexFieldName] = previousIndices = newQueue<string>();
  65.                 string previousIndicesValues = HttpContext.Current.Request[collectionIndexFieldName];
  66.                 if (!String.IsNullOrWhiteSpace(previousIndicesValues))
  67.                 {
  68.                     foreach (string index in previousIndicesValues.Split(‘,’))
  69.                         previousIndices.Enqueue(index);
  70.                 }
  71.             }
  72.             return previousIndices.Count > 0 ? previousIndices.Dequeue() : Guid.NewGuid().ToString();
  73.         }
  74.         private class CollectionItemNamePrefixScope : IDisposable
  75.         {
  76.             private readonly TemplateInfo _templateInfo;
  77.             private readonly string _previousPrefix;
  78.             public CollectionItemNamePrefixScope(TemplateInfo templateInfo, string collectionItemName)
  79.             {
  80.                 this._templateInfo = templateInfo;
  81.                 _previousPrefix = templateInfo.HtmlFieldPrefix;
  82.                 templateInfo.HtmlFieldPrefix = collectionItemName;
  83.             }
  84.             public void Dispose()
  85.             {
  86.                 _templateInfo.HtmlFieldPrefix = _previousPrefix;
  87.             }
  88.         }
  89.     }

 

Also we need to have a ModelBinder which uses the ModelType specified in the PartialView and cast that object in that class

Code Snippet
  1. public class EnhancedDefaultModelBinder : DefaultModelBinder
  2.     {
  3.         public override object BindModel(ControllerContext controllerContext,
  4.       ModelBindingContext bindingContext)
  5.         {
  6.             if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName + “.ModelType”))
  7.             {
  8.                 //get the model type
  9.                 var typeName = (string)bindingContext.ValueProvider
  10.                     .GetValue(bindingContext.ModelName + “.ModelType”).ConvertTo(typeof(string));
  11.                 var modelType = Type.GetType(typeName);
  12.                 //tell the binder to use it
  13.                 bindingContext.ModelMetadata = ModelMetadataProviders.Current
  14.                     .GetMetadataForType(null, modelType);
  15.             }
  16.             return base.BindModel(controllerContext, bindingContext);
  17.         }
  18.     }

 

Dont forget to bind this ModelBinder in the Global.asax.cs

Code Snippet
  1. protected void Application_Start()
  2. {
  3.     ModelBinders.Binders.Add(typeof(Asset), new EnhancedDefaultModelBinder());
  4.     ModelBinders.Binders.Add(typeof(Liability), new EnhancedDefaultModelBinder());
  5. }

 

Now all you need is to add @Html.BeginCollectionItem(“<PropertyName>”) in the PartialView and get the ModelType in the same view.

Now if I want multiple instances of Property class, I just specify the number and then JQuery function uses the Template (propertyTemplate) and creates instances (NO AJAX CALL).

Code Snippet
  1. $(“#someDiv”).append($(“#propertyTemplate”).tmpl({ index: _generateGuid() }));
  2. var _generateGuid = function () {
  3.     return‘xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx’.replace(/[xy]/g, function (c) {
  4.         var r = Math.random() * 16 | 0, v = c == ‘x’ ? r : (r & 0x3 | 0x8);
  5.         return v.toString(16);
  6.     });
  7. }

 
And now when you post the form the whole collection is posted and you get all the instances of the different classes (Property (as Asset) ) created for the main class (ApplicationBacker) in single click.