ASP.NET MVC And Client Side Lists with Kendo

In this post, I’m going to illustrate a very simple way to create client-side list-based UI’s in ASP.NET MVC. The idea with this example is to allow the ability to add a bulk number of list items that doesn’t require constantly posting back to add additional items. In this example, we’ll use Kendo UI Core framework templating capabilities, although any templating framework will do.

To start, let’s look at a very simple model:

public  class SystemTestModel
{
   public List Entities { get; set; }
}

public class SystemListEntity
{
   public string Name { get; set; }
   public string Value { get; set; }
   public bool IsDeleted { get; set; }
}

Here we have a View Model that has a list of child items. Very simple. Notice the entity has 2 properties, and an IsDeleted property, which will be illustrated later. For this example, the controller setup is simple; it reads the form posted values and rebuilds the non-deleted ones:

public ActionResult List()
{
   var model = new SystemTestModel { Entities = new List() };
   return View(model);
}

[HttpPost]
public ActionResult List(SystemTestModel model)
{
   //Remove non-deleted records - deleted from  client
   var entities = model.Entities.Where(i => i.IsDeleted == false);

   //Save entities to DB or other work
   //Reload UI
   return View(new SystemTestModel { Entities = entities.ToList() });
}

Within the view, the Name/Value pairs are rendered

<tbody id="Grid" data-last-index="@(Model.Entities.Count - 1)">
   @for (var i = 0; i < Model.Entities.Count; i++) {
     <tr>
         <td>
            <input type="text" name="@Html.NameFor(m => m.Entities[i].Name)" value="@Model.Entities[i].Name" />
            <input type="hidden" name="@Html.NameFor(m => m.Entities[i].IsDeleted)" value="@Model.Entities[i].IsDeleted" />
         </td>
         <td>
            <input type="text" name="@Html.NameFor(m => m.Entities[i].Value)" value="@Model.Entities[i].Value" />
         </td>
      </tr>
   }
</tbody>

Each server-side item is rendered using the collection-based naming syntax that MVC uses to identify collections. The name for a collection-based item looks like the server-side equivalent: Entities[X].[Field] (ie. Entities[0].Name). This is important because our client-side HTML template must do the same thing:


   <tr>
      <td>
          
          <input type="hidden" name="@Html.NameFor(m => m.Entities[-99].IsDeleted)" value="@Boolean.FalseString" />
      </td>
      <td>
          <input type="text" value="#= Value #" name="@Html.NameFor(m => m.Entities[-99].Value)" />
      </td>
   </tr>

Notice in the template the -99; the really neat thing about the NameFor helper is that the expression doesn’t need to be valid; -99 works and literally renders to the HTML as “Entities[-99].Name”. Notice that the script block is the same equivalent above, but will be used to render client-side additional elements. The HTML between the two doesn’t need to be exact, but similar. The view will use this template when the “Add” button is clicked.

What that really means is that the server may have rendered 2 name/value items, and the client can render additional pairs. Our approach is to ensure that we render the pairs in sequential order whether created from server or client, preserving that sequential order.

The view has an add button. The add button triggers the templating capability of kendo. The idea with the template is to get the HTML for the entry and replace the “-99” with the actual index. So if the server-side rendering produced 2 elements (using indexes 0 and 1), the client-side “Add” button generates items starting from index “2” and greater.

$(function () {
   //http://docs.telerik.com/kendo-ui/framework/templates/overview
   $("#NewButton").on("click", function (e) {
      var html = $("#ItemTemplate").html();
      //Get the last index, add 1 because we are adding an item at the new index
      var index = $("#Grid").data("last-index") + 1;
      $("#Grid").data("last-index", index);

      //Replace -99 with the new Index
      html = html.replace(/-99/g, index);

      //Can be used to apply data values from JS
      var template = kendo.template(html);
      var data = {}; //For now, not doing any template binding
      var content = template(data);
            
      $("#Grid").append(content);
   });
});

The first step here is get the templated HTML, and update the current index appropriately. When this UI posts back, the updated index sequence posts back the new items correctly at positions 2 and greater. When the UI reloads, we now have server-side items created from the new index, and new items can be added again.

You may have noticed the IsDeleted property; this can be used to indicate items as deleted. The UI can have a delete button, which can trigger JavaScript that can hide the entire TR tag of the item from view and update the hidden field. When posted back, a permanent delete can happen (purge from the DB if it was originally persisted).

The main goal of this approach is bulk entry of lists without having to postback to add each item, like web forms used to do.

Here is the entire View (assumes JQuery and Kendo scripts included):

@model SystemTestModel

<form action="" method="post">

   <div class="row">
      <div class="col-md-12">

         <table class="table table-bordered table-hover">
            <thead>
               <tr>
                  <th>Name
                  <th>Value
               </tr>
            </thead>
            <tbody id="Grid">
               @for (var i = 0; i < Model.Entities.Length; i++)
               {
                    <tr>
                      <td>
                         <input type="text" name="@Html.NameFor(m => m.Entities[i].Name)" value="@Model.Entities[i].Name" />
                          <input type="hidden" name="@Html.NameFor(m => m.Entities[i].IsDeleted)" value="@Model.Entities[i].IsDeleted" />
                        </td>
                         <td>
                             <input type="text" name="@Html.NameFor(m => m.Entities[i].Value)" value="@Model.Entities[i].Value" />
                           </td>
                          </tr>

               }
            </tbody>
         </table>

      </div>
   

   <div class="row">
      <div class="col-md-12">

         <button type="submit" name="Action" value="SAVE" class="btn btn-primary">
             Save 
         </button>
         <button type="button" name="Action" value="NEW" class="btn btn-default">
             New
         </button>

      </div>
   </div>

</form>

@section scripts {
   
      <script type="text/x-kendo-template">
               <tr>
                    <td>
                        <input type="text" value="#= Name #" name="@Html.NameFor(m => m.Entities[-99].Name)" />
                        <input type="hidden" name="@Html.NameFor(m => m.Entities[-99].IsDeleted)" value="@Boolean.FalseString" />
                    </td>
                     <td>
                        <input type="text" value="#= Value #" name="@Html.NameFor(m => m.Entities[-99].Value)" />
                     </td>
              </tr>
      </script>

<script type="text/javascript">
      $(function () {

         //http://docs.telerik.com/kendo-ui/framework/templates/overview
         $("#NewButton").on("click", function (e) {
            var html = $("#ItemTemplate").html();
            //Get the last index, add 1 because we are adding an item at the new index
            var index = $("#Grid").data("last-index") + 1;
            $("#Grid").data("last-index", index);

            //Replace -99 with the new Index
            html = html.replace(/-99/g, index);

            //Can be used to apply data values from JS
            var template = kendo.template(html);
            var data = {}; //For now, not doing any template binding
            var content = template(data);
            
            $("#Grid").append(content);
         });

      });
   </script>

}

And Controller:

public class SystemListEntity
   {
      public string Name { get; set; }

      public string Value { get; set; }

      public bool IsDeleted { get; set; }

   }


    public class SystemController : BaseController
    {

         public ActionResult List()
         {
            var model = new SystemTestModel { 
                          Entities = new List<SystemListEntity>() };

            return View(model);
         }

         [HttpPost]
         public ActionResult List(SystemTestModel model)
         {
            var entities = model.Entities.Where(i => i.IsDeleted == false);

            //Save entities

            //Reload UI
            return View(new SystemTestModel { Entities = entities.ToList() });
         }
}

Using Interfaces for ASP.NET MVC Model Binding

Sometimes you may want to support view reuse, where a partial view displays a portion of the application in a reusable fashion. Suppose you had the following entity which has a bunch of properties.

public class Customer
{
   public int CustomerID { get; set; }

   public string FirstName { get; set; }

   public string LastName { get; set; }

   public string AccountNumber { get; set; }

   .
   .
}

Suppose you wanted to use a PartialView because it was an AJAX widget with a JavaScript initialization script, and defining a reusable control made it really easy to support this. MVC defines a strongly typed model reference, so does that mean we have to bind the model directly to Customer, or define a custom type for an editor template?

We actually can use interfaces by adding the following:

public interface IAccountEntity
{
   string AccountNumber { get; set; }
}

We can define this in a partial view:

@model IAccountEntity

@Html.TextBoxFor(i => i.AccountNumber)

   $(function() {
      /* Registration Script */
   });

And then use it in our EditCustomer view, as a partial with the existing model:

@Html.Partial("EditAccountNumber", Model)

Since Model (of type Customer) is also IAccountEntity, this translates just fine with MVC. It works just as well when you use hierarchies too. For instance, Customer implements IAccountEntity, but what if we had a CustomerModel class above Customer like:

CustomerModel
     Customer : IAccountEntity
        AccountNumber

If EditCustomer view looks like the following:

@model CustomerModel
.
.

<div class="form-group">
   <label class="col-md-2 control-label">First Name</label>
   <div class="col-md-10">
       @Html.TextBoxFor(Function(i) i.Customer.FirstName)
   </div>
</div>
<div class="form-group">
   <label class="col-md-2 control-label">Last Name</label>
   <div class="col-md-10">
       @Html.TextBoxFor(Function(i) i.Customer.LastName)
   </div>
</div>
<div class="form-group">
   <label class="col-md-2 control-label">AccountNumber</label>
   <div class="col-md-10">
       @Html.Partial("EditAccountNumber", Model)
   </div>
</div>

Notice how the model is passed in directly; this won’t work for our scenario, because the EditAccountNumber expects an IAccountEntity, and requires that the partial be passed in this way:

@Html.Partial("EditAccountNumber", Model.Customer)

However, that doesn’t work for our needs, if our controller has a post operation of the following:

[HttpPost]
ActionResult Edit(CustomerModel model)
{
    if (string.IsNullOrEmpty(model.AccountNumber))
    {
      //This will always be the case, why?  See below
    }
}

Welcome to one of the challenges of MVC model binding. In the previous form, the first/last name and account number would post back to the server something like:

Customer__FirstName: BOB
Customer__LastName: SMITH
AccountNumber: ABC123

What needs to happen is the “AccountNumber” key also needs to be Customer__AccountNumber for it to successfully bind back to the server. You can add AccountNumber as a parameter to the post method, or we can add another interface that supports our hierarchy, and add this interface to the CustomerModel:

//Interface definition
public interface IAccountNumberModel
{
    IAccountNumberEntity Customer { get; set; }
}

//Update to the model class
public class CustomerModel : IAccountNumberModel
{

   public Customer Customer { get; set; }

   IAccountNumberEntity IAccountNumberModel.Customer { get; set; }

}

Here IAccountNumberModel has the same property name as CustomerModel; that’s important to make sure the model binding name structure matches. Now we can pass in the CustomerModel to the EditAccountNumber view with these modifications:

//EditAccountNumber
@model IAccountNumberModel

@Html.TextBoxFor(i => i.Customer.AccountNumber)
.
.

//EditCustomer:
.
.
@Html.Partial("EditAccountNumber", Model)

Since CustomerModel implements IAccountNumberModel, and this matches the type of model on our partial view, everything binds correctly.