ASP.NET Dynamic TextBox & DropDownList Control ViewState To Store NameValueCollection

This article demonstrates the ASP.NET page life cycle, dynamic controls, wiring up dynamic events, ViewState, and using the NameValueCollection.

  Download C# Source Code

Only a developer would know the absolute joy of moving a heavily database centric desktop application to a web environment.  Can you smell the sarcasm?  Aside from storing data in cache, session, or static objects there is no storage place in memory for us to put data for fast access and hold onto it until the user closes the application the same way a desktop application can.  Thus, we end up having to run expensive queries to retrieve meta data (information stored in our database that helps describe our data) each time the page loads as well as when it posts back to itself.

Wouldn't it be nice to store this information somewhere so we don't have to run the same query again on page post back?  As an added benefit, it would be nice if we could rebuild the dynamic controls in their entirety (including colors, fonts, styles, etc...) without requerying the database at all!

This nice to have feature has really become a bit of a necessity for a rather complex project our team has been working on that is a little bit out of the norm.  Let me briefly summarize the requirements of our data entry pages.  The entire data entry page is database driven and in many cases this includes user specific settings for each data entry point.  A typical page might have 40 or so dynamically generated data entry points.  A wide variety of settings or meta data exists for each data entry point that range from display oriented settings, server side calculation settings, relational settings to combinations of data entry points, and settings determining how and when to save the data.  As you can see, this can get rather complicated.

Let's also review the application environment.  A typical user analysis model would consist of around a 100 of these data entry pages.  The Web site itself supports an unlimited number of analysis models.  Thus, the database is quite large and the process of obtaining the meta data is quite complex.  Running more queries than absolutely necessary can be a real drag on performance.  Also keep in mind that a typical user will spend anywhere from 15 - 45 minutes on a single data entry page completing that section of the analysis model.

Normally, I would be reluctant to implement a solution that loads up the ViewState in this manner.  Our web farm environment pretty much ruled out the use of static objects or the application cache.  The performance impact of having larger than normal ViewState and the process of managing ViewState isn't necessarily prohibitive.  That said, if you are trying to squeeze out every once of performance, doing this without it being necessary could hurt your cause.

You can use my sample as a means to measure how much is being stored in ViewState for the amount of data you expect to have in your own application.  The default setting of 60 textboxes and 60 dropdown lists (each list contains 10 records) takes up roughly 100KB of html and ViewState combined.

In the code sample below (and the downloadable zip file above), we'll see how this can be accomplished.  In a nutshell, I've extended the existing ASP.NET TextBox server control by adding a custom made namevaluecollection class and overrode how ViewState is handled.  The custom namevaluecollection class inherits the base namevaluecollection class and adds two methods for loading the collection from a string and converting the collection to a string.  Our own Peter Bromberg contributed the loading method and I took care of the rest.  

These methods were needed because serializing the entire namevaluecollection will cause our ViewState to grow enormously and take up bandwidth unnecessarily.  All we really want to do is store a few strings and ints for each data point in ViewState.  So, we'll store a string representation of the namevaluepair, similar to what you would see on a querystring, in ViewState.  Then, when we load and save view state, we'll move the data in and out of the NameValueCollection to make it easier for our user interface developers to work with the control.

For your convenience, I've hard coded the base properties of the textbox and dropdownlist control that can be stored in ViewState.  These properties are not stored in ViewState by ASP.NET controls.  In your implementation, you will probably want to exclude any of those properties you don't need.  You'll also want to note that controls created at runtime via the Page_Init event will not have their ViewState contents loaded until the Page_Load or OnLoad event fire (assuming you aren't overriding the Page.LoadViewState event).
You'll want to pay close attention to the way I've broken out how these two controls are dynamically added to the Table at runtime.


1. In Page_Init, create an ArrayList of all the dynamic controls to be placed in my html table.  If not Page.IsPostBack, then create an ArrayList of controls based on data retrieved from the database.  If Page.IsPostBack, then utilize the Request.Form collection to find all controls that have the specific naming convention (found in the UI class) for each control and populate the ArrayList of controls from this instead of querying the database again.  We need to use a specific naming convention because the Request.Form collection has no concept of server side control class types.

The simple act of creating the control in Page_Init will allow the Page to hook up the existing ViewState for the control with the same ID.  Thus, with the exception of our events, everything including fonts, colors, styles, values, etc... is automatically reinitialized to our control from the previous page load.

2. Iterate through the ArrayList of controls and wire up any standard events based on class type.

3. Add the controls to our html table.
  
So far, I've only created custom versions of the TextBox and DropDownList controls.  I think you'll find that it would be pretty easy to create different custom controls that do the same thing.

WebForm1.aspx

using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Diagnostics;

namespace TextBoxSample
{

   public class WebForm1 : System.Web.UI.Page
   {
  
       protected System.Web.UI.WebControls.Button Button1;
       protected System.Web.UI.WebControls.Table Table1;
      
       private void Page_Init(object sender, System.EventArgs e)
       {

          ArrayList controllist;

          if(!Page.IsPostBack)
          {
             controllist = BuildDynamicControls();
          }
          else
          {
             controllist = UI.CreateControlsFromBrowserForm();
          }

          WireUpEvents(controllist);

          BuildHtmlTableWithControls(controllist);
            
       }
    

       protected override void OnLoad(EventArgs e)
       {
        
       }


       private void BuildHtmlTableWithControls(ArrayList controlList)
       {

          TableRow trow;
          TableCell tcell;
          int cols = 6;

          try
          {

             trow = new TableRow();

             Table1.Rows.Add(trow);

             for(int i=0;i<controlList.Count;i++)
             {

                tcell = new TableCell();

                UI.AddControlToCell(tcell,controlList,i);

                trow.Cells.Add(tcell);

                if ((i + 1) % cols == 0)  
                {
                  Table1.Rows.Add(trow);
                  trow = new TableRow();
                }

             }

          }
          catch (Exception) { throw; }
       }



       private ArrayList BuildDynamicControls()
       {

         int max = 60;
         ArrayList controllist = new ArrayList();

         try
         {

           DataTable dt = GetSampleData(max);

           // Load up some test data to simulate
           // loading dynamic controls based on a
           // an expensive query from the database.

           for(int i=0;i<max;i++)
           {

              DataRow row = dt.Rows[i];

              CustomControls.TxtBox txt = new CustomControls.TxtBox();

              txt.ID = UI.TextBoxPrefix + row["ID"].ToString();
              txt.Text = row["Description"].ToString();
              txt.EnableViewState = true;
              txt.ForeColor = System.Drawing.Color.Orchid;
              txt.Font.Bold = true;
              txt.Font.Name = "Verdana";
              txt.Font.Italic = true;
              txt.Font.Size = 8;

              // Just for kicks, alternate colors to
              // test storing color in viewstate.

              if (i%2==0)
              {
                 txt.ForeColor = System.Drawing.Color.Red;
              }

              // Store some test data in our new
              // namevalue storage bin in viewstate for
              // this control.

              txt.NameValuePairs.Add("test1",i.ToString());
              txt.NameValuePairs.Add("test2","10" + i.ToString());
  
              // Add the control to our list of controls.

              controllist.Add(txt);


              // Repeat the process for a drop down list.

              CustomControls.DropDown dd = new CustomControls.DropDown();

              dd.ID = UI.DropDownPrefix + row["ID"].ToString();
              dd.EnableViewState = true;
              dd.ForeColor = System.Drawing.Color.Orchid;
              dd.Font.Bold = true;
              dd.Font.Name = "Arial";
              dd.Font.Italic = true;
              dd.Font.Size = 12;
              dd.NameValuePairs.Add("test1",i.ToString());
              dd.NameValuePairs.Add("test2","10" + i.ToString());
              dd.AutoPostBack = true;
              dd.DataSource = GetSampleData(10);
              dd.DataTextField = "DESCRIPTION";
              dd.DataValueField = "ID";
              dd.DataBind();
              dd.SelectedIndex = 0;
                    
              controllist.Add(dd);

            }

         }
         catch (Exception) { throw; }
         return controllist;

      }



      private void WireUpEvents(ArrayList controlList)
      {

        CustomControls.TxtBox txt;
        CustomControls.DropDown dd;

        for(int i=0;i<controlList.Count;i++)
        {

           switch (controlList[i].GetType().FullName)
           {

              case "CustomControls.TxtBox":

                    txt = (CustomControls.TxtBox)controlList[i];
                    txt.TextChanged += new System.EventHandler(this.TextBox_TextChanged);
                    break;

              case "CustomControls.DropDown":

                    dd = (CustomControls.DropDown)controlList[i];
                    dd.SelectedIndexChanged += new System.EventHandler(this.DropDownList_SelectedIndexChanged);
                    break;

              default:

    break;

          }
}

      }

      
  private DataTable GetSampleData(int rows)
  {

    DataTable dt = new DataTable();
    DataRow row;
          
    dt.Columns.Add(new DataColumn("ID",
                                   System.Type.GetType("System.Int32")));
    dt.Columns.Add(new DataColumn("DESCRIPTION",
                                   System.Type.GetType("System.String")));

    for(int i=0;i<rows;i++)
    {
      row = dt.NewRow();
      row["ID"] = i;
      row["DESCRIPTION"] = "Item " + i.ToString();
      dt.Rows.Add(row);
    }

    return dt;
  }


  private void TextBox_TextChanged(object sender, System.EventArgs e)
  {
      CustomControls.TxtBox ctrl = (CustomControls.TxtBox)sender;  
      Debug.WriteLine(ctrl.ID + " " + ctrl.Text + " - pair= " +
                      ctrl.NameValuePairs["test1"].ToString());
  }

  private void DropDownList_SelectedIndexChanged(object sender, System.EventArgs e)
  {

     CustomControls.DropDown ctrl = (CustomControls.DropDown)sender;  

     if (ctrl.SelectedItem != null)
     {
        Debug.WriteLine(ctrl.ID + " " + ctrl.Items[ctrl.SelectedIndex].Text +
                        " - pair= " + ctrl.NameValuePairs["test1"].ToString());
     }

  }

      
  }
}



// CustomControls.TxtBox

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;


namespace CustomControls
{

   [DefaultProperty("Text"),
   ToolboxItem(true),
   ToolboxData("<{0}:TxtBox runat=server></{0}:TxtBox>")]
   public class TxtBox : System.Web.UI.WebControls.TextBox
   {
  
      private NameValueCollectionEx colNameValuePairs = new NameValueCollectionEx();


      [Bindable(false),Category("Design")]
      public NameValueCollectionEx NameValuePairs
      {
        get  { return this.colNameValuePairs; }
        set  { this.colNameValuePairs=value; }
      }

  
      protected override void Render(System.Web.UI.HtmlTextWriter writer)
      {
          base.Render(writer);
      }
  

      protected override object SaveViewState()
      {          

        object[] newstate = new object[27];

        try
        {
  
           newstate[0]=(object)this.NameValuePairs.ConvertPairsToString();  
           newstate[1] = (object)this.AutoPostBack;  
           newstate[2] = (object)this.Columns;                              
           newstate[3] = (object)this.MaxLength;  
           newstate[4] = (object)this.TextMode;  
           newstate[5] = (object)this.ReadOnly;  
           newstate[6] = (object)this.Rows;  
           newstate[7] = (object)this.Text;  
           newstate[8] = (object)this.Wrap;  
           newstate[9] = (object)this.AccessKey;  
           newstate[10] = (object)this.BackColor;  
           newstate[11] = (object)this.BorderColor;  
           newstate[12] = (object)this.BorderWidth;  
           newstate[13] = (object)this.BorderStyle;  
           newstate[14] = (object)this.CssClass;  
           newstate[15] = (object)this.Enabled;  
           newstate[16] = (object)this.ForeColor;  
           newstate[17] = (object)this.Height;  
           newstate[18] = (object)this.TabIndex;  
           newstate[19] = (object)this.ToolTip;  
           newstate[20] = (object)this.Width;  
           newstate[21] = (object)this.Site;  
           newstate[22] = (object)this.Visible;  
           newstate[23] = (object)this.Font.Bold;
           newstate[24] = (object)this.Font.Size;
           newstate[25] = (object)this.Font.Italic;
           newstate[26] = (object)this.Font.Name;

        }
catch { }
        return newstate;
     }
    

     protected override void LoadViewState(object savedState)
     {

       try
       {

         object[] newstate = (object[])savedState;
         this.colNameValuePairs.FillFromString((string)newstate[0]);  
         this.AutoPostBack = (System.Boolean)newstate[1];
         this.Columns = (System.Int32)newstate[2];                      
         this.MaxLength = (System.Int32)newstate[3];
         this.TextMode = (System.Web.UI.WebControls.TextBoxMode)newstate[4];
         this.ReadOnly = (System.Boolean)newstate[5];
         this.Rows = (System.Int32)newstate[6];
         this.Text = (System.String)newstate[7];
         this.Wrap = (System.Boolean)newstate[8];
         this.AccessKey = (System.String)newstate[9];
         this.BackColor = (System.Drawing.Color)newstate[10];
         this.BorderColor = (System.Drawing.Color)newstate[11];
         this.BorderWidth = (System.Web.UI.WebControls.Unit)newstate[12];
         this.BorderStyle = (System.Web.UI.WebControls.BorderStyle)newstate[13];
         this.CssClass =  (System.String)newstate[14];
         this.Enabled = (System.Boolean)newstate[15];
         this.ForeColor = (System.Drawing.Color)newstate[16];
         this.Height = (System.Web.UI.WebControls.Unit)newstate[17];
         this.TabIndex =  (System.Int16)newstate[18];
         this.ToolTip =  (System.String)newstate[19];
         this.Width = (System.Web.UI.WebControls.Unit)newstate[20];
         this.Site = (System.ComponentModel.ISite)newstate[21];
         this.Visible = (System.Boolean)newstate[22];
         this.Font.Bold = (System.Boolean)newstate[23];
         this.Font.Size = (System.Web.UI.WebControls.FontUnit)newstate[24];              
         this.Font.Italic = (System.Boolean)newstate[25];
         this.Font.Name = (System.String)newstate[26];

       }
       catch (Exception e)
       {
          Debug.WriteLine(e.Message);
       }
      
     }
    

   }
}

      


CustomControls.DropDown
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;

namespace CustomControls
{

    [DefaultProperty("Text"),
    ToolboxItem(true),
    ToolboxData("<{0}:DropDownList runat=server></{0}:DropDownList>")]
    public class DropDown : System.Web.UI.WebControls.DropDownList
    {

      private NameValueCollectionEx colNameValuePairs = new NameValueCollectionEx();
        
      
      [Bindable(false),Category("Design")]
      public NameValueCollectionEx NameValuePairs
      {
        get  { return this.colNameValuePairs; }
        set  { this.colNameValuePairs=value; }
      }

      protected override void Render(System.Web.UI.HtmlTextWriter writer)
      {
         base.Render(writer);
      }


      protected override object SaveViewState()
      {          

        NameValueCollectionEx items = new NameValueCollectionEx();
        object[] newstate = new object[27];
            
        try
        {

           for(int i=0;i<this.Items.Count;i++)
           {
              items.Add("value" + i.ToString(),this.Items[i].Value);
              items.Add("text" + i.ToString(),this.Items[i].Text);
           }

           newstate[0] = (object)this.NameValuePairs.ConvertPairsToString();  
           newstate[1] = (object)this.AutoPostBack;
           newstate[2] = (object)this.SelectedIndex;
           newstate[3] = (object)items.ConvertPairsToString();
           newstate[4] = (object)this.SelectedValue;
           newstate[9] = (object)this.AccessKey;  
           newstate[10] = (object)this.BackColor;  
           newstate[11] = (object)this.BorderColor;  
           newstate[12] = (object)this.BorderWidth;  
           newstate[13] = (object)this.BorderStyle;  
           newstate[14] = (object)this.CssClass;  
           newstate[15] = (object)this.Enabled;  
           newstate[16] = (object)this.ForeColor;  
           newstate[17] = (object)this.Height;  
           newstate[18] = (object)this.TabIndex;  
           newstate[19] = (object)this.ToolTip;  
           newstate[20] = (object)this.Width;  
           newstate[21] = (object)this.Site;  
           newstate[22] = (object)this.Visible;  
           newstate[23] = (object)this.Font.Bold;
           newstate[24] = (object)this.Font.Size;
           newstate[25] = (object)this.Font.Italic;
           newstate[26] = (object)this.Font.Name;
  
      }
      catch { }
      return newstate;
    }
  

    protected override void LoadViewState(object savedState)
    {
  
      NameValueCollectionEx items = new NameValueCollectionEx();

      try
      {

         object[] newstate = (object[])savedState;

         this.colNameValuePairs.FillFromString((string)newstate[0]);

         items.FillFromString((string)newstate[3]);

         int ListItemCount = 0;

         if (items.Count > 0)
         {

           ListItemCount = items.Count / 2;

           for(int i=0;i<ListItemCount;i++)
           {
              this.Items.Add(new ListItem(items["text" + i.ToString()].ToString(),
                             items["value" + i.ToString()].ToString()));
           }

           this.SelectedIndex = (System.Int32)newstate[2];
           this.SelectedValue = (System.String)newstate[4];

         }

         this.AutoPostBack = (System.Boolean)newstate[1];
         this.AccessKey = (System.String)newstate[9];
         this.BackColor = (System.Drawing.Color)newstate[10];
         this.BorderColor = (System.Drawing.Color)newstate[11];
         this.BorderWidth = (System.Web.UI.WebControls.Unit)newstate[12];
         this.BorderStyle = (System.Web.UI.WebControls.BorderStyle)newstate[13];
         this.CssClass =  (System.String)newstate[14];
         this.Enabled = (System.Boolean)newstate[15];
         this.ForeColor = (System.Drawing.Color)newstate[16];
         this.Height = (System.Web.UI.WebControls.Unit)newstate[17];
         this.TabIndex =  (System.Int16)newstate[18];
         this.ToolTip =  (System.String)newstate[19];
         this.Width = (System.Web.UI.WebControls.Unit)newstate[20];
         this.Site = (System.ComponentModel.ISite)newstate[21];
         this.Visible = (System.Boolean)newstate[22];
         this.Font.Bold = (System.Boolean)newstate[23];
         this.Font.Size = (System.Web.UI.WebControls.FontUnit)newstate[24];              
         this.Font.Italic = (System.Boolean)newstate[25];
         this.Font.Name = (System.String)newstate[26];
  
      }
      catch (Exception e)
      {
          Debug.WriteLine(e.Message);
      }
   }



  }
}

      


UI
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Diagnostics;

namespace TextBoxSample
{

   public class UI
   {

      public static string TextBoxPrefix = "TextBox";
      public static string DropDownPrefix = "DropDown";

      public UI()
      {

      }


      public static void AddControlToCell(TableCell tableCell,
                                          ArrayList controlList,
                                  int currentIndex)
      {

        switch (controlList[currentIndex].GetType().FullName)
        {

          case "CustomControls.TxtBox":

                tableCell.Controls.Add((CustomControls.TxtBox)controlList[currentIndex]);
                break;

          case "CustomControls.DropDown":

                tableCell.Controls.Add((CustomControls.DropDown)controlList[currentIndex]);
                break;

                default:
break;
        }
    
      }


      public static ArrayList CreateControlsFromBrowserForm()
      {

         string key = "";
         string val = "";
         ArrayList controllist = new ArrayList();
         System.Web.HttpContext ctx = System.Web.HttpContext.Current;

         for(int i=0;i<ctx.Request.Form.Count;i++)
         {
          
            key = ctx.Request.Form.GetKey(i);
            val = ctx.Request.Form[key];

            if (key.StartsWith(TextBoxPrefix))
            {
              CustomControls.TxtBox txt = new CustomControls.TxtBox();
              txt.ID = key;
              controllist.Add(txt);
              continue;
            }

            if (key.StartsWith(DropDownPrefix))
            {
               CustomControls.DropDown dd = new CustomControls.DropDown();
               dd.ID = key;
               controllist.Add(dd);
               continue;
            }
                
         }

         return controllist;

      }

  }
}

      


CustomControls.NameValueCollectionEx
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Runtime;
using System.Runtime.Serialization;
using System.Text;
using System.Web;

namespace CustomControls
{

  [SerializableAttribute()]
  public class NameValueCollectionEx:NameValueCollection
  {

    internal string ConvertPairsToString()
    {

       StringBuilder sb = new StringBuilder();
       string Ret="";

       try
       {
         for(int i=0;i<this.Count;i++)
         {
           sb.Append(this.Keys[i].ToString() + "=");
           sb.Append(this.Get(this.Keys[i].ToString()));
           sb.Append("&");
         }

         if (this.Count>0)
         {
           Ret = sb.ToString();
           Ret = Ret.Substring(0,Ret.Length - 1);
         }
       }
       catch { }

       return Ret;

    }



    internal void FillFromString(string s)
    {
      char chNameValueDelim=Convert.ToChar(" ");
      char chPairDelim=Convert.ToChar(" ");
      /*
       You may choose to implement the next three variables
       as parameters to this method and the ConvertPairsToString
       so I've left in certain logic to support this.
      */
      bool urlencoded = false;
      char nameValueDelim = Convert.ToChar(" ");
      char pairDelim = Convert.ToChar(" ");
      Encoding encoding = System.Text.Encoding.ASCII;

      if(nameValueDelim==Convert.ToChar(" "))
      {
        chNameValueDelim ='=';
        chPairDelim='&';
      }
      else
      {
       chNameValueDelim =nameValueDelim;
       chPairDelim=pairDelim;
      }

      int i1 = (s == null) ? 0 : s.Length;
      for (int j = 0; j < i1; j++)
      {
        int k = j;
        int i2 = -1;
        for (; j < i1; j++)
        {
          char ch = s[j];
          if (ch == chNameValueDelim)
          {
            if (i2 < 0)
            {
              i2 = j;
            }
          }
          else if (ch == chPairDelim)
          {
            break;
          }
        }
        string strName = null;
        string strValue = null;
        if (i2 >= 0)
        {
          strName = s.Substring(k, i2 - k);
          strValue = s.Substring(i2 + 1, j - i2 - 1);
        }
        else
        {
          strValue = s.Substring(k, j - k);
        }
        if (urlencoded)
        {
          Add(HttpUtility.UrlDecode(strName, encoding), HttpUtility.UrlDecode(strValue, encoding));
        }
        else
        {
          Add(strName, strValue);
        }
        if (j == i1 - 1 && s[j] == chPairDelim)
        {
          Add(null, "");
        }
       }
    }


  }
}



By Robbe Morris   Popularity  (10359 Views)
Picture
Biography - Robbe Morris
Robbe has been a Microsoft MVP in C# since 2004. He is also the co-founder of NullSkull.com which provides .NET articles, book reviews, software reviews, and software download and purchase advice.  Robbe also loves to scuba dive and go deep sea fishing in the Florida Keys or off the coast of Daytona Beach. Microsoft MVP