Friday, November 06, 2009

Generic editable GridView - ASP.NET MVC

The goal is to build editable data grid in a project uses ASP.NET MVC 1.0, it is know that ASP.NET MVC does not use controls with ( runat=”server” ) attribute, which means I can’t use regular DataGrid or GridView in my code. and HTMLHelper class in ASP.NET MVC does provide such HTML component.

So, there are two solutions:

  1. Using extension to extend HTMLHelper functionality and build HTML using code
       1: public static class GridViewExt



       2:     {



       3:         public static string GridView(this HtmlHelper html, IEnumerable DataSource, object HTMLAtrributes)



       4:         {



       5:             string htmlAttributesString = ConvertToAtrributes.ConvertObjectToAttributeList(HTMLAtrributes).Trim();



       6:             StringBuilder resultBuilder = new StringBuilder();



       7:             resultBuilder.AppendFormat("<table{0}{1}>", string.IsNullOrEmpty(htmlAttributesString) ? "" : " ", htmlAttributesString).AppendLine();



       8:             IEnumerator sourceEnumerator = DataSource.GetEnumerator();



       9:             Type itemType;



      10:             if (sourceEnumerator.MoveNext() == false)



      11:             {



      12:                 return "";



      13:             }



      14:             else



      15:             {



      16:                 itemType = sourceEnumerator.Current.GetType();



      17:             }



      18:             PropertyInfo[] properties = itemType.GetProperties();



      19:             resultBuilder.AppendLine("\t<tr>");



      20:             foreach (PropertyInfo property in properties)



      21:             {



      22:                 resultBuilder.AppendFormat("\t\t<th>{0}</th>", property.Name).AppendLine();



      23:             }



      24:             resultBuilder.AppendLine("\t</tr>");



      25:             foreach (object item in DataSource)



      26:             {



      27:                 resultBuilder.AppendLine("\t<tr>");



      28:                 foreach (PropertyInfo property in properties)



      29:                 {



      30:                     resultBuilder.AppendFormat("\t\t<td>{0}</td>", property.GetValue(item, null)).AppendLine();



      31:                 }



      32:                 resultBuilder.AppendLine("\t</tr>");



      33:             }



      34:             resultBuilder.Append("</table>");



      35:             return resultBuilder.ToString();



      36:         }



      37:     }



      38: public class ConvertToAtrributes



      39:     {



      40:         public static string ConvertObjectToAttributeList(object value)



      41:         {



      42:             StringBuilder builder = new StringBuilder();



      43:             if (value != null)



      44:             {



      45:                 IDictionary<string, object> dictionary = value as IDictionary<string, object>;



      46:                 if (dictionary == null)



      47:                 {



      48:                     dictionary = new RouteValueDictionary(value);



      49:                 }



      50:                 string format = "{0}=\"{1}\" ";



      51:                 foreach (string str2 in dictionary.Keys)



      52:                 {



      53:                     object obj2 = dictionary[str2];



      54:                     if (dictionary[str2] is bool)



      55:                     {



      56:                         obj2 = dictionary[str2].ToString().ToLowerInvariant();



      57:                     }



      58:                     builder.AppendFormat(format, str2.Replace("_", "").ToLowerInvariant(), obj2);



      59:                 }



      60:             }



      61:             return builder.ToString();



      62:         }



      63:     }


            now you should be able to call to call HTMLHelper extended method inside your View

      1: <%= this.Html.GridView(Model.Products)%>

    but it is still not editable and to add editing functionality it will be so hard to write a lot of HTML code inside C# code and will not be easy to maintain or fix issues; so let’s consider the second option.


  2. Building User control and get advantage of writing HTML inside the user control HTML (one thing to remember using ASP.NET MVC no use for page life cycle you can get rid of that).

    Now let me build a user control ascx page using fantastic JQuery fetures 


       1: <%@ Control Language="C#" AutoEventWireup="true" CodeBehind="GridView.ascx.cs" Inherits="Shared.UserControls.GridView" %>



       2: <%@ Import Namespace="System.Reflection" %>



       3: <%
       1: -- style code --
    %>



       4: <style type="text/css">



       5:     .gridView



       6:     {



       7:         border-collapse: collapse;



       8:     }



       9:     .gridView th, .gridView td



      10:     {



      11:         padding: 5px;



      12:     }



      13:     .gridView th



      14:     {



      15:         border-bottom: double 3px black;



      16:     }



      17:     .gridView td



      18:     {



      19:         border-bottom: solid 1px black;



      20:     }



      21:     .alternatingItem



      22:     {



      23:         background-color: lightgrey;



      24:     }



      25: </style>



      26: <%
       1: -- Javascripts code --
    %>



      27: <script src="../../Scripts/jquery-1.3.2.js" type="text/javascript"></script>
       1:  
       2: <%--<script src="../../Scripts/jquery-1.3.2-vsdoc.js" type="text/javascript">
       1: </script>--%>
       2: <script type="text/javascript">
       3:     var controller = '';
       4:     var SaveAction = '';
       5:     <% GetController(); GetSaveAction(); %>
       6:     function IsValidAction(ctrl, actn)
       7:     {
       8:         if(($.trim(ctrl) == '') || ($.trim(actn) == ''))
       9:             return false;
      10:         else
      11:             return true;
      12:     }
      13:     function SaveAllClick() {
      14:         //debugger;
      15:         if(!IsValidAction(controller,SaveAction))
      16:         {
      17:             alert("Error while saving data");
      18:             return;
      19:         }
      20:         var i = 0;
      21:         var inputs = new Array();
      22:         $("#BaseTable").find("input").each(function() {
      23:             inputs[i] = [this.id, this.value];
      24:             i++;
      25:         });
      26:  
      27:         var columnsCount = $("#BaseTable").find("th").length;
      28:         $.post("/"+controller+"/"+SaveAction, { inputs: inputs, columnsCount: columnsCount });
      29:         Border();
      30:         $("#ChangeFlag").text("");
      31:         $("#SaveAll").attr("disabled", "disabled");
      32:         alert("Saved");
      33:     }
      34:     var prevCell;
      35:     function Border(cell) {
      36:         if (prevCell != null) {
      37:             $(prevCell).attr("style", "border-style: none");
      38:         }
      39:         if (cell != null) {
      40:             var i = $(cell); //.find("input");
      41:             i.attr("style", "border-style: inset");
      42:         }
      43:         prevCell = cell;
      44:     }
      45:     function Unborder(cell) {
      46:         if (cell != null) {
      47:             var i = $(cell); //.find("input");
      48:             i.attr("style", "border-style: none");
      49:         }
      50:     }
      51:     function MarkChanges() {
      52:         $("#ChangeFlag").text("Page should be saved");
      53:         $("#SaveAll").removeAttr("disabled");
      54:     }
    </script>



      28: <%
       1: -- Show the Headers --
    %>



      29: <table class="gridView" id="BaseTable">



      30:     <thead>



      31:         <tr>



      32:             <%
       1:  foreach (PropertyInfo prop in this.Columns)
       2:                { 
    %>



      33:             <th>



      34:                 <%
       1: = prop.Name 
    %>



      35:             </th>



      36:             <%
       1:  } 
    %>



      37:         </tr>



      38:     </thead>



      39:     <%
       1: -- Show the Rows --
    %>



      40:     <tbody>



      41:         <%
       1:  foreach (object row in this.Rows)
       2:            { 
    %>



      42:         <tr class="<%= this.FlipCssClass( "item", "alternatingItem") %>">



      43:             <%
       1: -- Show Each Column --
    %>



      44:             <%
       1:  foreach (PropertyInfo prop in this.Columns)
       2:                { 
    %>



      45:             <td>



      46:                 <%
       1:  var typeCode = Type.GetTypeCode(prop.PropertyType); 
    %>



      47:                 <%
       1: var str_disabled = "";
       2:                   if (this.GetAtt(prop.Name))
       3:                       str_disabled = "disabled=\"disabled\"";
       4:                  
    %>



      48:                 <%
       1: -- String Columns --
    %>



      49:                 <%
       1:  if (typeCode == TypeCode.String)
       2:                    { 
    %>



      50:                 <input id="<%=prop.Name %>" type="text" style="border-style: none" value="<%= GetColumnValue(row, prop.Name)%>"



      51:                     onmousedown="Border(this)" onselect="Border(this)" onblur="Unborder(this)" onchange="MarkChanges()"



      52:                     &lt;%= str_disabled %&gt; />



      53:                 <%
       1:  } 
    %>



      54:                 <%
       1: -- DateTime Columns --
    %>



      55:                 <%
       1:  if (typeCode == TypeCode.DateTime)
       2:                    { 
    %>



      56:                 <input id="<%=prop.Name %>" type="text" style="border-style: none" value="<%= GetColumnValue(row, prop.Name, "{0:D}")%>"



      57:                     onmousedown="Border(this)" onselect="Border(this)" onblur="Unborder(this)" onchange="MarkChanges()"



      58:                     &lt;%= str_disabled %&gt; />



      59:                 <%
       1:  } 
    %>



      60:                 <%
       1: -- Decimal Columns --
    %>



      61:                 <%
       1:  if (typeCode == TypeCode.Decimal)
       2:                    { 
    %>



      62:                 <input id="<%=prop.Name %>" type="text" style="border-style: none" value="<%= GetColumnValue(row, prop.Name, "{0:f}") %>"



      63:                     onmousedown="Border(this)" onselect="Border(this)" onblur="Unborder(this)" onchange="MarkChanges()"



      64:                     &lt;%= str_disabled %&gt; />



      65:                 <%
       1:  } 
    %>



      66:                 <%
       1: -- Decimal Columns --
    %>



      67:                 <%
       1:  if (typeCode == TypeCode.Double)
       2:                    { 
    %>$



      68:                 <input id="<%=prop.Name %>" type="text" style="border-style: none" value="<%= GetColumnValue(row, prop.Name, "{0:f}") %>"



      69:                     onmousedown="Border(this)" onselect="Border(this)" onblur="Unborder(this)" onchange="MarkChanges()"



      70:                     &lt;%= str_disabled %&gt; />



      71:                 <%
       1:  } 
    %>



      72:                 <%
       1: -- Boolean Columns --
    %>



      73:                 <%
       1:  if (typeCode == TypeCode.Boolean)
       2:                    { 
    %>



      74:                 <%
       1:  if ((bool)(this.GetColumnValue(row, prop.Name)))
       2:                    { 
    %>



      75:                 <input type="checkbox" disabled="disabled" checked="checked" />



      76:                 <%
       1:  }
       2:                    else
       3:                    { 
    %>



      77:                 <input type="checkbox" disabled="disabled" />



      78:                 <%
       1:  } 
    %>



      79:                 <%
       1:  } 
    %>



      80:                 <%
       1: -- Integer Columns --
    %>



      81:                 <%
       1:  if (typeCode == TypeCode.Int32)
       2:                    { 
    %>



      82:                 <input id="<%=prop.Name %>" type="text" style="border-style: none" value="<%= GetColumnValue(row, prop.Name)%>"



      83:                     onmousedown="Border(this)" onselect="Border(this)" onblur="Unborder(this)" onchange="MarkChanges()"



      84:                     &lt;%= str_disabled %&gt; />



      85:                 <%
       1:  } 
    %>



      86:             </td>



      87:             <%
       1:  } 
    %>



      88:         </tr>



      89:         <%
       1:  } 
    %>



      90:     </tbody>



      91: </table>



      92: <p>



      93:     <input id="SaveAll" disabled="disabled" type="button" value="Save" onclick="SaveAllClick()" />



      94:     <span id="ChangeFlag"></span>



      95: </p>



    and building code behind code for this user control using last post idea of TypeDescriptor class to make the user control more generic accepting any type of collection and render editable properties according to custom attributes associated with it.



       1: using System;



       2: using System.Collections;



       3: using System.Collections.Generic;



       4: using System.Linq;



       5: using System.Web;



       6: using System.Web.UI;



       7: using System.Web.Mvc;



       8: using System.Reflection;



       9: using System.ComponentModel;



      10: using CustomAtrributes;



      11:  



      12: namespace Shared.UserControls



      13: {



      14:     public partial class GridView : System.Web.Mvc.ViewUserControl<IEnumerable>



      15:     {



      16:         protected string GetController()



      17:         {



      18:             



      19:             if (ViewContext.RouteData.Values["Controller"] != null)



      20:             {



      21:                 Response.Write("controller = '" + ViewContext.RouteData.Values["Controller"] as string + "';");



      22:                 return ViewContext.RouteData.Values["Controller"] as string;



      23:             }



      24:             else



      25:                 return string.Empty;



      26:         }



      27:         protected string GetSaveAction()



      28:         {



      29:             if (ViewData.Model.GetType().GetProperty("SaveAction") != null)



      30:             {



      31:                 Response.Write("SaveAction = '" + DataBinder.Eval(ViewData.Model, "SaveAction") as string + "';");



      32:                 return DataBinder.Eval(ViewData.Model, "SaveAction") as string;



      33:             }



      34:             else



      35:                 return string.Empty;



      36:         }



      37:         protected PropertyInfo[] Columns



      38:         {



      39:             



      40:             get 



      41:             {



      42:                 var e = ViewData.Model.GetEnumerator();



      43:                 e.MoveNext();



      44:                 object firstRow = e.Current;



      45:                 if (firstRow == null)



      46:                 {



      47:                     throw new Exception("No data passed to GridView User Control.");



      48:                 }



      49:                 return firstRow.GetType().GetProperties();



      50:             }



      51:         }



      52:         protected IEnumerable Rows



      53:         {



      54:             get { return ViewData.Model; }



      55:         }



      56:         protected object GetColumnValue(object row, string columnName)



      57:         {



      58:             return DataBinder.Eval(row, columnName);



      59:         }



      60:         protected object GetColumnValue(object row, string columnName, string format)



      61:         {



      62:             return DataBinder.Eval(row, columnName, format); 



      63:         }



      64:         bool flip = false;



      65:         protected string FlipCssClass(string className, string alternativeClassName)



      66:         {



      67:             flip = !flip;



      68:             return flip ? className : alternativeClassName;



      69:         }



      70:         



      71:         protected bool GetAtt(string Name)



      72:         {



      73:             



      74:             bool readonlyatt = false;



      75:             UIAttributes uiatt = TypeDescriptor.GetAttributes(Model).Cast<Attribute>().SingleOrDefault(a => a.GetType().Name == typeof(UIAttributes).Name) as UIAttributes;



      76:             if ((uiatt != null) && uiatt.IsReadOnly)



      77:                 return uiatt.IsReadOnly;



      78:             PropertyInfo propInfo = Model.GetType().GetProperty(Name);



      79:                 //(propInfo =>



      80:                     {



      81:                         PropertyDescriptor propDescriptor = TypeDescriptor.GetProperties(Model).Cast<PropertyDescriptor>().SingleOrDefault(p => propInfo.Name == p.Name);



      82:                         if (propDescriptor != null)



      83:                         {



      84:                             UIAttributes attrib = propDescriptor.Attributes.Cast<Attribute>().SingleOrDefault(p => p.GetType().Name == typeof(UIAttributes).Name) as UIAttributes;



      85:                             if (attrib != null)



      86:                             {



      87:                                 readonlyatt = attrib.IsReadOnly;



      88:                             }



      89:                         }



      90:                     }



      91:                     //);



      92:                     return readonlyatt;



      93:         }



      94:     }



      95: }



    and by using couple of ASP.NET MVC helpful classes like ViewContext, now I can detect which controller called this user control automatically and by adding new property to the the ViewData specifaying the Save Action method name in the controller; that gives more generic to the user control as it is not tight to specific controller or  methods.



Still need more enhancements and features like validation and styles, but so far it is nice.



It was very nice challenge to build such a control and I owe to references I mention below a lot.



References:



1 comment:

  1. Sorry Mina i think i need ur help if its possible in such an issue

    ReplyDelete