Jul 20 2011

Editing a list of Heterogeneous Types with the Mvc Controls Toolkit

Category: Asp.net | MVCFrancesco @ 04:35


All items controls of the Mvc Controls Toolkit, such as Datagrid, SortableList and TreeView are able to work with items that are subclasses of the class the items collection is strongly typed with. For instance, if in our ViewModel we have a List of Person objects we can pass it a list made of mixed types derived from Person, such as Customer, and Employed: when the view is posted the Model Binder will re-build each object with exactly the same sub-type it had at rendering time. 

However, each derived Type has some property more than its base Type, and we woud like to render also these type-specific properties. The TreeView has different item templates and we can supply a lambda expression to choose for each item and adequate template, so we need simply to make this lambda expression returns a template specific for each type by performing  type check with the is operator.

All other items controls have just one template, so we have to perform our type check within the template and then to invoke another template specific for the derived type:

  1. @if (item.ViewData.Model != null)
  2.     {
  3.         @item.HiddenFor(m => m.Code)
  4.         <div><h3>@(item.ViewData.Model is Employed ? "Employed" : "Customer")</h3></div>
  5.         <div>
  6.             @item.LabelFor(m => m.Name)
  7.             @item.TypedEditDisplayFor(m => m.Name, simpleClick: true)
  8.             @item.ValidationMessageFor(m => m.Name, "*")
  9.         </div>
  10.         <div>
  11.             @item.LabelFor(m => m.SurName)
  12.             @item.TypedEditDisplayFor(m => m.SurName, simpleClick: true)
  13.             @item.ValidationMessageFor(m => m.SurName, "*")
  14.         </div>
  16.         if (item.ViewData.Model is Employed)
  17.         {
  18.             @item.TemplateFor(m => m,
  19.                 _S.L<Employed>(
  20.                    l =>
  21.                     string.Format("<div>{0}{1}{2}</div>",
  22.                     l.LabelFor(m => m.Department),
  23.                     l.TypedEditDisplayFor(m => m.Department, simpleClick:true),
  24.                     l.ValidationMessageFor(m => m.Department, "*")
  26.                 )), typeof(Employed))
  28.         }
  29.         if (item.ViewData.Model is Customer)
  30.         {
  32.             @item.TemplateFor(m => m,
  33.                 _S.L<Customer>(
  34.                    l =>
  35.                     string.Format("<div>{0}{1}{2}</div>",
  36.                     l.LabelFor(m => m.ContactId),
  37.                     l.TypedEditDisplayFor(m => m.ContactId,
  38.                         ChoiceListHelper.Create(
  39.                             Person.ExtractEmployed(Model.AllPersons),
  40.                             m => m.Code,
  41.                                  m => m.SurName), simpleClick: true),
  42.                     l.ValidationMessageFor(m => m.ContactId, "*")
  43.                    )), typeof(Customer))
  44.         }
  45.      }

We first do a type check to write an adequate title, then we render all common properties. Having done this we do the first type check for the Employed type. Then we invoke a template that is specific for the Employed type with the help of the TemplateFor helper. The typeof(Empolyed) Type passed as third (optional) argument to the TemplateFor helper ask this  helper to interpret the original Person object as an Employed.

The template specific for the Employed derived type renders just the properties that are specific for Employed. Since Razor doesn’t allow the nesting of in-line Razor helpers, we implement this simple template with a lambda expression . In case of more complex templates we can just pass a Razor helper we defined previously in the View.

Then we do the same job for the Customer type.

Now we have to face the problem of the addition of a new item: we would like to give to the user the possibility to choose the type of the new item to create. Again, the TreeView supports multiple add buttons each for a different item template, so the user can select the type by pressing a different add button.

For all others items controls we must handle the choice within the template As first step we have to recognize we are rendering a template for a new item, this is easily carried out by checking if the model is null.

Then we can use a ViewList to let the user select among the different insertion forms that are specific for the various derived classes:

  1. @if (item.ViewData.Model == null)
  2.     {
  3.         HtmlHelper<Customer> hCustomer;
  4.         HtmlHelper<Employed> hEmployed;
  6.         <strong>@item.SelectionButton("Customer", "insertCustomer", item.PrefixedId("personType"), item.PrefixedId("customerSelection"), ManipulationButtonStyle.Link)</strong>
  7.         <strong>@item.SelectionButton("Employed", "insertEmployed", item.PrefixedId("personType"), item.PrefixedId("employedSelection"), ManipulationButtonStyle.Link)</strong>
  8.         <div id='@item.PrefixedId("insertCustomer")' class='PersonListItem @item.PrefixedId("personType")'>
  9.             @item.DescendatntCast(m => m).To(out hCustomer)
  10.             <div>
  11.             @hCustomer.LabelFor(m => m.Name)
  12.             @hCustomer.TextBoxFor(m => m.Name)
  13.             @hCustomer.ValidationMessageFor(m => m.Name, "*")
  14.         </div>
  15.         <div>
  16.             @hCustomer.LabelFor(m => m.SurName)
  17.             @hCustomer.TextBoxFor(m => m.SurName)
  18.             @hCustomer.ValidationMessageFor(m => m.SurName, "*")
  19.         </div>
  20.         <div>
  21.             @hCustomer.LabelFor(m => m.ContactId)
  22.             @hCustomer.DropDownListFor(m => m.ContactId,
  23.                         ChoiceListHelper.Create(
  24.                             Person.ExtractEmployed(Model.AllPersons),
  25.                             m => m.Code,
  26.                             m => m.SurName))
  27.             @hCustomer.ValidationMessageFor(m => m.ContactId)
  28.             </div>
  29.         </div>
  30.         <div id='@item.PrefixedId("insertEmployed")' class='PersonListItem @item.PrefixedId("personType")'>
  31.         @item.DescendatntCast(m => m).To(out hEmployed)
  32.         <div>
  33.             @hEmployed.LabelFor(m => m.Name)
  34.             @hEmployed.TextBoxFor(m => m.Name)
  35.             @hEmployed.ValidationMessageFor(m => m.Name, "*")
  36.         </div>
  37.         <div>
  38.             @hEmployed.LabelFor(m => m.SurName)
  39.             @hEmployed.TextBoxFor(m => m.SurName)
  40.             @hEmployed.ValidationMessageFor(m => m.SurName, "*")
  41.         </div>
  42.         <div>
  43.             @hEmployed.LabelFor(m => m.Department)
  44.             @hEmployed.TextBoxFor(m => m.Department)
  45.             @hEmployed.ValidationMessageFor(m => m.Department)
  46.         </div>
  47.         </div>
  48.         @item.ViewList(item.PrefixedId("personType"), "personSelected", "insertCustomer")
  49.     }

Basically we have two divs one containin the insertion form for the Employed, and the other containing the insertion form for the Customer, and the ViewList attaches to the DOM just one div, according to the selection button clicked by the user.

For the details on the use of the VieList, please consult the on-line documentation.

Normally when an item of an items control is rendered, the control writes information about the exact type of the object in the View , to enable the Model Binder to re-build exactly the same type on post. However, in the case of the template for the null element, no type information can be extracted from the object, so the Model Binder is instructed just to create an instance of the base the collection is strongly typed with, in our case an instance of the Person type.

In order to instruct the Model Binder to create exactly the type chosen by the user we have to use the DescendatntCast that has an output parameter that returns an HtmlHelper strongly typed with a derived classes:

  1. @item.DescendatntCast(m => m).To(out hCustomer)

Where hCustomer has been defined previously as:

  1. HtmlHelper<Customer> hCustomer;

The DescendatntCast extension method, returns an helper strongly typed with a subclass of the property passed in the lambda expression, that we can select by adequately typing the hCustomer helper, and the write in the View the information needed by the Model Binder to create the right derived type.

It is fundamental to put each DescendatntCast call within the div that is attached/detached from the DOM by the ViewList. This way, only the set of Model Binder instructions for the selected type are retrieved by the Model Binder since the other divs are detached and accordingly their input fields are not submitted.

Well, for now…that’s all! The full code of the tutorial is contained in the HeterogeneousListEditing file of the download area of the Mvc Controls Toolkit.


                                        Enjoy! Francesco