Click here to Skip to main content
Click here to Skip to main content

Building ASP.NET Web Pages Dynamically in the Code-Behind - Part 2: A Form Example

By , 6 May 2008
Rate this:
Please Sign up or sign in to vote.

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:

DynamicFormProject

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;

/// <summary />
/// Class for displaying error messages
/// <span class="code-SummaryComment"></summary></span>
public class ErrorLabel : Label
{
    public ErrorLabel()
    {
        CssClass = "errorblock"; // set the CSS style
        Visible = false; // keep label hidden unless there is text
    }

    public void SetErrorText(string strError)
    {
        Visible = true; // Make it visible

        // add or append the error message
        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;

/// <span class="code-SummaryComment"><summary></span>
/// Summary description for MyTextBox.
/// <span class="code-SummaryComment"></summary></span>
public class MyTextBox : System.Web.UI.WebControls.TextBox
{
    // these constants tell us what kind of data this box accepts
    // they are negative because any positive value is interpreted as 
    // a maximum text length
    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;

    // FormatLength is the format value from the constants above
    // if it is a positive value, it is the maximum length for the data field
    protected int m_nFormatLength = 0;

    // specifies if the value is required
    protected bool m_bRequired = false;

    // FieldName is accepted for display in error messages
    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)
        {
            // in this case, a maximum length is specified
            if (Text.Length > m_nFormatLength)
            {
                // data exceeds the maximum length, so throw an exception
                // note use of field name in the exception
                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)
    {
        // function checks to see if value is required and provided
        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;

/// <span class="code-SummaryComment"><summary></span>
/// A class for easily creating forms with columns of controls.
/// <span class="code-SummaryComment"></summary></span>
public class FormTable : System.Web.UI.WebControls.Table
{
    public FormTable()
    {
        CssClass = "formtable";
        this.CellPadding = 2;
        this.CellSpacing = 2;
        Width = Unit.Percentage(100);
    }

    // this function adds a horizontal row with a caption
    // to divide the form into sections
    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;
    }

    // add a row of text that goes all the way across the width of the form
    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);
    }

    // add a row of a control that goes all the way across the width of the form
    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;
    }

    // add a row with a control - typically a button
    // centered across the width of the form
    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;

    }

    // add a row of with text for a field label and a control for accepting data
    // text is in left column, control is in right column
    // this is the function used most often
    public void AddRow(string strText, System.Web.UI.Control control, bool bRequired)
    {
        TableRow row = AddRow();

        TableCell cell = new TableCell();

        if (bRequired)
        {
            // Add the required *
            // Note the use of a CSS class to control display
            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);
    }

    // Adds a row to the table indicating that * fields are required
    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)
        {
            // Only controls of MyTextBox support the required attribute
            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:

  1. I often overload the getter or setter passing different parameters.
  2. 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.

/// <span class="code-SummaryComment"><summary></span>
/// This is a class to hold contact data
/// It can include functions to read/write database if desired
/// <span class="code-SummaryComment"></summary></span>
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()
    {
        //
        // TODO: Add constructor logic here
        //
    }

    /////////////////////////////////////////////////////////////////
    //  Get functions

    public string GetFirstName()
    {
        return m_strFirstName;
    }

    public string GetLastName()
    {
        return m_strLastName;
    }

// Code removed for brevity

    ///////////////////////////////////////////////////////////
    //  Set functions

    public void SetFirstName(string strNew)
    {
        m_strFirstName = strNew;
    }

    public void SetLastName(string strNew)
    {
        m_strLastName = strNew;
    }

// Code removed for brevity

} // end of class declaration

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;

/// <span class="code-SummaryComment"><summary></span>
/// This is the table that allows users to enter contact information
/// <span class="code-SummaryComment"></summary></span>
public class ContactFormTable : FormTable
{
    // declare the controls for the form
    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()
    {
        // Build out the form - note that first and last name are required
        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);

        // in this case, the integer 100 is the maximum length for the value
        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");

        // add a text box for the birthday and specify that it should be date format
        m_BirthdayTextBox = new MyTextBox("Birthday", MyTextBox.tbDate);
        AddRow("Birthday", m_BirthdayTextBox, false);

        // add the row indicating required values
        AddRequiredLabelRow();
    }

    public void FillForm(ContactData contact)
    {
        // fill the form from the contact object
        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());

        // note that for the combo boxes, we are selecting by value - not by text
        m_StateComboBox.SelectItemByValue(contact.GetStateCode());
        m_CountryComboBox.SelectItemByValue(contact.GetCountryCode());

        // set the birthday as a date
        m_BirthdayTextBox.SetDate(contact.GetBirthday());
    }

    public void GetFormData(ContactData contact)
    {
        // build the contact object from form values
        // this function will throw an exception if form data is not complete

        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());

        // get state and country names and codes
        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;

/// <span class="code-SummaryComment"><summary></span>
/// Class to display a panel of contact information
/// <span class="code-SummaryComment"></summary></span>
public class ContactInfoPanel : MyPanel
{
    // A call to the base class specifies the CSS style for this panel
    public ContactInfoPanel() : base("contactpanel")
    {

    }

    public void BuildPanel(ContactData contact)
    {
        // Build the panel from a contact object
        Controls.Clear(); // remove any existing controls

        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;

        // be sure to safely encode to HTML any text that is entered
        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
{
    // here are some of the controls that will appear on our page
    protected ContactFormTable m_FormTable;
    protected ErrorLabel m_ErrorLabel;
    protected ContactInfoPanel m_ContactPanel;

    // here is the contact data object to store form data
    protected ContactData m_Contact = null;

    protected void Page_Load(object sender, EventArgs e)
      {
        try
        {
            // create the contact object
            m_Contact = new ContactData();

            // add the error label as the first control so errors are 
            // at the top of the page
            m_ErrorLabel = new ErrorLabel();
            LocalPlaceHolder.Controls.Add(m_ErrorLabel);

            // add some title information
            MyLiteral lit = new MyLiteral(TextWriter.MakeH1Text
                ("Enter Contact Information"));
            LocalPlaceHolder.Controls.Add(lit);

            // build the contact info panel
            m_ContactPanel = new ContactInfoPanel();
            LocalPlaceHolder.Controls.Add(m_ContactPanel);
            m_ContactPanel.BuildPanel(m_Contact);

            // add a spacer for aesthetics
            MyPanel spacer = new MyPanel("spacer");
            LocalPlaceHolder.Controls.Add(spacer);

            // Add the form
            m_FormTable = new ContactFormTable();
            LocalPlaceHolder.Controls.Add(m_FormTable);

            // Add a submit button - the table centers it
            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
        {
            // get the data from the form.  Remember that any missing or invalid data
            // will throw an exception, so wrap this in an exception handler
            m_FormTable.GetFormData(m_Contact);

            // now fill the contact panel with the updated data
            m_ContactPanel.BuildPanel(m_Contact);
        }
        catch (Exception ex)
        {
            m_ErrorLabel.SetErrorText(ex);
        }
    }
} // end of class

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:

<!--<span class="code-comment"> Make sure you call process postback operations BEFORE the placeholder --></span>
<%

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()
{
    // this function is called from the page directly on any postback event.
    // Another option is to use a button click event handler

    if (IsPostBack == false) return; // A little extra error checking

    try
    {
        // get the data from the form.  Remember that any missing or invalid data
        // will throw an exception, so wrap this in an exception handler
        m_FormTable.GetFormData(m_Contact);
    
        // now fill the contact panel with the updated data
        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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Robert Pittenger, MCPD-EAD
President Starpoint Software Inc.
United States United States
Bob Pittenger is founder and President of Starpoint Software Inc. He holds a B.A. degree from Miami University, M.S. and Ph.D. degrees from Purdue University, and an MBA from Xavier University. He has been programming since 1993, starting with Windows application development in C++/MFC and moving to C# and .NET around 2005 and is a .NET Microsoft Certified Professional Developer.
 
Bob is the author of two books:
Billionaire: How the Ultra-Rich Built Their Fortunes Through Good and Evil and What You Can Learn from Them
and
Wealthonomics: The Most Important Economic and Financial Concepts that Can Make You Rich Fast.
Visit http://www.billionairebook.net for more information.

Comments and Discussions

 
General[My vote of 1] Stop It! Pinmemberdblankenship23-Feb-11 6:37 
GeneralMy vote of 1 PinmemberDavej7516-Feb-11 9:06 
GeneralYou did it wrong PinmemberS432**%$14-May-08 4:40 
GeneralAny sample to read the data from a database Pinmembermohanjohn13-May-08 4:17 
GeneralRe: Any sample to read the data from a database PinmemberRobert Pittenger13-May-08 10:31 
GeneralRe: Any sample to read the data from a database Pinmembermohanjohn14-May-08 0:58 
QuestionWhy do you make your own validation controls? Pinmemberhighandhigher7-May-08 3:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140415.2 | Last Updated 6 May 2008
Article Copyright 2008 by Robert Pittenger, MCPD-EAD
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid