Click here to Skip to main content
15,881,413 members
Articles / Web Development / ASP.NET
Article

Web Application Page Patterns

Rate me:
Please Sign up or sign in to vote.
4.47/5 (4 votes)
23 Jan 200710 min read 47.1K   349   53   9
Two common design patterns for web application pages: the Single Entity Postback Editor and the Multi-Entity Postback Editor

Introduction

After many years of solving the same problems over and over you'd think I'd have learned something. This was my thought after going through a final round of refactoring of our latest application. It was a thing of wonder to look at the multitude of ways we had developed for simple record editing pages. Some places we were building the entities in Load and saving them in event handlers. In some places we were doing everything in the Load event. Some places were saving in PreRender. And some places were only saving about 75% of the time.

One interesting thing about many patterns is how obvious they seem once you see them, and see them we shall. Presented in this article will be two patterns for common web application pages: The Single Entity Postback editor and the Multiple Entity Postback editor.

Background and Suppositions

Both patterns have a reliance on the Entity/Factory pattern set. In the course of this article I'll use the terms Object Relation Modeling (ORM) and Entity/Factory interchangably. While there are definitely differences, as a broad subject encompassing patterns, tools, techniques, and concrete code, they are close enough in meaning for my purposes. You can look up more information in the Resources section at the end of the article.

For the sake of simplicity and access, I built this example on top of MyGeneration dOOdads This is a freely available tool/framework that will be adequate to show these patterns. The dOOdads framework is not a pure Entity/Factory pattern set, but is a hybrid pattern I'll call Smart Entity for lack of a better term. All this means is that the factory methods are integrated into the entity. This reduces the number of classes, but increases the size/complexity of the base classes. At the end of the article I'll touch on some items to consider when you pick your ORM framework.

Single Entity Postback Editor

The Single Entity Postback pattern is used for updating a single entity (usually a row in a database) with a web page.

Let's start by boiling this pattern down to it's simplest form. We'll do that by looking at what steps must be accomplished for this pattern to work:

Loading an Entity Record

  1. Determine ID of entity record for page.
  2. Retrieve entity data from datastore into page-level object.
  3. Load data from object into form.
  4. Render page.

Saving an Entity Record

  1. Determine ID of entity record for page.
  2. Retrieve entity from datastore into page-level object.
  3. Load postback data into entity object.
  4. Save object to data store.
  5. Render page.

Sounds trivial, but I can count on both hands and both feet the number of code paths I've seen for doing this. From doing a column-level get and column-level set to tripple gets, reloads, and non-saving saves. It makes my head spin just to think about it.

Abstract PostbackEditor Page Class

Now that we have the basic execution flow for the pattern, let's abstract out the design and see what else we need. The class presented below implements the logic described above. Concrete implementations will need to provide specific implementations of the methods to:

  • Place entity values into form controls: LoadEntityIntoForm()
  • Update entity values with data posted back by client: LoadFormValuesIntoEntity()
  • Coordinate Factory and Entity to populate based on Record ID: LazyLoadEntity()
  • Coordinate Factory and Entity to persist data to data store: Save()

Other than that, all the default wiring and logic is in place. This class makes an a default assumption that the record ID is an Integer (handling other ID schemes will be discussed below) and we are using a parameter named recid for both QueryString and Form control values. Because the code assumes a predicatable name value for the form field in the Request.Form collection you should stay away from the heavyweight asp server controls as they tend to garble the form field name when they render. You will also need to avoid lighter (but still heavy) HtmlControls that have the runat="server" attribute if you are using Master pages or User controls.

C#
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;

/// <summary>
/// Abstract page class for supporting postback
/// entity editor pattern
/// </summary>
abstract public class PostbackEditor : System.Web.UI.Page
{
    private object _myEditingEntity = null;
    private object _myEntityFactory = null;
    private int _recordID;


    #region Supporting Entity/Factory and ID properties
    /// <summary>
    /// Primary key value for this page entity.
    /// Assumes all entities utilize an integer key
    /// </summary>
    virtual public int RecordID
    {
        get { return _recordID; }
        set { _recordID = value; }
    }

    /// <summary>
    /// Entity object being edited by this postback page
    /// </summary>
    public object MyEditingEntity
    {
        get {
            if (_myEditingEntity == null)
            {
                _myEditingEntity = LazyLoadEntity();
            }
            return _myEditingEntity; }
        set { _myEditingEntity = value; }
    }

    /// <summary>
    /// Factory class that will handle creating, retrieving,
    /// and saving out entity.
    /// </summary>
    public object MyEntityFactory
    {
        get {
            if (_myEntityFactory == null)
            {
                _myEntityFactory = LazyLoadFactory();
            }
            return _myEntityFactory;
        }
        set { _myEntityFactory = value; }
    }
    #endregion


    #region Abstract methods the concrete editing page must implement

    /// <summary>
    /// Method to save MyEditingEntity back to persistence layer
    /// </summary>
    abstract protected bool Save();

    /// <summary>
    /// Transfer entity data values into the form prior to render
    /// </summary>
    abstract protected void LoadEntityIntoForm();

    /// <summary>
    /// Load the posted values back into the form
    /// </summary>
    abstract protected void LoadFormValuesIntoEntity();

    /// <summary>
    /// Load the entity desired based on RecordID
    /// </summary>
    /// <returns></returns>
    abstract protected object LazyLoadEntity();

    /// <summary>
    /// Load the factory associated with entity
    /// </summary>
    /// <returns></returns>
    abstract protected object LazyLoadFactory();

    #endregion


    /// <summary>
    /// Default constructor for page.  This wires the default load event.
    /// </summary>
    public PostbackEditor()
    {
        this.Load += new EventHandler(Page_Load);
    }

    /// <summary>
    /// Load event for editor page.  This contains the execution
    /// logic for processing.  If you override, you must call
    /// this base load method.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    virtual protected void Page_Load(object sender, EventArgs e)
    {
        GetRecordIDValue();
        if (IsPostBack)
        {
            this.LoadFormValuesIntoEntity();
            this.Save();
        }
        else
        {
            this.LoadEntityIntoForm();
        }

    }

    /// <summary>
    /// Attempts to determine record ID from QueryString or
    /// form values if postback.  Use param <b>recid</b> for both
    /// querystring and form field name holding the ID.
    /// </summary>
    /// <remarks>
    /// If no value is found page assumes you are creating a new record.
    /// Default implementation uses parameter <b>recid</b> to pass in the
        /// ID value. By default this code assumes the ID is an integer.
    /// </remarks>
    virtual protected void GetRecordIDValue()
    {
        int myId = 0;
        string tmpVal = null;
        if (IsPostBack)
        {
            //look for post value in form collection directly
            //avoid using ASP.Net controls as they won't reliably
            //populate in all scenarios
            tmpVal = Request.Form["recid"];
        }
        else
        {
            tmpVal = Request.QueryString["recid"];
        }
        //if empty assume new
        if(tmpVal != null)
        {
            if (!Int32.TryParse(tmpVal, out myId))
            {
                myId = 0;
            }
        }
        this.RecordID = myId;
    }

}

Now we have the basic skeletal page class.

Single Entity Postback Implementation

In the sample code you will find two instances of the Single-Entity Postback pattern: CustomerEditor and ProductEditor. The CustomerEditor is the clearest, so it's the one we'll look at. We have to reference our ORM framework and implement the four abstract methods to make this work.

One could embed the ORM (Entity/Factory business logic) layer in the main website project, but I find it to be cleaner and more maintainable to place it in a separate dll. As the ORM code in the sample is vanilla MyGeneration dOOdads, I'll leave it to you to explore in the code and with their tools.

C#
//supporting ORM framework
using WebAppPatterns.Bizlayer;
using MyGeneration.dOOdads;

Next we have to change the Page class to extend our PostbackEditor class instead of the default System.Web.UI.Page class. We should also delete the Page_Load method as this is now handled in PostbackEditor. If you want to use the load event you must call base.Page_Load in your implementation for the base code to function properly. See the ProductEditor page for an example of this.

C#
public partial class CustomerEditor : PostbackEditor

Our implementations of the abstract methods are very straight forward.

/// <summary>
/// Loads entity values into the form.
/// </summary>
protected override void LoadEntityIntoForm()
{
    Customer entity = (Customer)this.MyEditingEntity;
    this.recid.Value = entity.CustomerID.ToString();
    this.txtName.Value = entity.Name;
    this.txtAddress.Value = entity.Address;
}

/// <summary>
/// Loads posted values from the form back into the entity.
/// </summary>
protected override void LoadFormValuesIntoEntity()
{
    Customer entity = (Customer)this.MyEditingEntity;
    entity.Name = txtName.Value;
    entity.Address = txtAddress.Value;
}

/// <summary>
/// Saves changes to <c>MyEditingEntity</c> back to the database.
/// </summary>
/// <returns></returns>
protected override bool Save()
{
    Customer entity = (Customer)this.MyEditingEntity;
    entity.Save();
    lblResult.Text = "Saved Customer " + entity.CustomerID.ToString();
    return true;
}

#region Lazy Load methods

/// <summary>
/// Loads the entity object based on page record id
/// </summary>
/// <returns></returns>
protected override object LazyLoadEntity()
{
    Customer entityObj = new Customer();
    entityObj.LoadByPrimaryKey(this.RecordID);
    if (entityObj.EOF)
    {
        entityObj.AddNew();
        entityObj.CustomerID = 0;
    }

    return entityObj;
}

/// <summary>
/// Load factory class. dOOdads utilize an integrated entity/factory
/// object, so this also points to a Customer
/// </summary>
/// <returns></returns>
protected override object LazyLoadFactory()
{
    return new Customer();
}
#endregion

Based on your ORM framework, implementations will vary subtly, but the overall logic remains the same. If we now fire up and test this code - Viola! The page handles both edits of existing objects and creation of new objects.

Multiple Entity Postback Page

The Multiple Entity Postback pattern is really a varient of the Single Entity Postback pattern. This pattern is used when multiple records in the same table must be updated simultaneously. It often falls from a parent-child record relationship. In our example we'll use the Order - OrderDetail paradigm from the familiar e-commerce application.

In implementations I have found two variations of this pattern: simple multi-entity and complex multi-entity. Simple multi-entity is when there is only a boolean decision - either the relationship exists or it does not exist. Complex multi-entity implementations require you to set one or more values with each relationship. In our example we'll use a complex implementation.

Simple Example: A site user is presented with a list of 20 specialty newsletters they may subscribe to. Each subscription is represented by a checkbox.
Complex Example: A lunch order is placed for 5 turkey sandwiches, 3 roast beef, and 4 Cesar salads.

This pattern works by using matching (simple) or similar (complex) form field names when collecting data. This allows you to iterate the Request.Form collection and get your values without any prior knowledge of the data, ViewState dependencies, or spurious re-binding.

The OrderEditor in the sample code is our implementation of this pattern. As you can see from the code, it uses the same abstract base page and overall framework as the Single Entity Postback implementation.

public partial class OrderEditor : PostbackEditor
{
...
}

This pattern differs mostly in how we will construct and name our controls and how we extract the values.
Rule #1: You cannot use the built-in postback controls to implement this pattern. Microsoft, in their infinite wisdom, always overrides the name attribute of form fields with the ID value. As a result, the form post results come back with mangled name/value pairs, which will break the pattern. My advice is to either use <asp:PlaceHolder ... /> controls and manually build your control strings or implement custom controls that will behave properly.

Magic Names

The "magic", as it were, comes from how we name our form controls. Something many web developers seem to have forgotten with their wanton thirst for heavyweight controls and the convenience of the ASP.Net event model is how the browser submits form data. Name/value pairs are submitted to the server in the form name1=value1&name2=value2.... When two controls happen to have the same name, they are submitted together like name1=value1,value2....

Consider the check boxes shown below: 
Example Check Boxes

<input type="checkbox" name="newsletters" value="1" checked="checked" /> 
Bike News

<input type="checkbox" name="newsletters" value="2" /> Rider Alerts

<input type="checkbox" name="newsletters" value="3" checked="checked" /> 
Jumping Contests

<input type="checkbox" name="newsletters" value="4" checked="checked" /> 
Weather Watch

If written in standard HTML the above check boxes, when submitted back to the page, will generate the value: newsletters=1,3,4. This, in turn, yeilds the string 1,3,4 from Request.Form["newsletters"], which can then be split and parsed. From here, save the new set of foreign key IDs either in bulk (single SQL call) or individually.

If we were to use the asp CheckBoxList control, by contrast (which is the closest equivilent control I have found), the underlying code looks quite different.

HTML
<table id="CheckBoxList1" border="0">
<tr>
   <td><input id="CheckBoxList1_0" type="checkbox" name="CheckBoxList1$0" 
      checked="checked" /><label for="CheckBoxList1_0">Bike News</label></td>
</tr><tr>
    <td><input id="CheckBoxList1_1" type="checkbox" name="CheckBoxList1$1" />
    <label for="CheckBoxList1_1">Rider Alerts</label></td>
    </tr><tr>
    <td><input id="CheckBoxList1_2" type="checkbox" name="CheckBoxList1$2" 
    checked="checked" /><label for="CheckBoxList1_2">Jumping Contests</label>
    </td>
    </tr><tr>
    <td><input id="CheckBoxList1_3" type="checkbox" name="CheckBoxList1$3" 
    checked="checked" /><label for="CheckBoxList1_3">Weather Watch</label>
    </td>
    </tr>
</table>

This control, by contrast, yeilds something quite different in the posted form variables:
CheckBoxList1$0=on&CheckBoxList1$2=on&CheckBoxList1$3=on
You could guess at the control names, but it would be error prone and is not recommended. Not only that, you must either have the ViewState turned on or re-bind the data in order for the control to even acknowledge that any values exist.

Complex Values

For complex values a similar approach is taken. The difference is that the ID is embedded within the field name instead of carried as a value. As a result you will have to iterate the entire Request.Forms collection.

Example: Let's say you want to trap an order for 14 pork sandwiches and 3 salads.

HTML
Pork Sandwich: <input type="text" name="fooditem_4" value="14" />
Salad: <input type="text" name="fooditem_5" value="3" />

You then parse the Request.Form collection for any control containing fooditem_ and extract the ID and value. This keeps you away from any messy re-binding or ViewState issues you might otherwise encounter with a grid or repeater control.

Order Editor Example

In our example, we override the Page_Load event. Nothing is done here, but I wanted to include it for the sake of completeness. If you wished to perform any special load logic, create helper objects, etc. or render controls that exist outside the entity load, this would be the place.

C#
/// <summary>
/// New load event must override load in the PostbackEditor base page
/// and call that method to invoke the required functionality.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
override protected void Page_Load(object sender, EventArgs e)
{
    System.Diagnostics.Debug.WriteLine("Begin Loading Order");
    base.Page_Load(sender, e);
}

The product list is loaded, which will render input boxes all with the name prodqty_N where N is the ID of the product. On submission of the form we first load the data into our object framework, then save it.

C#
    /// <summary>
    /// Load the entered values into our AnOrder entity object
    /// </summary>
    /// <remarks>
    /// In this method we set the top level values and create the sub-items.
    /// During the save we'll have to populate the foreign key values of
    /// our sub-items
    /// </remarks>
    protected override void LoadFormValuesIntoEntity()
    {
        AnOrder order = (AnOrder)MyEditingEntity;

        //Get the posted customer ID from the Request.Forms collection.
        //This circumvents databinding sequence problems found with normal 
        // postback controls.
        order.CustomerID = Convert.ToInt32(Request.Form["selCustomerId"]);

        //Now cycle the form and look for our known values, extracting the 
        //ID from the name
        string[] formKeys = Request.Form.AllKeys;
        int pos;
        for (int i = 0; i < formKeys.Length; i++)
        {
            //If we find our marker in the name, extract the id
            //and create a new detail entry
            pos = formKeys[i].IndexOf(DETAIL_QUANTITY_CTLNAME);
            if (pos > -1)
            {
                int prodId = 0;
                if (Int32.TryParse(formKeys[i].Substring((pos + 
                              DETAIL_QUANTITY_CTLNAME.Length) ), out prodId))
                {
                    //next loop if empty
                    if (Request.Form[formKeys[i]].Length == 0) continue;

                    int qty = 0;
                    Int32.TryParse(Request.Form[formKeys[i]], out qty);
                    if (qty > 0)
                    {
                        MyOrderDetails.AddNew();
                        MyOrderDetails.ProductID = prodId;
                        MyOrderDetails.Quantity = qty;
                    }
                }
            }

        }

    }

...

    /// <summary>
    /// Save the values back to the database.
    /// </summary>
    /// <remarks>
    /// Because of how MyGeneration dOOdads work, we have to
    /// loop thru the details after saving the main item to apply
    /// our new ID.
    /// </remarks>
    /// <returns></returns>
    protected override bool Save()
    {
        AnOrder order = (AnOrder)MyEditingEntity;
        order.Save();
        int orderId = order.OrderID;

        //rewind, apply our new orderID, and save
        MyOrderDetails.Rewind();
        do
        {
            MyOrderDetails.OrderID = orderId;
        } while (MyOrderDetails.MoveNext());

        //dOOdads perform a batch save since they are an abstraction on top 
        //of a data table
        MyOrderDetails.Save();

        return true;
    }

And that's all there is to it.

Conclusion

I have found these to be a couple of very reliable and maintainable patterns. After refactoring this last application, I now apply this to all my postback editing pages. It has enabled me to avoid making stupid mistakes, like rendering but not saving my data.

Where Do We Go from Here?

I'm glad you asked. From here I began to see new places in my Entity/Factory framework for code optimization (note: I don't use dOOdads in production). Consider adding the following method signatures to your interfaces:

C#
//for IEntity
object[] RecordPrimaryKey{get;set;}
bool IsNew();

//for IFactory
IEntity CreateNewEntity();
IEntity FindByPrimaryKey(object[] keyvalues);
IEntity Save();

With these interfaces the logic for both Save and LazyLoadEntity can be pushed into the abstract PostbackEditor base class. Then all your concrete implementations have to do is map the entity values into and out of the form fields. And if you treat your key as an object array you will be able to handle single or compound entity keys of any data type.

C#
virtual protected void LazyLoadEntity()
{
    MyEditingEntity = MyEntityFactory.FindByPrimaryKey(GetRecordIDValue());
    if(MyEditingEntity.IsNew()) 
       MyEditingEntity = MyEntityFactory.CreateNewEntity();
}

Another item on my list is to build a replacement for the ASP:CheckBoxList control that works with this pattern.

Finally, you should build a code generator to output editing pages for any entity class you may have (say, by building around calls to MyEntity.GetType().GetProperties()). I say "you" because I already built one for the ORM framework I use. From experience, I can say that this will dramatically speed up you application delivery and code consistency and quality.

Questions and Concerns

How can I use ASP:Button controls with these patterns?

I generally steer away from this control because of the default submit behavior issues of various browsers, but if you do want to use these controls and event system, move the two calls LoadFormValuesIntoEntity() and Save() out of the Page_Load event and into a button event handler. Be sure to register your save button as the default with the line Page.RegisterHiddenField( "__EVENTTARGET", mySaveButton.ClientID );

Can this pattern work with DataTable objects?

Of course it can. Expect your code to be much messier and more error prone since you will have to repeat your SELECT/UPDATE/INSERT statements in every page that uses those records. As an alternative you could take the time to get comfortable with an Entity/Factory framework and start using it.

How can I use AJAX calls with these patterns?

Totally beyond the scope. You could mix and match, but handling AJAX calls is a completely different logic path requiring you to deal with web services and XML or JSON. Once I settle on a solid set of patterns for this I'll write another article.

Resources

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Architect Milliman
United States United States
I have been involved in professional software development for over 15 years, focusing on distributed applications on both Microsoft and Java platforms.

I also like long walks on the beach and a sense of humor and don't like mean people Wink | ;-)

Comments and Discussions

 
GeneralGreat Article! Pin
Daniel.Perfect.Element19-Jul-07 21:15
Daniel.Perfect.Element19-Jul-07 21:15 
GeneralLove the commentary, and code I use for multi item inserts, edits Pin
philmee9518-May-07 11:18
philmee9518-May-07 11:18 
GeneralForm Control Names Vs. Entity Property Names Pin
dlausch200121-Feb-07 9:47
dlausch200121-Feb-07 9:47 
GeneralRe: Form Control Names Vs. Entity Property Names Pin
Chris Cole21-Feb-07 13:23
professionalChris Cole21-Feb-07 13:23 
QuestionCan anyone figure out a different naming convention? Pin
Kevin Jensen15-Feb-07 10:55
Kevin Jensen15-Feb-07 10:55 
Great Article! Can anyone figure out a different naming convention? I try to avoid having My in front of everything.

Ideas?

.NET Consulting
http://www.kineticmedia.net

QuestionHow to handle forms that need to post back? Pin
Steven Berkovitz25-Jan-07 3:43
Steven Berkovitz25-Jan-07 3:43 
AnswerRe: How to handle forms that need to post back? Pin
Chris Cole25-Jan-07 7:18
professionalChris Cole25-Jan-07 7:18 
QuestionNice But? Pin
asifrazach24-Jan-07 19:34
asifrazach24-Jan-07 19:34 
AnswerSource Link Updated Pin
Chris Cole25-Jan-07 7:02
professionalChris Cole25-Jan-07 7:02 

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

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