In large Web sites, there are generally just a couple of unique page
layouts. In the past, developers had to duplicate large chunks of code on
each page. Doing so made the site hard to maintain and difficult to change
on a global scale. This tended to be an issue even when using include files,
as it was difficult to pass page-specific information down the chain. To the
rescue are the .NET Web User Controls and their ability to interact with a
parent page's exposed custom properties. Through an assortment of referenced
custom controls, the look and layout of a site can be enforced and page
differences can be controlled from one location on the parent page.
Additionally, global site changes can then be made by simply changing a
single control.
In this article, I assume that you are familiar with the creation of
ASP.NET WebForm pages and Web User Controls, and the use of the various
other ASP.NET controls.
The Dark Ages
Before .NET, standard ASP pages tended to become a spaghetti plate of
HTML and code intertwined with each other. Over time several techniques were
developed for cleaning up this HTML/code interdependence as much as
possible. One such technique was to use include files for anything that
appeared on more than one page. An include file used the following IIS
directive hidden in an HTML comment:
<!-- #INCLUDE VIRTUAL="/site/includepage.asp" -->
This method worked fine at first, but as the complexity of our sites
increased, so did the issues involved with them. One such issue was that the
included pages inherited the same environment as the parent page, so any
variables declared would carry over, the original query string would carry
over, etc.
For some things this was good. For instance, you could define a variable
"color" on the parent page, then on the included page reference it with:
<%=color%>
However, you could not transfer data to the include page through any query
string values as in:
<!-- #INCLUDE VIRTUAL="/site/include page.asp?color=red" -->
Additionally, if you tried to assign or redeclare a variable, such as
"color", on the included page, it would stomp on the parent page's value, or
raise an error condition.
With all this in mind, you can see that the old way was not very
elegant, and was prone to errors. Tracking the cause of these errors was
also very difficult since it was hard to trace the interdependence of these
pages.
The Enlightenment
ASP.NET introduces a replacement for the old include files in its Web
User Control. A Web User Control is similar to an include file in that it
contains a block of HTML, but it also has a programmatic interface to
encapsulate any data and also executes in an environment independent from
the page that uses it.
In the simplest case, a Web User Control just contains a block of HTML
code to place on a page wherever the control is referenced:
<uc1:Control1 id="control1" runat="server" />
In a more complex case, a control exposes properties that can be set by
the page when the control is referenced:
<uc1:Control1 id="control1" customProperty="green"
runat="server" />
This provides a more elegant means of communicating information into the
control from the parent page. Both the page and control can use a variable
called "color", but the value must be passed explicitly from the parent page
to the control. No more stepping on each other's toes.
180-Degree Spin
Above, I described how to pass information to a control from a parent
page. This is the typical way that data communication occurs from a parent
page to its child controls.
However, what if you have a site with hundreds of pages using the same
controls several times per page? Every time the control appears on the page,
you have to assign a value to the custom control properties. If you want to
provide the same value to several controls, you will get something like
this:
<uc1:Control1 id="control1" userName="Jeff" runat="server" />
<uc1:Control2 id="control2" userName="Jeff" runat="server" />
<uc1:Control3 id="control3" userName="Jeff" runat="server" />
Seeing the duplication of hard-coded values above should immediately
raise a red flag. The problem is that if the userName changes, the value has
to be changed in three different places. It would be much cleaner to simply
be able to change it in only one place, and have every control on the page
inherit the single value. Implementing that goal is what the remainder of
this article is about.
There really are two ways to achieve the goal of having page controls
automatically gain access to a parent page's properties. I will start with
the most elegant one, which is inheritance. However, there are problems with
using this technique in Visual Studio, so I will also provide an alternative
method, which is through the use of an interface.
The Project
The sample code provided throughout the remainder of this article
references a sample project I developed that consists of three pages, and
three controls that appear on each page. Each page lays out the control
locations in a table and sets properties that the controls inherit. This
page layout consists of a title area populated by Control1, a menu area
populated by Control2, and a content area populated by Control3 (see Figure
1). The variables defined on each page that I want the controls to access are:
private System.Drawing.Color color;
private string title;
These consist of the color scheme and the title of the page. Since the
variables are private, they need to be exposed to code outside the page
through the following public property blocks:
public string Title
{
set {title = value;}
get {return title;}
}
public System.Drawing.Color Color
{
set {color = value;}
get {return color;}
}
The three pages that I created are "PageRed", "PageGreen", and
"PageBlue", which utilize the same three controls. The controls look
different on each page, however, depending on the values of each page's
custom properties. Figures 2, 3, and 4 show the changes on each page: the
title text and color, the menu area color, and the name of the color in the
content area.
The code defining the page properties looks simple enough at first, but
our controls still cannot access these properties directly. Since a WebForm
page inherits from System.Web.UI.Page, and the Page class does not implement
a "Color" or "Title" property, a control will raise an exception if it tries
to do something like the following:
System.Drawing.Color localColor = Page.Color;
So, how can we get the controls to see these properties? The answer is
inheritance.
Base (Template) Page Class
Since the custom properties we want to access from all controls
"Color" and "Title" are not part of the System.Web.UI.Page class, we need
to subclass it and add these new properties. This simply requires creating
an empty WebForm called "BasePage.aspx" with the code shown in Listing 1.
BasePage inherits from System.Web.UI.Page as usual, and implements the
two new properties that we desire to have on every page. Once this is
created, we have each of our pages inherit from this new class instead of
the built-in Page class. Each of our page class declarations look like the
following:
public class PageGreen : BasePage
{
...
}
We need to be able to set properties on a page level, then allow
included controls to find and access them. All of our pages will now
automatically inherit support for our two custom properties. Additionally,
there is now a mechanism for our controls to expect and find these
properties, which I will cover in the next section.
Since the pages automatically inherit the two properties, we can simply
assign a value to them once on each page's Page_Load handler:
private void Page_Load(objectsender, System.EventArgs e)
{
// Set the page's custom properties
Color = System.Drawing.Color.FromName("Green");
Title = "A Grassy Meadow Color";
}
Controls
In order for our controls to find the two properties of our pages, they
need to be able to expect them to be there. Instead of treating the
control's Page property as a System.Web.UI.Page class, we need to treat it
as a BasePage class. This is done through a cast; for example, to set the
text and color of a title Label via the Page_Load handler of Control1, we do
the following:
private void Page_Load(objectsender, System.EventArgs e)
{
// Set control properties based on parent page values
TitleLabel.Text = ((BasePage)Page).Title;
TitleLabel.ForeColor = ((BasePage)Page).Color;
}
The control gets the Page property, which is expected to be of type
System.Web.UI.Page, and instead treats it like a BasePage type, which it
actually is. Now the two properties are available for access.
That's all there is to it. We created a new base page class that defines
all the custom public properties that a page will expose. We inherited from
that page on each of our real pages, and we assigned values to the
properties in its Page_Load handler. Then each control that needs a value
from the page gets its Page property, casts it as our base page, and
accesses the custom property values.
Issues and an Alternative
The above solution, in my opinion, is the most elegant. However, Visual
Studio 7 (at least Beta 2) disagrees with me. The Beta 2 designer does not
know what a "BasePage" class is and therefore cannot interact with it
properly. It is not smart enough (yet?) to realize that the BasePage in turn
inherits from System.Web.UI.Page so it can treat it like a Page class.
In the real world, I found that the ability to use the visual design
tools outweighs the elegance of inheritance. Luckily, there is another
solution almost as nice that works flawlessly the implementation of a
custom interface.
Instead of creating a base page to inherit from, we will define our
properties through an interface and require that each page implement that
interface. This allows us to cast the page in each control as the interface
with the same result as using a base page class. The drawback is that we
have to implement the properties on every page, rather than just on a single
page that we inherit from.
So, the interface is defined through a simple .cs file as:
public interface IPageTraits
{
string Title {set; get; }
System.Drawing.Color Color {set; get; }
}
It tells all our pages that they have to implement a set and get
function for the two properties we want them to expose. Each page
declaration will then appear as shown in Listing 2 and will be required to
implement the two properties of the interface.
Notice that our pages inherit from System.Web.UI.Page and also implement
IPageTraits. C# can only inherit from one parent class, but it can implement
any number of interfaces. It would be nice to have multiple inheritance in
C# because we could inherit from page as well as another class that has our
custom properties, but that is not an option.
Our control code then needs only to cast the page as an IPageTraits
object to access our properties:
private void Page_Load(objectsender, System.EventArgs e)
{
// Set control properties based on parent page values
TitleLabel.Text = ((IPageTraits)Page).Title;
TitleLabel.ForeColor = ((IPageTraits)Page).Color;
}
Conclusion
Things have come a long way since the days of include files and
hard-to-follow code. The object-oriented capabilities of ASP.NET coupled
with Web Custom Controls allow a higher degree of power, customization, and
encapsulation than anything prior. The result is cleaner code and a unified
structure that can make maintaining a large site a breeze.
Author Bio
Jeff Jorczak built his first computer at the age of 10 using tinker toys, string, and a
dismembered Barbie doll. Since then, he has maintained a strong addiction to shuffling 1s and 0s into ever-increasing patterns of complexity. Jeff has worked as everything from a video games developer to a Microsoft technology guru. He is currently the chief
architect of Etroductions.com (www.etroductions.com), which utilizes the technique described in this article.
jeff@jorczak.com
Listing 1
public class BasePage : System.Web.UI.Page
{
private System.Drawing.Color color;
private string title;
public string Title
{
set {title = value;}
get {return title;}
}
public System.Drawing.Color Color
{
set {color = value;}
get {return color;}
}
}
Listing 2
public class PageGreen: System.Web.UI.Page, IPageTraits
{
private System.Drawing.Color color;
private string title;
public string Title
{
set {title = value;}
get {return title;}
}
public System.Drawing.Color Color
{
set {color = value;}
get {return color;}
}
...
}
All Rights Reserved
Copyright © 2004 SYS-CON Media, Inc.
E-mail:
info@sys-con.com