Introduction
When it comes to loading user controls in ASP.NET, you are forced to do several steps that will make any OO-purist cringe. First, you have to call LoadControl
, passing it the file path to the .ASCX file you want to load. Next, you have to cast that control to the type of control that it is. Then, you have to figure out which parameters are optional and which are required, and have to set each of them individually. This leads to code that looks like this:
WebUserControl1 newUserControl = (WebUserControl1)LoadControl("WebUserControl1.ascx");
newUserControl.Item = item;
newUserControl.ItemFormat = itemFormat;
this.PlaceHolder.Add(newUserControl);
Here's an example from another article on CodeProject ...
hdrCtl = LoadControl("./controls/SiteHeader.ascx");
if (hdrCtl != null)
{
((SiteHeader)hdrCtl).LeftLogoImgPath = "..\\images\\ps_logo.gif";
((SiteHeader)hdrCtl).RightLogoImgPath = "..\\images\\ps_name.gif";
HeaderCtl.Controls.Add(hdrCtl);
}
There has to be a better way to do this, and in this article, I'll show you one way to improve on the situation ...
Background
Code such as that shown in the introduction has several issues:
- References to ./controls/SiteHeader.ascx are often scattered throughout the solution, making it hard to move a control to a different folder or to rename it.
- Since there is no constructor with a required set of parameters, you can end up with partially instantiated controls, leading to run-time errors when someone forgets to set the
Item
required by your control. - It is hard to discover (using the IDE) what the required parameters are for the control.
Using the code
The solution to these issues is to create a strongly-typed, static factory method on each of your control classes that returns an instance of the control with all of the required parameters set on it. If there is more than one way to instantiate the control, then you can provide multiple static methods on your control class.
public static ItemView LoadThisControl (Page page, Item item, Itemformat itemFormat)
{
ItemView result = (ItemView)page.LoadControl("~/controls/itemview.ascx");
result.Item = item;
result.ItemFormat = itemFormat;
return result;
}
public static ItemView LoadThisControl (Page page, Item item)
{
return LoadThisControl (page, item, Itemformat.Default)
}
With these static factory methods in place, you can now discover (using the IDE) how to load an ItemView
control. Intellisense will show you that there are two calls you can make and that you must pass in an Item
object. If you stick to using these methods, you will always get a properly instantiated control, and when you make changes to the required parameters, the compiler will point out all the places you need to go fix up.
Furthermore, the 'known' location of your control is now defined in just one place in your code, making it easy to relocate it.
A more complex factory
In your domain layer, you will often have a base class and then a set of derived classes; e.g., our base domain object might be an Item
, and we have ItemImage
and ItemVideo
derived from it. When it comes time to load a control to display these items, you might need to load an ItemImageView
or an ItemVideoView
control, depending on the type of the item you want to display.
This leads to code with switch
statements in it examining the type of object to decide which control to load, and as an OO-purist will tell you, switch
statements nearly always mean that you aren't writing OO-code. There's only one thing worse than a switch
statement, and that's a duplicated switch
statement! So, if you find yourself writing the same switch
statement multiple times to load the appropriate control, you know you are doing something wrong.
One approach would be to ask the domain object for the control to display itself, so you might have item.LoadViewControl
which would return you the appropriate ItemView
control with the Item
already set on it. The problem with this approach is that now your domain layer knows about your UI layer, and that's bad news; in fact, it's not a 'layer' anymore, it's a tangled mess that will be hard to reuse and hard to maintain. So, don't do that!
So, keeping the knowledge of the UI layer in the UI layer itself, a compromise approach is to have a factory method on your base UI control. Let's call it
ItemView.LoadAppropriateControl(Item item)
, and have just one
switch
statement inside that factory method that looks at the
Item
type, calls the appropriate factory method to get a control for that
Item
, and then returns the control.
This would look something like ...
public static ItemView LoadAppropriateControl (Page page, Item item)
{
if (item is ItemImage)
return ItemImageView.LoadThisControl (page, (ItemImage)item);
else if (item is ItemVideo)
return ItemVideoView.LoadThisControl (page, (ItemVideo)item);
else
throw new ArgumentException("ItemView only knows how " +
"to make controls for ItemImage and ItemVideo types");
}
With this approach, you now have one place to go define how to load a control with the correct parameters, and one place to go define how to map an object derived from Item
onto the appropriate control.
Of course, you may well like this approach so much that you add other factory methods to the base ItemView
to load the various types of control you need that all take an Item
as a parameter. Perhaps, one to create a PanelView
of the Item
, another to create an IconView
of the Item
, and a third to create a ListEntryView
of the Item
.
Outside of this factory method, your code is now much easier to read:
foreach (Item item in this.Items)
{
Control c = ItemView.LoadAppropriateListEntryView (item);
this.placeHolderList.Controls.Add (c);
}
foreach (Item item in this.Items)
{
Control c = ItemView.LoadAppropriateIconView (item);
this.placeHolderIcons.Controls.Add (c);
}
Points of interest
Duplicate code is always a bad idea: it leads to code bloat; it causes bugs to appear in some places, but not others; 'fixed' bugs mysteriously reappear in other places because the developer didn't go find all copies of that code when he or she fixed a bug that had been found in one of them.
"One-fact, one-place" is a great mantra not just for database developers, everyone should aim for it. It you are ever tempted to use "cut-and-paste coding" because it's 'quicker', just remember that 2x the code will produce 2x as many bugs and will be 2x as hard to maintain. It is always better to avoid duplication and to spend time creating a reusable method that puts "one fact" in "one place". Every sprint cycle should include time for this kind of refactoring to ensure that your solution stays clean and maintainable.
The solution proposed here isn't perfect, but it's a lot better than seeing all those duplicate .ascx file references in your code and not knowing until run-time whether every control was instantiated properly.
History
- August 1, 2007: First version.
- August 4, 2007: Fixed a couple of typos.