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, "");
}
}
}
}
}