Over the past couple of years I had the opportunity to learn a lot of great tips and tricks from some of the best programmers in the area I live in, especially when it comes to design patterns and architectual design in general. With this blog I have the opportunity to pass some of those things and some of the things I've taught myself along the way to others. What I'd like to cover in this article is a situation I have seen arise involving the use of User Controls in ASP.NET applications.
Typically a user control in ASP.NET is used for grouping together a generic logical set of controls into one containing control to use across several web pages. An example of this could be a 'Address User Control'. This control may be used on the 'Customer Information' page of a web application, but also on the 'Billing Information' page of the same application; both pages need to show the customer address.
So in this situation you usually see code like this on each of the html pages:
<
uc1:AddressControl ID="AddressControl1" runat="server" />
How this control is populated may vary depend on your school of thought. Some developers will find the AddressControl object on the Page in their Page_Load event and set public properties of the user control such as this:
Control
c = Page.FindControl("AddressControl");
if(c != null)
{
((AddressControl)c).ToLine = CustomerObject.Name;
}
And here's a quick tip if you don't know this already...you can actually declare your user control manually in your code behind and ASP.NET will set it for you when the parser goes through the page (you don't have to do the old FindControl method as I did above), you just need to ensure your object name matches the control ID in your HTML view:
protected
AddressControl AddressControl1;
So this is all fine and well, as long as you know what properties need to be set on the control; there's no guarantee that these properties will be set unless you build logic into the control itself that will throw an error if these properties aren't set...this works...but there's a better way, however the usual comeback I'm given is that the developer will 'remember' to set these properties on any new pages that they add this control to. This way doesn't take into account though methods that may need to vary based upon the page though (more on this later).
Before I get into that though, there's the second way I usually see situtions like this handled... a giant switch (or Select Case if your a VB person) statement or a huge if...else if...etc. statement inside the control itself where the control is trying to determine what it needs to load itself by looking at the Page or ironically some property of the control that the page set:
//PageName is a variable that holds the name
//of the page or a property of the user
//control that was set by the page
switch(PageName)
{
case "CustomerInfo.aspx":
this.AddressTo = GetCustomerAddress().ToLine;
break;
case "BillingInfo.aspx":
this.AddressTo = GetBillingAddress().ToLine;
break;
}
So the control is no longer generic when we do something like this...what happens if the PageName property isn't set? Or worse what happens if we're looking at the actual page the control is on and that page is renamed to something else or the control is put on a new page that doesn't exist in the case statement? You'd be surpised how often I've seen this. So the most common argument I receive is that the person who has built this will 'remember' to add it to the case statement.
This brings up some points I want to make:
- What if you win the lottery and are no longer there?
- Even if you doucment these special rules somewhere...how many people actually read documentation? The business doesn't usually give you time to write or read documentation (or design and that's why we get into these situations in the first place), so I think we need something a little more robust to tell us when we've done something wrong.
- In the case of throwing an error when a property isn't set are you going to make some poor new developer build the project 20 times and get 20 different run-time errors to see that there was yet another property that they needed to set?
- What if this control has a 'Save' method on it that varies between BillingInformation and CustomerInformation? Are you going to further couple your control by having two SaveMethods that depend on some property that was set (assuming the developer knew they had to set this property)? What happens when you use the control on several different pages?
So let's look at an alternative buy using the Template pattern. While this is not a true implementation of the Template pattern ,in the GoF definition, but it falls within it's intended purpose and the same type of problem it solves and it's structure is similiar.
The key to the solution is to define an interface that will be used by the user control against the page, below is an example definition:
public
interface IAddressControl
{
string ToLine { get; set; }
string AddressLine1 { get; set; }
string AddressLine2 { get; set; }
string City { get; set; }
string State { get; set; }
string Zip { get; set; }
//Will determine whether control is read only or not
//and whether to show the 'Edit' button
bool AllowEdit { get; }
void SaveChanges();
}
In our user control we will program against the interface. Notice we throw one error on the Initialize event if the interface isn't used, we don't have multiple errors for each property that's not set. This allows us to guarantee or calls against the interfaces in other portions of the class Control values are populated by properties of the interface, not properties of the control itself. We also only have one method that is called when the save button is clicked, but the save algoritim has been deferred to the implementor of the interface (the page):
public
partial class AddressControl : System.Web.UI.UserControl
{
IAddressControl pageInterface = null;
protected void AddressControl_Load(object sender, EventArgs e) { this.AddressName.Text = pageInterface.ToLine;
this.AddressLine1.Text = pageInterface.AddressLine1;
this.AddressLine2.Text = pageInterface.AddressLine2;
this.City.Text = pageInterface.City;
this.State.Text = pageInterface.State;
this.Zip.Text = pageInterface.Zip;
this.Edit.Visible = pageInterface.AllowEdit;
}
private void AddressControl_Init(object sender, EventArgs e)
{
Type t = Page.GetType().GetInerface("IAddressControl");
if(t == null)
{
throw new Exception("In order to use the AddressControl user control you must implement the IAddressControl interface");
}
pageInterface = Page as IAddressControl;
}
protected void Save_Click(object sender, EventArgs e)
{
//Now save the changes...user can only get here if AllowEdit = true
pageInterface.ToLine = this.AddressName.Text;
pageInterface.AddressLine1 = this.AddressLine1.Text;
pageInterface.AddressLine2 = this.AddressLine2.Text;
pageInterface.City = this.City.Text;
pageInterface.State = this.State.Text;
pageInterface.Zip = this.Zip.Text;
//Now save the changes...we don't care how...the user of the control
//is the object who is responsible for this.
pageInterface.SaveChanges();
}
}
On the page itself we implement the interface and how we implement that interface will be up to the individual pages. Below I have shown the one for BillingInformation, the CustomerInformation page could have a whole different architecture and way of obtaining this information, but the AddressControl doesn't care!
CustomerObject Customer =
null;
protected void Page_Load(object sender, EventArgs e)
{
if(!Page.IsPostBack)
{
Customer = CustomerManager.LoadCustomer();
}
}
#region IAddressControl Members
public string ToLine
{
get
{
return Customer.Address[0];
}
set
{
Customer.Address[0] = value;
}
}
public string AddressLine1
{
get
{
return Customer.Address[1];
}
set
{
Customer.Address[1] = value;
}
}
public string AddressLine2
{
get
{
return Customer.ExtraLine[0];
}
set
{
Customer.ExtraLine[0] = value;
}
}
public string City
{
get
{
return Customer.City;
}
set
{
Customer.City = value;
}
}
public string State
{
get
{
return Customer.State;
}
set
{
Customer.State = value;
}
}
public string Zip
{
get
{
return Customer.Zip;
}
set
{
Customer.Zip = value;
}
}
public bool AllowEdit
{
get { return true; }
}
public void SaveChanges()
{
CustomerManager.SaveCustomer(Customer);
}
#endregion
}
So now looking at the formal definition of Template Pattern you'll see that while our implementation isn't exact, our overall purpose and outcome is similiar:
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.
Using this pattern allows us to easily implement everything needed on future pages where we want to use this control, and pages that do not implement the interface are promptly given an error that is meaningful.
One word of caution is that because of the way the structure of ASP.NET has changed in version 2.0, but the overall principal is the same and can be applied in version 2.0. For 2.0 you should be able to just change your Control_Init and Control_Load methods to Page_Init and Page_Load... the init doesn't matter when it fires as long as it's before any code that looks at the interface and the user control's Page_Load event should fire after the Page's Page_Load event where you'd probably be getting the business object that is used in the interface stubs, as always though...test first! We have to make these changes because of changes in how user controls are compiled in 2.0.