ASP.NET Controls from Scratch

In this article is presented a step by step approach to create a fully rendered ASP.NET custom control.

Custom controls are a very important part of ASP.NET. They provide a very high level of encapsulation and reusability. There are roughly three types of controls, as follows:

  • Web User Control - involving a resource with .ASCX extension
  • Composite control - a custom control embedding other ASP.NET controls
  • Custom control - the fully rendered control, with full control on state management and rendering

There are many advantages related to the fully rendered custom control (the third category) as follows:

  • Full control of all the aspects of the control lifecycle
  • Full control of the rendering process
  • The final result can be delivered as a simple library - assembly - unlike the web user control where the .ascx file is a required deliverable

Most of the professional grade custom controls are distributed as fully rendered controls

Create the Class

The custom control is a regular .NET class, extending System.Web.WebControl. To start our test, create a new ASP.NET project, then add a new item and select "ASP.NET Server Control". I removed the existent Text property from the created class (we start from scratch, remember?) and there is the result:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.ComponentModel;
   4:  using System.Linq;
   5:  using System.Text;
   6:  using System.Web;
   7:  using System.Web.UI;
   8:  using System.Web.UI.WebControls;
   9:   
  10:  namespace TestControl
  11:  {
  12:      
  13:      [ToolboxData("<{0}:TestControl runat=server></{0}:TestControl>")]
  14:      public class TestControl : WebControl
  15:      {
  16:          protected override void RenderContents(HtmlTextWriter output)
  17:          {
  18:              output.Write("test control here");
  19:          }
  20:      }
  21:  }

Now compile the project and open your main page in design mode. Automatically the IDE toolbox will contain your component ready to use. Get the control and drag it to your page, and you will see the output from the RenderContents method there.

Finish the componet state related design:

  • Decide what are the attributes that define the control state (names, types, cardinality)
  • Decide which attributes will be part of form controls (editable)
  • Decide the attributes that will have static values - these will be saved into the ViewState object
  • In the end, decide how the componet will be rendered and finish the rendering method

For our purpose, we will have three attributes, address (string), number (integer), and fullName (string). The first two will be static, the third one editable.

We go ahead and implement these attributes as part of our class (two string attributes, one integer)

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.ComponentModel;
   4:  using System.Linq;
   5:  using System.Text;
   6:  using System.Web;
   7:  using System.Web.UI;
   8:  using System.Web.UI.WebControls;
   9:   
  10:  namespace TestControl
  11:  {
  12:      
  13:      [ToolboxData("<{0}:TestControl runat=server></{0}:TestControl>")]
  14:      public class TestControl : WebControl
  15:      {
  16:          private string address;
  17:          private int number;
  18:   
  19:          private string fullName;
  20:   
  21:          public string Address
  22:          {
  23:              get { return address; }
  24:              set { address = value; }
  25:          }
  26:   
  27:          public int Number
  28:          {
  29:              get { return number; }
  30:              set { number = value; }
  31:          }
  32:   
  33:          public string FullName
  34:          {
  35:              get { return fullName; }
  36:              set { fullName = value; }
  37:          }
  38:          protected override void RenderContents(HtmlTextWriter output)
  39:          {
  40:              output.Write("test control here");
  41:          }
  42:      }
  43:  }

Add the following interfaces as part of the interface list implemented by the control: INamingContainer and IPostBackDataHandler

INamingContainer does not have any methods, however any container that implements this interface will receive an uniqe identifier from its parent.

IPostBackDataHandler is very important for processing the state attributes that are rendered in http form controls. Its method LoadPostData is invoked when the controller should load the postback values from the request

Override the methods LoadViewState and SaveViewState from the parent; they will be invoked when the information should be saved to or loaded from the ViewState object.

ViewState

A lot of people say that ViewState is a very bad thing, which is not true. ViewState is an amazing way of preserving the page state across requests. Sometimes it is used in a wrong way and that is the bad thing.

Rule of thumb: if the state of a component is restored anyway from the database or the session every time the page is loaded, then turn its EnableViewState off. Look at each component from this perspective and you will never have problems with ViewState

Don't forget to call the base class methods LoadViewState and SaveViewState with the appropriate information. You will not see any difference if you just started from scratch and the superclass is WebControl, however if you plan a more complex class hierarchy, you will avoid potential issues.

All the code samples from the internet use constructs as follows in order to save / retrieve information from the ViewState:

 1:              ViewState["whatevername"] = FullName;

In this case, both the name (whatevername) and the value (FullName) will be saved in the view state field increasing the page size. This is a waste of traffic. You don't need this, there is a better way:

  • Create an ArrayList object
  • Add the objects you want to save in the array list
  • One of the objects in the ArrayList must be the one returned by the super class SaveViewState method
  • This should be the object returned by the SaveViewState method
  • This will be the object passed to LoadViewState when this method will be invoked (ok, not the very same instance, but an identical one anyway)
  • Process the stored values in the same order (it is easy to check, the two methods are a couple of lines away)
  • You are done!

Here is our implementation. Note the fact that we process the object for the super class (don't forget that) and also we process our attributes in the very same order.

   1:          protected override void LoadViewState(object current)
   2:          {
   3:              // here we know the object must be a
   4:              // ArrayList, as this is the object saved by
   5:              // SaveViewState method
   6:              if (current is ArrayList)
   7:              {
   8:                  ArrayList list = (ArrayList)current;
   9:   
  10:                  // we assume the array list is of length 3 (as saved in
  11:                  // SaveViewState, we also know exactly what is at each index
  12:                  // we just need to double check the SaveViewState to
  13:                  // confirm
  14:                  object baseObject = list[0];
  15:                  base.LoadViewState(baseObject);
  16:   
  17:                  object adr = list[1];
  18:                  object nr = list[2];
  19:                  object fname = list[3];
  20:   
  21:                  Address = adr == null ? null : adr.ToString();
  22:                  Number = (nr == null ? 0 : (int)nr);
  23:                  FullName = fname == null ? null : fname.ToString();
  24:              }
  25:              else
  26:              {
  27:                  throw new Exception("Wrong object in the LoadViewState");
  28:              }
  29:          }
  30:   
  31:          protected override object SaveViewState()
  32:          {
  33:              ArrayList list = new ArrayList();
  34:   
  35:              // superclass
  36:              list.Add(base.SaveViewState());
  37:   
  38:              list.Add(Address);
  39:              list.Add(Number);
  40:              list.Add(FullName);
  41:   
  42:              return list;
  43:          }

Load the postback data from the request

This step consists in implementing the method LoadPostData from the IPostBackDataHandler interface. You know what names you attached to the form controls, basically just look for these names and attach the values to the variables.

In order for this method to be invoked, you need to have a http field part of your component with the name identical with the component UniqueID. This is very important. Normally this will be a hidden text field with some hardcoded value as we're only interested in its presence. Don't store an empty value in it, just put something.

Here is the implemented method

   1:          public bool LoadPostData(string postDataKey, 
                      System.Collections.Specialized.NameValueCollection postCollection)
   2:          {
   3:              // load the values componenet by component; we only have one
   4:              string fname = postCollection[UniqueID + "_full"];
   5:   
   6:              // see if the state has changed - in FullName we already
   7:              // have the value loaded from ViewState, which is the value
   8:              // kept by FullName on the previous page display
   9:              bool stateChanged = false;
  10:              if (FullName == null)
  11:              {
  12:                  stateChanged = fname != null;
  13:              }
  14:              else
  15:              {
  16:                  stateChanged = ! FullName.Equals(fname);
  17:              }
  18:   
  19:              FullName = fname;
  20:   
  21:              return stateChanged;
  22:          }

In order to see if the component state changed, you need to see if the previous and current values for the editable controls are the same. This can be achieved in the following way:

  • Save the editable attributes in ViewState as well
  • The ViewState will contain the "previous" attribute value
  • The postback data will contain the actual value
  • Check to see whether they are equal or not, this will determine the value returned by LoadPostData method.
  • After the evaluation, emember to store the postback data in the attribute, because this is the actual value

Rendering

Basically the method RenderContents will actually generate the html for the component, based on the component state.

Here is the code; spot the hidden field at the beginning of the rendered content:

   1:          protected override void RenderContents(HtmlTextWriter output)
   2:          {
   3:              StringBuilder buffer = new StringBuilder();
   4:   
   5:              buffer.Append("<input type=\"hidden\" name=\"");
   6:              buffer.Append(UniqueID);
   7:              buffer.Append("\" value=\"hd\">");
   8:   
   9:              buffer.Append("<table cellpadding=\"4\" cellspacing=\"1\" border=\"1\">");
  10:              buffer.Append("<tr><td>Item</td><td>Value</td></tr>");
  11:   
  12:              buffer.Append("<tr><td>Address</td><td>");
  13:              buffer.Append(Address);
  14:              buffer.Append("</td></tr>");
  15:   
  16:              buffer.Append("<tr><td>Number</td><td>");
  17:              buffer.Append(Number);
  18:              buffer.Append("</td></tr>");
  19:   
  20:              buffer.Append("<tr><td>Full Name</td><td><input type=\"text\" name=\"");
  21:              buffer.Append(UniqueID + "_full");
  22:              buffer.Append("\" value=\"");
  23:              buffer.Append(FullName);
  24:              buffer.Append("\"></td></tr>");
  25:   
  26:              buffer.Append("</table>");
  27:   
  28:              output.Write(buffer.ToString());
  29:          }

Final code

Here is how the component looks like rendered; upon postback, the editable field and the two static attributes will hold their values

Here is the final code; if you don't need to track the state change, you can stop saving the editable fields in the ViewState and remove the comparison done in the LoadPostData method; just override the attribute with the actual value.

   1:  using System;
   2:  using System.ComponentModel;
   3:  using System.Web.UI;
   4:  using System.Web.UI.WebControls;
   5:  using System.Collections;
   6:  using System.Text;
   7:   
   8:  namespace TestControl
   9:  {
  10:      
  11:      [ToolboxData("<{0}:TestControl runat=server></{0}:TestControl>")]
  12:      public class TestControl : WebControl, INamingContainer, IPostBackDataHandler
  13:      {
  14:   
  15:          private string address;
  16:          private int number;
  17:   
  18:          private string fullName;
  19:   
  20:          public string Address
  21:          {
  22:              get { return address; }
  23:              set { address = value; }
  24:          }
  25:   
  26:          public int Number
  27:          {
  28:              get { return number; }
  29:              set { number = value; }
  30:          }
  31:   
  32:          public string FullName
  33:          {
  34:              get { return fullName; }
  35:              set { fullName = value; }
  36:          }
  37:          protected override void RenderContents(HtmlTextWriter output)
  38:          {
  39:              StringBuilder buffer = new StringBuilder();
  40:   
  41:              buffer.Append("<input type=\"hidden\" name=\"");
  42:              buffer.Append(UniqueID);
  43:              buffer.Append("\" value=\"hd\">");
  44:              
  45:              buffer.Append("<table cellpadding=\"4\" cellspacing=\"1\" border=\"1\">");
  46:              buffer.Append("<tr><td>Item</td><td>Value</td></tr>");
  47:   
  48:              buffer.Append("<tr><td>Address</td><td>");
  49:              buffer.Append(Address);
  50:              buffer.Append("</td></tr>");
  51:   
  52:              buffer.Append("<tr><td>Number</td><td>");
  53:              buffer.Append(Number);
  54:              buffer.Append("</td></tr>");
  55:   
  56:              buffer.Append("<tr><td>Full Name</td><td><input type=\"text\" name=\"");
  57:              buffer.Append(UniqueID + "_full");
  58:              buffer.Append("\" value=\"");
  59:              buffer.Append(FullName);
  60:              buffer.Append("\"></td></tr>");
  61:   
  62:              buffer.Append("</table>");
  63:   
  64:              output.Write(buffer.ToString());
  65:          }
  66:   
  67:          public bool LoadPostData(string postDataKey, 
        System.Collections.Specialized.NameValueCollection postCollection)
  68:          {
  69:              // load the values componenet by component; we only have one
  70:              string fname = postCollection[UniqueID + "_full"];
  71:   
  72:              // see if the state has changed - in FullName we already
  73:              // have the value loaded from ViewState, which is the value
  74:              // kept by FullName on the previous page display
  75:              bool stateChanged = false;
  76:              if (FullName == null)
  77:              {
  78:                  stateChanged = fname != null;
  79:              }
  80:              else
  81:              {
  82:                  stateChanged = ! FullName.Equals(fname);
  83:              }
  84:   
  85:              FullName = fname;
  86:   
  87:              return stateChanged;
  88:          }
  89:   
  90:          public void RaisePostDataChangedEvent()
  91:          {
  92:              // not implemented for the current example
  93:          }
  94:   
  95:          protected override void LoadViewState(object current)
  96:          {
  97:              // here we know the object must be a 
  98:              // ArrayList, as this is the object saved by
  99:              // SaveViewState method
 100:              if (current is ArrayList)
 101:              {
 102:                  ArrayList list = (ArrayList)current;
 103:   
 104:                  // we assume the array list is of length 3 (as saved in
 105:                  // SaveViewState, we also know exactly what is at each index
 106:                  // we just need to double check the SaveViewState to 
 107:                  // confirm
 108:                  object baseObject = list[0];
 109:                  base.LoadViewState(baseObject);
 110:   
 111:                  object adr = list[1];
 112:                  object nr = list[2];
 113:                  object fname = list[3];
 114:   
 115:                  Address = adr == null ? null : adr.ToString();
 116:                  Number = (nr == null ? 0 : (int)nr); 
 117:                  FullName = fname == null ? null : fname.ToString();
 118:              }
 119:              else
 120:              {
 121:                  throw new Exception("Wrong object in the LoadViewState");
 122:              }
 123:          }
 124:          
 125:          protected override object SaveViewState()
 126:          {
 127:              ArrayList list = new ArrayList();
 128:   
 129:              // superclass
 130:              list.Add(base.SaveViewState());
 131:   
 132:              list.Add(Address);
 133:              list.Add(Number);
 134:              list.Add(FullName);
 135:   
 136:              return list;
 137:          }
 138:      }
 139:  }