Table of contents
Master Pages mechanism allows to define a site "skin" - the common user interface elements that would appear on every visited page. The main idea of Master Pages architecture is to separate the development process of Master Page and a Context Page making it possible to contain style definition and common basic functionality inside the Master Page and letting the Context Page handle the main functionality.
Awwwhhh, Master Pages...
How often we use these words now anticipating release of Microsoft Visual Studio Whidbey with native support for this useful architecture. But what about the .NET Framework 1.1 or even 1.0? I am sure everybody who reads this article spent too much time designing complex master-context pages frameworks involving custom controls, bubbling events, or even hard-coding common HTML text inside the code behind classes. After revisiting existing patterns I've used before and what's proposed by the others so far, I've come up with the conclusion that we shouldn't suffer that much while creating those complicated architectures which, by the way, would be almost impossible to migrate to the upcoming Whidbey Master Pages.
And all that resulted into the following framework...
The basic idea of framework is to define and implement base classes for two types of Web Pages: Master Page and Context Page.
Before explaining the implementation, I want to point that those two types are defined using one common class (MasterContextPage
) - the differentiation is achieved using special class attributes: [MasterPage]
and [ContextPage]
. The explanation behind this approach is simple: if developers want to create their own application core page classes on top of the Master Pages framework, they don't have to duplicate the implementation for Master Pages and for Context Pages.
Main framework class derives from Page
class in order to intercept the initialization of the page controls and to load the control collection from the master page. The mechanics are simple:
- During the context page load, execute assigned Master Page
- Replace context page
Page
control collection with the controls from Master Page
- Insert the Context Page controls into special placeholder inside the Master Page
Doesn't sound like a hard task to do in .NET, does it?
Let's review the MasterContextPage
's context page section: we overload TrackViewState
method to "mess" with form controls before view state is loaded.
protected override void TrackViewState()
{
base.TrackViewState ();
bool isMasterPage = this.IsMasterPage;
bool isContextPage = this.IsContextPage;
if( isMasterPage || isContextPage)
{
if( isContextPage)
{
OnContextPageLoad();
}
if( isMasterPage)
{
OnMasterPageLoad();
}
}
}
Here's the OnContextPageLoad()
method implementation.
private void OnContextPageLoad()
{
Server.Execute( AppContextPath + this.MasterPage);
ControlCollection masterControls =
(ControlCollection)Context.Items[ MASTER_CONTROLS_KEY];
if( masterControls == null)
{
throw new Exception( "Failed to locate Master Page in the context");
}
...
As you can see here, after Master Page was executed, we load its controls from current HttpContext
. Who stores them there - we do!...by deriving the master page class from our class. When the page is executed we store the loaded control collection into the HttpContext
and block the page from rendering any content into HTTP Response stream. Here's how its done:
private void OnMasterPageLoad()
{
Context.Items[ MASTER_CONTROLS_KEY] = this.Controls;
}
protected override void Render(HtmlTextWriter writer)
{
if( !this.IsMasterPage)
{
base.Render (writer);
}
}
Now, let's return to loading of the Context Page. It's time to locate the controls inside the Context Page. We want to "stick" into Master Page, save that control collection into variable, and clear all the controls from the Context Page:
Control contextRoot = this.ContextHtmlForm;
if( contextRoot == null)
{
contextRoot = this.FindControl( CONTEXT_ROOT_CONTROL_ID);
}
if( contextRoot == null)
{
throw new Exception( "Context Page must contain " +
"HtmlForm or control with id = " + CONTEXT_ROOT_CONTROL_ID);
}
ControlCollection contextCollection = contextRoot.Controls;
this.Controls.Clear();
After that, we copy the controls from the Master Page to the Context Page. When context placeholder control is copied - we move all the Context Page controls as child controls of the placeholder. Note that this placeholder control has to have a framework-predefined ID "masterContextContainer
":
Control control = null;
for( int i = 0; i < masterControls.Count;)
{
control = masterControls[i];
if( control == null) continue;
this.Controls.Add( control);
if( control is HtmlForm)
{
Control contextContainer = control.
FindControl( CONTEXT_CONTAINER_CONTROL_ID);
if( contextContainer == null)
{
throw new Exception( "Master Page must contain the control " +
"with id = " + CONTEXT_CONTAINER_CONTROL_ID);
}
contextContainer.ID = Guid.NewGuid().ToString();
Control subControl = null;
for( int j = 0; j < contextCollection.Count;)
{
subControl = contextCollection[j];
if( subControl == null) continue;
contextCollection.Remove(subControl);
contextContainer.Controls.Add( subControl);
}
}
}
As I mentioned earlier, the framework uses attributes to assign a Context Page and/or Master Page functionality to the page. Master page attribute implementation is very simple - it is just a declaration attribute.
[AttributeUsage(AttributeTargets.Class)]
public class MasterPageAttribute : Attribute
{
public MasterPageAttribute()
{
}
}
Context page attribute is more complicated because it may specify a Master Page alias that will contain this page. Using alias to assign a master page allows to decouple a context and master page implementation. Developers should use <appSettings>
configuration section to assign a particular master page to an alias:
[AttributeUsage(AttributeTargets.Class)]
public class ContextPageAttribute : Attribute
{
public ContextPageAttribute()
{
}
public ContextPageAttribute( String masterPageAlias)
{
_MasterPageAlias = masterPageAlias;
}
private String _MasterPageAlias = "";
public String MasterPageAlias
{
get
{
return _MasterPageAlias;
}
set
{
_MasterPageAlias = value;
}
}
}
Let's create a project that's using Master Pages Framework.
First of all, Framework allows developers to have more than one Master Page per project: web project should contain one default master page and could be configured to support personal master pages for certain ASPX pages using web.config, or developers could implement their own mechanism of choosing Master pages by overwriting MasterPage
property in the MasterContextPage
class.
Now, we need the default master page for the site. It will be a standard header-menu-footer layout done using HTML tables.
<table width="100%" ID="Table1">
<tr>
<td colspan=2 align=center bgcolor=navy>
<span style="COLOR:white">SITE HEADER</span>
</td>
</tr>
<tr>
<td bgcolor=silver width=200>
LEFT MENU
</td>
<td id="masterContextContainer" runat="server">
</td>
</tr>
<tr>
<td colspan=2 align=center bgcolor=lightskyblue>
SITE FOOTER
</td>
</tr>
</table>
The place where we want our Context pages to appear is "marked" with service-side control with ID "masterContextContainer
". Then we open a code behind for our master page, inherit the class from MasterContextPage
class, and apply the [MasterPage]
attribute to the class.
[DPDTeam.Web.MasterPages.MasterPage]
public class DefaultMasterPage :
DPDTeam.Web.MasterPages.MasterContextPage
Building the master page is complete. Let's create ViewOrders Context page - it will be just a simple list box with Order Number columns and the Refresh button at the bottom of the page. When the page is shown the first time, we show default set of data, and when Refresh button is pressed, we change the data source and re-bind the list. We don't have to add anything related to our framework in the ASPX code or in the code-behind class except deriving the class from MasterContextPage
class and adding [ContextPage]
attribute.
[DPDTeam.Web.MasterPages.ContextPage]
public class ViewOrders :
DPDTeam.Web.MasterPages.MasterContextPage
The only thing left to do is to create configuration item in the web.config file that defines the site default master page. This item we create in the <appSettings>
section:
<appSettings>
<add key="MasterPages.DefaultMasterPage" value="DefaultMasterPage.aspx" />
</appSettings>
And that's it - now when we run View Orders page, it will be wrapped inside our master page. The most important thing is all code-behind code works as it worked before in both master and context pages. The only change was inheritance and application of the attribute!
Next page - Order Items page - will have its own Master Page, different from the default one for the web site. For that, we create OrderItemMasterPage.aspx master page exactly the same way we created default master page - new master page will not have a menu to the left of the context area. We use the <appSettings>
section again to specify a personal master page for our ViewOrderItems.aspx:
<appSettings>
<add key="MasterPages.DefaultMasterPage" value="DefaultMasterPage.aspx" />
<add key="MasterPages.PersonalMasterPage.ViewOrderItems.aspx"
value="OrderItemMasterPage.aspx" />
</appSettings>
Now it's time to inherit ViewOrderItems
class from MasterContextPage
class and apply the [ContextPage]
attribute, and voila - orders items are wrapped in their own master page.
Now, what if part of the pages in our web application has to appear under certain master page and another part has to be "skinned" by another master page? Defining a personal master page for each context page in the configuration file would be a very hard task to do if the project consists of let's say 200 pages. To help resolving this situation, ContextPageAttribute
contains a String
field called MasterPageAlias
. Using this field, developers can assign a context to a master page "type" without specifying the master page's implementation and do it later in the configuration file. In the demo project, open ViewOrderItems
code and take a look at the class definition:
[ContextPage( MasterPageAlias = "ViewOrderItemsMaster")]
public class ViewOrderItems :
DPDTeam.Web.MasterPages.MasterContextPage
Then, in the web application configuration file, we just create a special item that will forward all calls for alias "ViewOrderItemsMaster
" to "OrderItemMasterPage.aspx" page:
<appSettings>
<add key="MasterPages.Alias.ViewOrderItemsMaster"
value="OrderItemMasterPage.aspx" />
</appSettings>
Before this section, we used a simple scenario for Master-Context page pattern: all our context pages had only one master page. Now, let's see how using this framework we can "wrap" context page with several layers of master pages.
Let's say we're developing a big web site and we have four teams:
- Context page developers
- Artists developing main banner
- Left menu developers
- Right side News portion developers
All teams can work only independent from each other (different time zones, for example) and we must organize their efforts. First, let's write a skeleton page for our context page team:
[ContextPage( MasterPageAlias = "ContextPage1Master")]
public class CasadingContextPage1 : MasterContextPage
...
Here's their page without our framework:
Our job is done here - they can create a dummy master page in their project and assign the "ContextPage1Master
" alias to it.
Now, it's time to help Left Menu Developers. We create almost the same skeleton for their page but we give then another alias for the master page. When their development and testing is complete, they will add [MasterPage]
attribute to the class definition, and on the page, insert the placeholder control to store the context page control.
[MasterPage]
[ContextPage( MasterPageAlias = "ContextPage2Master")]
public class CascadingLeftMasterPage : MasterContextPage
...
View their job done with the framework turned off:
We do the same steps for the News Team as we did for Left Menu Developers resulting in the following class definition:
[MasterPage]
[ContextPage( MasterPageAlias = "ContextPage2Master")]
public class CascadingRightMasterPage : MasterContextPage
...
The team came up with these results:
Our Main Banner Team will create something like this:
[MasterPage]
public class CascadingTopMasterPage : MasterContextPage
...
resulting in this page:
Now when development is done, it is time to assemble all the pages in one project and connect aliases used in the pages to the real implementations. For that, we use <appSettings>
section in the web.config file.
<appSettings>
<add key="MasterPages.Alias.ContextGroup1Master"
value="cascading/CascadingLeftMasterPage.aspx" />
<add key="MasterPages.Alias.ContextGroup2Master"
value="cascading/CascadingRightMasterPage.aspx" />
<add key="MasterPages.Alias.ContextGroup3Master"
value="cascading/CascadingTopMasterPage.aspx" />
</appSettings>
This configuration means the following:
- We connect Context Page to Left Menu Page.
- After that, ee connect Left Menu Page to Right News Page.
- And finally, we connect Right News Page to Banner Page.
After the project is compiled and launched, we can observe the results - our context page is wrapped with three layers of master pages:
Remember to give special prefixes for the controls on every Master Page and Context Pages. That will ensure that you won't have any "duplicate control name" exceptions.
If you used anything from this article in your application and wish your app to be in the list of the projects built on this framework, please send me an email.
- Create a folder for the project called MasterPages somewhere on the file system (for example, c:\projects\MasterPages).
- Unzip content of the effectivempages_demo.zip into that folder.
- Open Internet Information Service console and create virtual directory for your web site called MasterPages, and make it point to the MasterPages folder you've created.
- Click on "demo" project folder properties in IIS console, and on the Properties dialog, press "Create" button to create web application for this folder.
- Open the demo.sln solution file in Visual Studio .NET 2003 or demo2002.sln in Visual Studio .NET 2002, to view the code and compile the project.
- Run the application in your browser using http://localhost/masterpages/demo/vieworders.aspx URL.
- Version 1.0. October 24th, 2004.
- Added solution files, and solution and project files for VS.NET 2002. October, 29th 2004.
- Fixed the problem with context pages located in web application's sub-folders.
- Fixed the bug with MasterPage ViewState. November, 1st 2004.
- Added master page aliasing (thanks to Dave Glaeser for the idea). November 2nd, 2004.
- Added master page cascading. November 4th, 2004.