Introduction
This article is a follow-up to my article Building ASP.NET Pages Dynamically in the Code-Behind. The previous article presented the concept and included a very basic example. This article shows how to implement a more complicated sample project - collecting data from a form and displaying it back on the page as shown in the screenshot below:
In my previous article, I also emphasized the importance of building your own class library to make the process more efficient. This example provides a richer class library - although still only a small portion of what you could develop for your own projects.
There is a lot of code here and even more in the project. Take the classes for your own use, or just borrow ideas for your own modification. These classes evolved over time for specific projects on which I worked. I wouldn't expect them to fit perfectly into your projects. It is more important to understand the concepts and possibilities.
The Classes
This project makes use of the classes from my previous article. It adds classes to make building forms easier (that is, actual forms where the user enters data - not "forms" in the way ASP.NET refers to every Web page as a form).
ErrorLabel - A Class for Displaying Error Messages
This class is intended to only display error messages. Add the label to your page. When there is no text, the label is not visible. In the catch handler for any exceptions, add the exception to the label and it becomes visible, displaying the error to the user. The appearance of error messages is specified in the CSS file. Remember to use friendly error messages that the user can actually understand!
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Runtime.InteropServices;
public class ErrorLabel : Label
{
public ErrorLabel()
{
CssClass = "errorblock";
Visible = false;
}
public void SetErrorText(string strError)
{
Visible = true;
if (Text == string.Empty)
{
Text = TextWriter.MakeParagraph(strError);
}
else
{
Text += TextWriter.MakeParagraph(strError);
}
}
public void SetErrorText(string strError, Exception e)
{
SetErrorText(strError + " " + e.Message);
}
public void SetErrorText(Exception e)
{
SetErrorText(e.Message);
}
}
MyTextBox - An Enhanced Text Box
The MyTextBox
class is derived from the standard text box. It adds data validation to check for required fields, maximum length, and data type. If the data does not pass validation, an exception is thrown. Code for the class is shown below:
using System;
using System.Web.UI.WebControls;
using System.Net.Mail;
public class MyTextBox : System.Web.UI.WebControls.TextBox
{
public const int tbUnlimited = 0;
public const int tbPassword = -1;
public const int tbInteger = -2;
public const int tbDate = -3;
public const int tbDecimal = -4;
public const int tbTime = -5;
public const int tbEmail = -6;
protected int m_nFormatLength = 0;
protected bool m_bRequired = false;
protected string m_strFieldName = "";
public MyTextBox(string strID, int nFormatLength)
{
ID = strID;
m_nFormatLength = nFormatLength;
TextMode = TextBoxMode.SingleLine;
Rows = 1;
if (nFormatLength == tbPassword)
{
TextMode = TextBoxMode.Password;
}
CssClass = "mytextbox";
}
public void SetMultiline(int nRows)
{
TextMode = TextBoxMode.MultiLine;
Rows = nRows;
}
public void SetDate(DateTime dt)
{
try
{
Text = dt.ToShortDateString();
}
catch
{
Text = "";
}
}
public void SetText(string strValue)
{
Text = strValue;
}
public string GetText()
{
CheckRequired(m_strFieldName);
if (m_nFormatLength > 0)
{
if (Text.Length > m_nFormatLength)
{
throw new Exception("Field " + m_strFieldName +
" exceeds the maximum length of " + Convert.ToString(m_nFormatLength));
}
}
return Text;
}
public DateTime GetDate()
{
CheckRequired(m_strFieldName);
if (Text == string.Empty) return DateTime.FromOADate(0.0);
try
{
return Convert.ToDateTime(Text);
}
catch
{
throw new Exception("Field " + TextWriter.MakeItalicText(m_strFieldName) +
" is not in a valid date format.");
}
}
public DateTime GetTime()
{
CheckRequired(m_strFieldName);
if (Text == string.Empty) return DateTime.FromOADate(0.0);
try
{
return Convert.ToDateTime(Text);
}
catch
{
throw new Exception("Field " + TextWriter.MakeItalicText(m_strFieldName) +
" is not in a valid time format.");
}
}
public int GetInt()
{
CheckRequired(m_strFieldName);
try
{
return Convert.ToInt32(Text);
}
catch
{
throw new Exception("Field " + TextWriter.MakeItalicText(m_strFieldName) +
" is not in a valid integer format.");
}
}
public double GetDouble()
{
CheckRequired(m_strFieldName);
try
{
return Convert.ToDouble(Text);
}
catch
{
throw new Exception("Field " + TextWriter.MakeItalicText(m_strFieldName) +
" is not in a valid numeric format.");
}
}
protected void CheckRequired(string strField)
{
if (m_bRequired == false) return;
if (Text == string.Empty)
{
throw new Exception("Field " + TextWriter.MakeItalicText(strField) +
" is a required value.");
}
}
public void SetRequired(bool bValue, string strFieldName)
{
m_bRequired = bValue;
m_strFieldName = strFieldName;
}
public string GetEmail()
{
CheckRequired(m_strFieldName);
ValidateEmail();
return Text.ToLower();
}
protected void ValidateEmail()
{
if (m_bRequired == false && Text == string.Empty) return;
try
{
MailAddress addr = new MailAddress(Text);
}
catch
{
throw new Exception(TextWriter.MakeItalicText(m_strFieldName) +
" is not a valid email address.");
}
}
}
A Note on Throwing Exceptions
One of the C# technical interview questions available on the Web states that you shouldn't throw exceptions from your own code. One should be careful about blanket statements about what you should or should not do in programming. C# finally gives us a language that has outstanding exception handling. It would be a shame not to make full use of it. This class is a perfect example of when throwing exceptions is appropriate. When data is unacceptable, throw an exception. Your database does it. There is no reason your custom data entry controls shouldn't either.
FormTable - A Table for Building Forms
The FormTable
class allows you to quickly add controls to forms programmatically. It is a table with two columns. The left column has field labels. The right column contains the controls. Note the use of CSS styles throughout.
using System;
using System.Web.UI.WebControls;
public class FormTable : System.Web.UI.WebControls.Table
{
public FormTable()
{
CssClass = "formtable";
this.CellPadding = 2;
this.CellSpacing = 2;
Width = Unit.Percentage(100);
}
public void AddDivider(string strText)
{
Label l = new Label();
l.Text = TextWriter.MakeBoldText(strText);
TableRow row = AddRow();
row.CssClass = "formtabledivider";
TableCell cell = new TableCell();
cell.Width = Unit.Percentage(100);
cell.Controls.Add(l);
row.Cells.Add(cell);
cell.ColumnSpan = 2;
}
public void AddRow(string strText)
{
TableRow row = AddRow();
TableCell cell = new TableCell();
cell.Width = Unit.Percentage(30);
cell.Text = strText;
cell.ColumnSpan = 2;
row.Cells.Add(cell);
}
public void AddRow(System.Web.UI.Control control)
{
TableRow row = AddRow();
TableCell cell = new TableCell();
cell.Width = Unit.Percentage(100);
cell.Controls.Add(control);
row.Cells.Add(cell);
cell.ColumnSpan = 2;
}
public void AddButtonRow(System.Web.UI.Control control)
{
TableRow row = AddRow();
row.HorizontalAlign = HorizontalAlign.Center;
TableCell cell = new TableCell();
cell.Width = Unit.Percentage(100);
cell.Controls.Add(control);
row.Cells.Add(cell);
cell.ColumnSpan = 2;
cell.HorizontalAlign = HorizontalAlign.Center;
}
public void AddRow(string strText, System.Web.UI.Control control, bool bRequired)
{
TableRow row = AddRow();
TableCell cell = new TableCell();
if (bRequired)
{
cell.Text = TextWriter.MakeBoldText(strText) +
TextWriter.Span("required", "*");
}
else
{
cell.Text = strText;
}
cell.Width = Unit.Percentage(30);
row.Cells.Add(cell);
cell = new TableCell();
cell.Controls.Add(control);
cell.Width = Unit.Percentage(70);
row.Cells.Add(cell);
SetControlRequired(control, strText, bRequired);
}
public void AddRequiredLabelRow()
{
string s;
s = TextWriter.Span("required", "*") + "" +
TextWriter.MakeBoldText("Required Value");
AddRow(s);
}
protected TableRow AddRow()
{
TableRow row = new TableRow();
Rows.Add(row);
row.Width = Unit.Percentage(100);
return row;
}
protected void SetControlRequired(System.Web.UI.Control control,
string strFieldName, bool bRequired)
{
if (control is MyTextBox)
{
MyTextBox tb = (MyTextBox)control;
tb.SetRequired(bRequired, strFieldName);
}
}
}
ContactData - The Data Object
We need an object to hold the data from our form. We could specify the members as properties, but I am an old C++ programmer and old habits die hard. I like to avoid properties with getters and setters for two reasons:
- I often overload the getter or setter passing different parameters.
- I like all of my
Get
and Set
functions to appear in a group in the Intellisense popup list. If you leave them as properties, they are all over the list based on their name.
public class ContactData
{
protected string m_strFirstName = "";
protected string m_strLastName = "";
protected string m_strAddress1 = "";
protected string m_strCity = "";
protected string m_strStateName = "";
protected string m_strStateCode = "";
protected string m_strCountryName = "";
protected string m_strCountryCode = "";
protected string m_strPostalCode = "";
protected DateTime m_dtBirthday = DateTime.MinValue;
public ContactData()
{
}
public string GetFirstName()
{
return m_strFirstName;
}
public string GetLastName()
{
return m_strLastName;
}
public void SetFirstName(string strNew)
{
m_strFirstName = strNew;
}
public void SetLastName(string strNew)
{
m_strLastName = strNew;
}
}
ContactFormTable - A FormTable for Collecting Contact Data
Now we put all of the classes together to see how we easily create a table to collect contact data. If this was the only data entry form in our project, it would be a lot of work. However, on a large project with many pages of forms, it is nice to be able to build them out so easily.
ContactFormTable
is derived from FormTable
. The form is filled from a ContactData
object. On submit, the form populates a ContactDataObject
.
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public class ContactFormTable : FormTable
{
protected MyTextBox m_FirstNameTextBox;
protected MyTextBox m_LastNameTextBox;
protected MyTextBox m_AddressTextBox;
protected MyTextBox m_CityTextBox;
protected MyTextBox m_PostalCodeTextBox;
protected StateComboBox m_StateComboBox;
protected CountryComboBox m_CountryComboBox;
protected MyTextBox m_BirthdayTextBox;
public ContactFormTable()
{
BuildForm();
}
protected void BuildForm()
{
m_FirstNameTextBox = new MyTextBox("FirstName", MyTextBox.tbUnlimited);
AddRow("First Name", m_FirstNameTextBox, true);
m_LastNameTextBox = new MyTextBox("LastName", MyTextBox.tbUnlimited);
AddRow("Last Name", m_LastNameTextBox, true);
m_AddressTextBox = new MyTextBox("Address", 100);
AddRow("Address", m_AddressTextBox, false);
m_CityTextBox = new MyTextBox("City", 100);
AddRow("City", m_CityTextBox, false);
m_StateComboBox = new StateComboBox("State");
AddRow("State", m_StateComboBox, false);
m_CountryComboBox = new CountryComboBox("Country");
AddRow("Country", m_CountryComboBox, false);
m_PostalCodeTextBox = new MyTextBox("PostalCode", 10);
AddRow("Postal Code", m_PostalCodeTextBox, false);
AddDivider("Personal Information");
m_BirthdayTextBox = new MyTextBox("Birthday", MyTextBox.tbDate);
AddRow("Birthday", m_BirthdayTextBox, false);
AddRequiredLabelRow();
}
public void FillForm(ContactData contact)
{
m_FirstNameTextBox.SetText(contact.GetFirstName());
m_LastNameTextBox.SetText(contact.GetLastName());
m_AddressTextBox.SetText(contact.GetAddress1());
m_CityTextBox.SetText(contact.GetCity());
m_PostalCodeTextBox.SetText(contact.GetPostalCode());
m_StateComboBox.SelectItemByValue(contact.GetStateCode());
m_CountryComboBox.SelectItemByValue(contact.GetCountryCode());
m_BirthdayTextBox.SetDate(contact.GetBirthday());
}
public void GetFormData(ContactData contact)
{
contact.SetFirstName(m_FirstNameTextBox.GetText());
contact.SetLastName(m_LastNameTextBox.GetText());
contact.SetAddress1(m_AddressTextBox.GetText());
contact.SetCity(m_CityTextBox.GetText());
contact.SetPostalCode(m_PostalCodeTextBox.GetText());
contact.SetBirthday(m_BirthdayTextBox.GetDate());
contact.SetStateCode(m_StateComboBox.GetSelectedValue());
contact.SetCountryCode(m_CountryComboBox.GetSelectedValue());
contact.SetStateName(m_StateComboBox.GetSelectedText());
contact.SetCountryName(m_CountryComboBox.GetSelectedText());
}
}
Note that I used a couple of other classes - StateComboBox
and CountryComboBox
. These are included in the project.
ContactInfoPanel - Display Contact Information on the Page
We also need to display our contact information on the page. I do this using the ContactInfoPanel
class. This is derived from MyPanel
- a class described in my previous article. It displays the rectangle of contact information above the form in the screenshot.
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public class ContactInfoPanel : MyPanel
{
public ContactInfoPanel() : base("contactpanel")
{
}
public void BuildPanel(ContactData contact)
{
Controls.Clear();
AddLiteral(TextWriter.MakeH2Text("Contact Information"));
AddContactInfo("First Name", contact.GetFirstName());
AddContactInfo("Last Name", contact.GetLastName());
AddContactInfo("Address", contact.GetAddress1());
AddContactInfo("City", contact.GetCity());
AddContactInfo("State Name", contact.GetStateName());
AddContactInfo("State Code", contact.GetStateCode());
AddContactInfo("Country Name", contact.GetCountryName());
AddContactInfo("Country Code", contact.GetCountryCode());
AddContactInfo("Birthday", contact.GetBirthday().ToShortDateString());
}
protected void AddContactInfo(string strFieldName, string strValue)
{
string s;
s = TextWriter.MakeBoldText(strFieldName) + "" +
System.Web.HttpUtility.HtmlEncode(strValue);
AddLiteral(TextWriter.MakeLine(s));
}
}
The Pages
Those are the important classes for the project. Now let's see the look on the page.
As I showed in Part 1, the only important line of code in the page markup is the placement of a PlaceHolder
control. This PlaceHolder
will contain all of the controls on the page.
<asp:PlaceHolder id="LocalPlaceHolder" runat="server"></asp:PlaceHolder>
The Code-Behind
Here is the code-behind for the form. I add the ErrorLabel
, the ContactInfoPanel
, the form itself, a button and a few other aesthetic elements. The button gets an event handler which reads data from the form and displays it back on the panel. One thing I like about this method is the way exceptions are handled in the Page_Load
event. Creation of all of the controls is wrapped in an exception handler. Any exception thrown displays an error in the ErrorLabel
. Also wrap the form processing in an event handler. This event handler will catch any form validation errors.
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public partial class ContactForm : BasePage
{
protected ContactFormTable m_FormTable;
protected ErrorLabel m_ErrorLabel;
protected ContactInfoPanel m_ContactPanel;
protected ContactData m_Contact = null;
protected void Page_Load(object sender, EventArgs e)
{
try
{
m_Contact = new ContactData();
m_ErrorLabel = new ErrorLabel();
LocalPlaceHolder.Controls.Add(m_ErrorLabel);
MyLiteral lit = new MyLiteral(TextWriter.MakeH1Text
("Enter Contact Information"));
LocalPlaceHolder.Controls.Add(lit);
m_ContactPanel = new ContactInfoPanel();
LocalPlaceHolder.Controls.Add(m_ContactPanel);
m_ContactPanel.BuildPanel(m_Contact);
MyPanel spacer = new MyPanel("spacer");
LocalPlaceHolder.Controls.Add(spacer);
m_FormTable = new ContactFormTable();
LocalPlaceHolder.Controls.Add(m_FormTable);
SubmitButtonTable buttontable = new SubmitButtonTable("Submit");
LocalPlaceHolder.Controls.Add(buttontable);
buttontable.m_Button.Click +=new EventHandler(SubmitButton_Click);
}
catch (Exception ex)
{
m_ErrorLabel.SetErrorText(ex);
}
}
protected void SubmitButton_Click(object sender, EventArgs e)
{
try
{
m_FormTable.GetFormData(m_Contact);
m_ContactPanel.BuildPanel(m_Contact);
}
catch (Exception ex)
{
m_ErrorLabel.SetErrorText(ex);
}
}
}
An Alternative Postback Processing Method
If you want a more classic ASP approach to handling form postback, you can create a function for handling postback in the code behind and call it from a script block in the HTML. Make sure your script block occurs before the PlaceHolder! Otherwise, it won't work. The HTML would be modified to look like this:
<!---->
<%
if (IsPostBack)
{
ProcessPostback();
}
%>
<asp:PlaceHolder id="LocalPlaceHolder" runat="server"></asp:PlaceHolder>
In the code-behind, we would omit the button event handler and instead implement this function called from the markup.
protected void ProcessPostback()
{
if (IsPostBack == false) return;
try
{
m_FormTable.GetFormData(m_Contact);
m_ContactPanel.BuildPanel(m_Contact);
}
catch (Exception ex)
{
m_ErrorLabel.SetErrorText(ex);
}
}
The Project
The project was written using Visual Studio 2005 and ASP.NET 2.0 in C#.
Conclusion
As I stated in the first article, the idea of moving all presentation to the code-behind, leaving nothing but a place holder in the markup, is a little extreme and may not be for every project. However, it is an important concept to understand. This article shows how you can extend the basic .NET classes to make building pages dynamically easier. Even if you do not adopt the technique in full, learning to enhance the basic .NET classes and learning to implement a better class design in the code-behind are important tools to improve your ASP.NET development.
History
- 6th May, 2008: Initial post