|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionAfter 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 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 SuppositionsBoth 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 EditorThe 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
Saving an Entity Record
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 ClassNow 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:
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 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 ImplementationIn 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. //supporting ORM framework
using WebAppPatterns.Bizlayer;
using MyGeneration.dOOdads;
Next we have to change the Page class to extend our 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 PageThe 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. This pattern works by using matching (simple) or similar (complex) form field names when collecting data. This allows you to iterate the 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. Magic NamesThe "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 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: If we were to use the asp <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: Complex ValuesFor 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 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 Order Editor ExampleIn our example, we override the /// <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. /// <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. ConclusionI 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: //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 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 Finally, you should build a code generator to output editing pages for any entity class you may have (say, by building around calls to Questions and ConcernsHow can I use
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 23 Jan 2007 Editor: Chris Maunder |
Copyright 2007 by Chris Cole Everything else Copyright © CodeProject, 1999-2008 Web16 | |