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
- Determine ID of entity record for page.
- Retrieve entity data from datastore into page-level object.
- Load data from object into form.
- Render page.
Saving an Entity Record
- Determine ID of entity record for page.
- Retrieve entity from datastore into page-level object.
- Load postback data into entity object.
- Save object to data store.
- 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.
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;
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
virtual public int RecordID
{
get { return _recordID; }
set { _recordID = value; }
}
public object MyEditingEntity
{
get {
if (_myEditingEntity == null)
{
_myEditingEntity = LazyLoadEntity();
}
return _myEditingEntity; }
set { _myEditingEntity = value; }
}
public object MyEntityFactory
{
get {
if (_myEntityFactory == null)
{
_myEntityFactory = LazyLoadFactory();
}
return _myEntityFactory;
}
set { _myEntityFactory = value; }
}
#endregion
#region Abstract methods the concrete editing page must implement
abstract protected bool Save();
abstract protected void LoadEntityIntoForm();
abstract protected void LoadFormValuesIntoEntity();
abstract protected object LazyLoadEntity();
abstract protected object LazyLoadFactory();
#endregion
public PostbackEditor()
{
this.Load += new EventHandler(Page_Load);
}
virtual protected void Page_Load(object sender, EventArgs e)
{
GetRecordIDValue();
if (IsPostBack)
{
this.LoadFormValuesIntoEntity();
this.Save();
}
else
{
this.LoadEntityIntoForm();
}
}
virtual protected void GetRecordIDValue()
{
int myId = 0;
string tmpVal = null;
if (IsPostBack)
{
tmpVal = Request.Form["recid"];
}
else
{
tmpVal = Request.QueryString["recid"];
}
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.
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.
public partial class CustomerEditor : PostbackEditor
Our implementations of the abstract methods are very straight forward.
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;
}
protected override void LoadFormValuesIntoEntity()
{
Customer entity = (Customer)this.MyEditingEntity;
entity.Name = txtName.Value;
entity.Address = txtAddress.Value;
}
protected override bool Save()
{
Customer entity = (Customer)this.MyEditingEntity;
entity.Save();
lblResult.Text = "Saved Customer " + entity.CustomerID.ToString();
return true;
}
#region Lazy Load methods
protected override object LazyLoadEntity()
{
Customer entityObj = new Customer();
entityObj.LoadByPrimaryKey(this.RecordID);
if (entityObj.EOF)
{
entityObj.AddNew();
entityObj.CustomerID = 0;
}
return entityObj;
}
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:
<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.
<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.
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.
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.
protected override void LoadFormValuesIntoEntity()
{
AnOrder order = (AnOrder)MyEditingEntity;
order.CustomerID = Convert.ToInt32(Request.Form["selCustomerId"]);
string[] formKeys = Request.Form.AllKeys;
int pos;
for (int i = 0; i < formKeys.Length; i++)
{
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))
{
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;
}
}
}
}
}
...
protected override bool Save()
{
AnOrder order = (AnOrder)MyEditingEntity;
order.Save();
int orderId = order.OrderID;
MyOrderDetails.Rewind();
do
{
MyOrderDetails.OrderID = orderId;
} while (MyOrderDetails.MoveNext());
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:
object[] RecordPrimaryKey{get;set;}
bool IsNew();
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.
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.