Showing posts with label EditorTemplates. Show all posts
Showing posts with label EditorTemplates. Show all posts

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>