Monday, February 20, 2012

MVC ListBox using EditorTemplates and KnockoutJS

Editor templates are really handy tool when we want to create controls for a specific type. Recently I had a requiremnt to show winform styled ListBox control with lists for Available/Selected and allowing user to move items from one collection to other and move items up/down in selected collection. It was similar to this:
Step 1: Considering this as a pretty common functionality in our application,I created a ListSource class to support this UI.
MvcLists\Models\ListSource.cs
  1. using System.Collections.Generic;
  2.  
  3. namespace MvcLists.Models
  4. {
  5.     public class ListSource
  6.     {
  7.         public List<Item> Available { get; set; }
  8.         public List<Item> Selected { get; set; }
  9.     }
  10.  
  11.     public class Item
  12.     {
  13.         public int Value { get; set; }
  14.         public string Text { get; set; }
  15.     }
  16. }
Step 2: Add a property of type ListSource to Model.
MvcLists\Models\Person.cs
  1. public class Person
  2.     {
  3.         [Tooltip("Enter First Name")]
  4.         public string FirstName { get; set; }
  5.         [Tooltip("Enter Last Name")]
  6.         public string LastName { get; set; }
  7.         [Tooltip("Enter SSN")]
  8.         [Mask("999-99-9999")]
  9.         public string SSN { get; set; }
  10.         [Tooltip("Enter Age")]
  11.         [Mask("99")]
  12.         public string Age { get; set; }
  13.         [Mask("(999)-999-9999")]
  14.         [Tooltip("Enter Phone")]
  15.         public string Phone { get; set; }
  16.         [Mask("99999?-9999")]
  17.         [Tooltip("Enter Zip Code")]
  18.         public string ZipCode { get; set; }
  19.         [Mask("9999-9999-9999-9999")]
  20.         [Tooltip("Enter Credit Card")]
  21.         public string CreaditCard { get; set; }
  22.         [AutoComplete("Person", "GetStates", "state")]
  23.         public string State { get; set; }
  24.         [AutoComplete("Person", "GetStates", "state")]
  25.         public string USState { get; set; }
  26.         public ListSource Country { get; set; }
  27.     }
Step 3: Created an EditorTemplate for this type and added knockoutJS functions for move items left/right/up/down.
MvcLists\Views\Shared\EditorTemplates\ListSource.cshtml
  1. @model MvcLists.Models.ListSource
  2. <style type="text/css">
  3.     .divItem
  4.     {
  5.         float: none;
  6.         border-bottom-width: .2em;
  7.         border-bottom-style: solid;
  8.         border-bottom-color: #000000;
  9.         padding: 5px;
  10.         cursor: pointer;
  11.     }
  12.     .rightBoxItem
  13.     {
  14.         padding-left: 10px;
  15.     }
  16.     .indexCell
  17.     {
  18.         /*border-right-width: .2em;
  19.             border-right-style: solid;
  20.             border-right-color: #900;*/
  21.         padding: 5px;
  22.         padding-right: 10px;
  23.     }
  24.     .selectedItem
  25.     {
  26.         background: gold;
  27.     }
  28.     .header
  29.     {
  30.         font-family: arial;
  31.         color: #003469;
  32.         background-color: #dcdcdc;
  33.         margin: 0 0 10px 0;
  34.         padding: 2px 5px 2px 5px;
  35.     }
  36. </style>
  37. <script src="../../../Scripts/jquery-1.5.1.min.js" type="text/javascript"></script>
  38. <script src="../../../Scripts/knockout-2.0.0.js" type="text/javascript"></script>
  39. <script>
  40.         
  41.         function ListViewModel() {
  42.             var listItem = function(text,id,itemIndex) {
  43.                 this.Text = text;
  44.                 this.Id = id;
  45.             };
  46.             var self = this;
  47.             var listA = @Html.Raw(Json.Encode(Model.Available.ToList()));
  48.             var listB = @Html.Raw(Json.Encode(Model.Selected.ToList()));
  49.             self.listAArray= ko.observableArray(ko.utils.arrayMap(listA,function(list) {
  50.                 return new listItem(list.Text, list.Value,index);
  51.             }));
  52.             var index = 0;
  53.             self.listBArray= ko.observableArray(ko.utils.arrayMap(listB,function(list) {
  54.                 index++;
  55.                 return new listItem(list.Text, list.Value,index);
  56.             }));
  57.             self.changeClass = function(item) {
  58.                 $(item).closest("divItem").toggleClass("selectedItem");
  59.             };
  60.             self.moveRight = function() {
  61.                 $(".leftBox .selectedItem").each(function() {
  62.                     var item=ko.dataFor(this);
  63.                     self.listBArray.push(item);
  64.                     self.listAArray.remove(item);
  65.                 });
  66.             };
  67.             self.moveLeft = function() {
  68.                 $(".rightBox .selectedItem").each(function() {
  69.                     var item = ko.dataFor(this);
  70.                     self.listAArray.push(item);
  71.                     self.listBArray.remove(item);
  72.                     
  73.                 });
  74.             };
  75.             self.moveUp = function() {
  76.                 $(".rightBox .selectedItem").each(function() {
  77.                     var item = ko.dataFor(this);
  78.                     var index = self.listBArray.indexOf(item);
  79.                     if(index>0) {
  80.                         self.listBArray.remove(item);
  81.                         self.listBArray.splice(index - 1, 0, item);
  82.                         $(".rightBox .divItem:eq(" + (index -1) + ")").toggleClass("selectedItem");
  83.                     } else {
  84.                         return false;  
  85.                     }
  86.  
  87.                 });
  88.             };
  89.             self.moveDown = function() {
  90.                 $(".rightBox .selectedItem").each(function() {
  91.                     var item = ko.dataFor(this);
  92.                     var index = self.listBArray.indexOf(item);
  93.                     if(index<self.listBArray.length-1) {
  94.                         self.listBArray.remove(item);
  95.                         self.listBArray.splice(index + 1, 0, item);
  96.                         $(".rightBox .divItem:eq(" + (index +1) + ")").toggleClass("selectedItem");
  97.                     } else {
  98.                         return false;  
  99.                     }
  100.  
  101.                 });
  102.             };
  103.             
  104.             self.afterRender = function() {
  105.                 var i = 0;
  106.                 $(".rightBox .divItem").each(function() {
  107.                     i++;
  108.                     $(this).find(".indexCell")[0].text(i);
  109.                 });
  110.             };
  111.             ko.bindingHandlers.Index = {
  112.                 update:function (element,valueAccessor) {
  113.                     var i = 0;
  114.                     $(".rightBox .indexCell").each(function() {
  115.                         i++;
  116.                         $(this).text(i);
  117.                     });
  118.                     $(".divItem").unbind("click").bind("click", function() {
  119.                         $(this).toggleClass("selectedItem");
  120.                     });
  121.                 }
  122.             };
  123.         }
  124.  
  125.         $(document).ready(function () {
  126.             ko.applyBindings(new ListViewModel());
  127.         });
  128. </script>
  129. @Html.LabelForModel()
  130. <div style="margin-left: 5px; width: 100%;">
  131.     <div style="margin: auto; width: 90%;">
  132.         <div>
  133.             <div style="float: left">
  134.                 <div>
  135.                     <span>Available</span>
  136.                 </div>
  137.                 <div data-bind="foreach:listAArray" style="color: black; background-color: #dcdcdc;
  138.                     display: inline-block; border-style: solid; border-width: 2px; border-bottom-width: 0px;
  139.                     border-color: #000000" class="leftBox">
  140.                     <div class="divItem">
  141.                         <span data-bind="text:Text"></span>
  142.                     </div>
  143.                 </div>
  144.             </div>
  145.             <div style="float: left; vertical-align: middle; padding: 5px; display: inline-block;
  146.                 margin-top: 50px; width: 75px;">
  147.                 <div>
  148.                     <div>
  149.                         <input type="button" data-bind="click:moveRight" value=">" /></div>
  150.                     <div>
  151.                         <input type="button" data-bind="click:moveLeft" value="<" /></div>
  152.                 </div>
  153.             </div>
  154.             <div style="float: left">
  155.                 <div>
  156.                     <span>Selected</span>
  157.                 </div>
  158.                 <div class="rightBox" data-bind="foreach:listBArray,Index:listBArray" style="
  159.                     background-color: #FFFFAA; display: inline-block; border-style: solid; border-width: 2px;
  160.                     border-color: #000000; border-bottom-width: 0px">
  161.                     <span class="indexCell" style="float: left;"></span>
  162.                     <div class="divItem rightBoxItem">
  163.                         <span data-bind="text:Text"></span>
  164.                     </div>
  165.                 </div>
  166.             </div>
  167.             <div style="float: left; vertical-align: middle; padding: 5px; display: inline-block;
  168.                 margin-top: 50px; width: 75px;">
  169.                 <div style="background-color: bisque;">
  170.                     <div>
  171.                         <input type="button" data-bind="click:moveUp" value="Move Up" /></div>
  172.                     <div>
  173.                         <input type="button" data-bind="click:moveDown" value="Move Down" /></div>
  174.                 </div>
  175.             </div>
  176.             <div style="clear: both">
  177.             </div>
  178.         </div>
  179.     </div>
  180. </div>
Step 4: In controller/viewModel set DataSource for list box.
MvcLists\Controllers\PersonController.cs
  1. public ListSource Countries
  2.         {
  3.             get
  4.             {
  5.                 return new ListSource
  6.                            {
  7.                                Available = new List<Item>
  8.                                                {
  9.                                                    new Item {Text = "US", Value = 1},
  10.                                                    new Item {Text = "UK", Value = 2},
  11.                                                    new Item {Text = "Canada", Value = 3},
  12.                                                    new Item {Text = "France", Value = 4}
  13.                                                },
  14.                                Selected = new List<Item> {new Item {Text = "India", Value = 5}}
  15.                            };
  16.             }
  17.         }
Step 5: Render this listbox(Country)
MvcLists\Views\Person\Index.cshtml
  1. @using MvcLists.Common.HtmlHelpers
  2. @model MvcLists.Models.Person
  3. @{
  4.     ViewBag.Title = "Index";
  5.     Layout = "~/Views/Shared/_Layout.cshtml";
  6. }
  7. <h2>
  8.     Index</h2>
  9. <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
  10. <script src="../../Scripts/jquery.maskedinput.js" type="text/javascript"></script>
  11. @using (Html.BeginForm("Index","Person"))
  12. {
  13.     <fieldset>
  14.         <legend>Person</legend>
  15.         <div class="editor-label">
  16.             @Html.LabelFor(model => model.FirstName)
  17.         </div>
  18.         <div class="editor-field">
  19.             @Html.TextBoxFor(model => model.FirstName,new{title=@Html.TooltipFor(x=>x.FirstName)})
  20.         </div>
  21.         <div class="editor-label">
  22.             @Html.LabelFor(model => model.LastName)
  23.         </div>
  24.         <div class="editor-field">
  25.             @Html.TextBoxFor(model => model.LastName,new{title=Html.TooltipFor(x=>x.LastName)})
  26.         </div>
  27.         @Html.EditorFor(x => x.SSN)
  28.         @Html.EditorFor(x => x.Age)
  29.         @Html.EditorFor(x => x.Phone)
  30.         @Html.EditorFor(x => x.CreaditCard)
  31.         @Html.EditorFor(x => x.ZipCode)
  32.         @Html.EditorFor(x=>x.State)
  33.         @Html.EditorFor(x=>x.USState)
  34.         @Html.EditorFor(x=>x.Country)
  35.  
  36.         <p>
  37.             <input type="submit" value="Create" />
  38.         </p>
  39.     </fieldset>
  40. }
  41. <div>
  42.     @Html.ActionLink("Back to List", "Index")
  43. </div>

No comments: