Click here to Skip to main content
15,867,686 members
Articles / Web Development / ASP.NET
Article

How To Develop Cascading Master Pages in ASP.NET (.NET Framework 1.x) Without Using Custom Controls

Rate me:
Please Sign up or sign in to vote.
3.68/5 (29 votes)
9 Nov 20049 min read 207.7K   1.1K   74   66
Article shows a very simple but powerful way to use Cascading Master Pages design pattern in .NET without using Custom Controls.

Table of contents

Introduction

Sample Image - effectivempages.jpg

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.

Background

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...

Basic Idea

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?

Implementation

MasterContextPage class

Let's review the MasterContextPage's context page section: we overload TrackViewState method to "mess" with form controls before view state is loaded.

C#
protected override void TrackViewState()
{
    base.TrackViewState ();
    bool isMasterPage = this.IsMasterPage;
    bool isContextPage = this.IsContextPage;
    if( isMasterPage || isContextPage)
    {
       // First, we check if current page is context page
       if( isContextPage)
       {
          OnContextPageLoad();
       }
       // Then, if its a master page also, 
       //   we save combined controls into context
       if( isMasterPage)
       {
          OnMasterPageLoad();
       }
    }
}

Here's the OnContextPageLoad() method implementation.

C#
private void OnContextPageLoad()
{
   // Execute master page 
   Server.Execute( AppContextPath + this.MasterPage);
   // Retrieve master page controls from the Http Context
   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:

C#
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:

C#
// locate the context root control in 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);
}
// save its controls and clear all form controls
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":

C#
Control control = null;
for( int i = 0; i < masterControls.Count;)
{
   control = masterControls[i];
   if( control == null) continue;
   // Add master page controls into context page
   this.Controls.Add( control);
   if( control is HtmlForm) 
   {
      // When master page form is located, find the container control
      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);
      }
      // Assign a context container new ID 
      // to avoid control name duplication
      // in the cascading master pages case
      contextContainer.ID = Guid.NewGuid().ToString();
      Control subControl = null;
      // Copy all control from context root control collection 
      //    to the context container control from the master page 
      for( int j = 0; j < contextCollection.Count;) 
      {
         subControl = contextCollection[j];
         if( subControl == null) continue;
         contextCollection.Remove(subControl);
         contextContainer.Controls.Add( subControl);
      }
   }
}

[MasterPage] and [ContextPage] attributes

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.

C#
[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:

C#
[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;
    }
  }
}

Using the Framework

Let's create a project that's using Master Pages Framework.

Simple Master-Context pages

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.

HTML
<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.

C#
[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.

C#
[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:

XML
<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!

Image 2

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:

XML
   <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.

Image 3

Master Page Aliasing

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:

C#
[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:

XML
<appSettings>
 <add key="MasterPages.Alias.ViewOrderItemsMaster" 
                      value="OrderItemMasterPage.aspx" />
</appSettings>

Cascading Master Pages

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:

C#
[ContextPage( MasterPageAlias = "ContextPage1Master")]
public class CasadingContextPage1 : MasterContextPage
...

Here's their page without our framework:

Image 4

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.

C#
[MasterPage]
[ContextPage( MasterPageAlias = "ContextPage2Master")]
public class CascadingLeftMasterPage : MasterContextPage
...

View their job done with the framework turned off:

Image 5

We do the same steps for the News Team as we did for Left Menu Developers resulting in the following class definition:

C#
[MasterPage]
[ContextPage( MasterPageAlias = "ContextPage2Master")]
public class CascadingRightMasterPage : MasterContextPage
...

The team came up with these results:

Image 6

Our Main Banner Team will create something like this:

C#
[MasterPage]
public class CascadingTopMasterPage : MasterContextPage
...

resulting in this page:

Image 7

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.

XML
<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:

  1. We connect Context Page to Left Menu Page.
  2. After that, ee connect Left Menu Page to Right News Page.
  3. 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:

Image 8

Special notes

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.

How to setup demo project

  1. Create a folder for the project called MasterPages somewhere on the file system (for example, c:\projects\MasterPages).
  2. Unzip content of the effectivempages_demo.zip into that folder.
  3. 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.
  4. Click on "demo" project folder properties in IIS console, and on the Properties dialog, press "Create" button to create web application for this folder.
  5. 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.
  6. Run the application in your browser using http://localhost/masterpages/demo/vieworders.aspx URL.

History

  • 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralLoading MasterPage Pin
anniejalbert9-Aug-07 3:49
anniejalbert9-Aug-07 3:49 
Generalerror in content page Pin
veerbala18-Apr-07 2:57
veerbala18-Apr-07 2:57 
GeneralRe: error in content page Pin
Mike Doe27-Apr-07 9:59
Mike Doe27-Apr-07 9:59 
GeneralCan't load projects in VS 2005 Pin
RichNFamous12-May-06 3:55
RichNFamous12-May-06 3:55 
GeneralCannot step into the code to debug... Pin
AArnie1-Mar-06 23:17
AArnie1-Mar-06 23:17 
GeneralRe: Cannot step into the code to debug... Pin
AArnie1-Mar-06 23:19
AArnie1-Mar-06 23:19 
GeneralASP.NET 2.0 Pin
Mike Doe2-Feb-06 16:23
Mike Doe2-Feb-06 16:23 
QuestionUser Controls and Events Pin
MrKeyz8826-Jan-06 4:04
MrKeyz8826-Jan-06 4:04 
AnswerRe: User Controls and Events Pin
Mike Doe2-Feb-06 16:17
Mike Doe2-Feb-06 16:17 
AnswerRe: User Controls and Events Pin
Mike Doe2-Feb-06 16:37
Mike Doe2-Feb-06 16:37 
QuestionValidations Help Pin
premrishi2-Sep-05 0:44
premrishi2-Sep-05 0:44 
AnswerRe: Validations Help Pin
Mike Doe12-Sep-05 5:49
Mike Doe12-Sep-05 5:49 
QuestionTrace mode not working? Pin
Ben Allfree12-Jul-05 12:57
Ben Allfree12-Jul-05 12:57 
AnswerRe: Trace mode not working? Pin
Mike Doe12-Sep-05 5:47
Mike Doe12-Sep-05 5:47 
GeneralskmMenu Pin
xtrac2222-Feb-05 13:22
xtrac2222-Feb-05 13:22 
GeneralRe: skmMenu Pin
Mike Doe6-Feb-05 2:18
Mike Doe6-Feb-05 2:18 
GeneralRe: skmMenu Pin
MrKeyz8826-Jan-06 3:54
MrKeyz8826-Jan-06 3:54 
GeneralMultiple placeholders on masterpage Pin
ilog.km31-Jan-05 1:33
ilog.km31-Jan-05 1:33 
GeneralRe: Multiple placeholders on masterpage Pin
Mike Doe6-Feb-05 2:17
Mike Doe6-Feb-05 2:17 
General&lt;% ... %&gt; code blocks Pin
Maxis300018-Jan-05 2:19
Maxis300018-Jan-05 2:19 
GeneralRe: &lt;% ... %&gt; code blocks Pin
Mike Doe18-Jan-05 16:47
Mike Doe18-Jan-05 16:47 
Generalthink i found a "bug" Pin
benblo16-Dec-04 14:34
benblo16-Dec-04 14:34 
GeneralHaving a problem with validators... Pin
moneyshot26-Nov-04 6:33
moneyshot26-Nov-04 6:33 
GeneralRe: Having a problem with validators... Pin
moneyshot26-Nov-04 7:01
moneyshot26-Nov-04 7:01 
GeneralMissing BaseAttribute.cs in zip on 11/10/2004 Pin
Anonymous10-Nov-04 12:31
Anonymous10-Nov-04 12:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.