Click here to Skip to main content
Email Password   helpLost your password?

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:

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.

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.

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:

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:

// 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":

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.

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

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.

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

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:

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

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:

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:

  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:

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

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralLoading MasterPage
anniejalbert
4:49 9 Aug '07  
Hi!

We're having a web application that use by different customer. We put a masterPage to be able to personalize the look of each customer.

We have a build and the techs install this kit. Each time we have a new customer I have to create an another kit with the new masterpage. Now we have 10 customers an the application I have 10 differents masterpage, but to be able to load the masterpage, it had to built in the application.

My question is, how can I have only one installation kit and when I have a new customer, put the new masterpage, change the web.config and the application can charge the masterpage same if the masterpage is not in the built?

Thank you very much!



Annie
Sorry for my english , I'm a french...Quebec
Generalerror in content page
veerbala
3:57 18 Apr '07  
I am creating a master page and then adding a content page to it. when i place controls(tags) like update panel or table or textbox or label in the content page(within the content tags) i get a red line under all the tags in that page. when i move my mouse over it, it says "Element UpdatePanel is not a known element This can occur if there is a compilation error in the website" . I have tried all possible alterations but in vain. please help me out. many thanks


balaji
GeneralRe: error in content page
Mykola Dolmatov
10:59 27 Apr '07  
You don't have to specify content tags on the content page.

Your content pages should stay exactly the same ( you have to only specify MasterContextPage to be the base class of your content page)

In your master page you define the placeholder where the framework will insert the controls from your content page using
<DIV runat="server" id="masterContextContainer"> markup.


GeneralCan't load projects in VS 2005
RichNFamous
4:55 12 May '06  
The projects don't convert properly: is there a workaround for this?
GeneralCannot step into the code to debug...
AArnie
0:17 2 Mar '06  
Hi,

I have the examples running (I converted to vb .net). If I add a breakpoint anywhere within the code, the breakpoint is hit, but then immediately jumps back out and the page finishes loading. I don't get a chance to debug at all. Any suggestions on how I may get around/solve this issue?

Many Thanks,

Duncan.
GeneralRe: Cannot step into the code to debug...
AArnie
0:19 2 Mar '06  
I should probably also mention, I cannot do this in C# either, and this occurs in all pages within my solution.

Duncan.
GeneralASP.NET 2.0
Mykola Dolmatov
17:23 2 Feb '06  
After one week of trying to marry ComponentArt and cascading Master Pages in ASPNET 2.0 i moved my project back to my master pages.
The implementation of the native master pages is basically creating a new user control for every master page layer( yes, that means prefixes for all the controls on the page multiple times how many layers of master pages you have ) and that screws up ComponentArt components when inserted into second or third level of the master pagesFrown

The framework still works. The only problem i encountered so far is that skins dont work. the rest - works perfectly.

i will see if i can resolve that one.
QuestionUser Controls and Events
MrKeyz88
5:04 26 Jan '06  
I have a user control added to my context page at design time. The user control is tested to function properly when included in a standard aspx web form (not a context page). However, the postback events are not firing when it is used on a context page. I noticed a similar post concerning viewstate, but I'm not sure if that is the same issue or not. I've been using the MasterPages for quitte some time, and they are great! This is the first real issue I've had. Any help or guidance is appreciated.
AnswerRe: User Controls and Events
Mykola Dolmatov
17:17 2 Feb '06  
Hi there...
Hmmm, that's interesting - i will try to figure out what's going on during the weekend.
AnswerRe: User Controls and Events
Mykola Dolmatov
17:37 2 Feb '06  
Just tested it - i created user control with label textbox and button, created handler for the button that was changing the label with the content of the text box, dragged the control onto the context page and everything worked..:(

maybe you can create a small test project and send it to me?

QuestionValidations Help
premrishi
1:44 2 Sep '05  
Please guide on making the project accept validation.
Your help would be very much useful.

Thank you,
Angel
AnswerRe: Validations Help
Mykola Dolmatov
6:49 12 Sep '05  
Hmmm, i do have validation in my projects and it works fine...
Can you check out the guy's post down the thread - i think he had the same issue and figured out how to fix it....

I will try to duplicate the behaviour and add it to the framework
GeneralTrace mode not working?
Ben Allfree
13:57 12 Jul '05  
When I enable page tracing, it complains that multiple controls are named _ctl0

Any ideas?
GeneralRe: Trace mode not working?
Mykola Dolmatov
6:47 12 Sep '05  
Hmmm, probably tracing inserts literal controls to the page..i will check it out..

GeneralskmMenu
xtrac222
14:22 2 Feb '05  
First of all thanks a million for this excellent master pages solution!

I'm trying to implement the menus on a master page using skmMenu (http://www.skmmenu.com/menu/) but I'm having problems.

I can configure a menu to work on a context page using the bind to XML file method no problem so I know the sknMenu DLL is correctly referrenced however immediately I implement a menu on a master page I get an "Object reference not set to an instance of an object" error.

I'd be extremely grateful for any guidance...Confused

Matthew
GeneralRe: skmMenu
Mykola Dolmatov
3:18 6 Feb '05  
Please, send me the sample project by email - i will see what can be done.
GeneralRe: skmMenu
MrKeyz88
4:54 26 Jan '06  
I have had this problem too, and found that putting the skmMenu in a user control, then adding that to the master page solves the problem (better late than never)
GeneralMultiple placeholders on masterpage
ilog
2:33 31 Jan '05  
Currently the approach is limited with one placeholder ("masterContextContainer") per masterpage. Why not to add another attribute TargetID to ContextPageAttribute class pointing to the necessary ID on the page? Confused
GeneralRe: Multiple placeholders on masterpage
Mykola Dolmatov
3:17 6 Feb '05  
Well, the idea of the master pages is that you have one context page "wrapping" itself into many master pages. That means in the implementation all the master pages are not actually rendering the context - they're just creating the page controls. Master page does not contain reference to the context page that is using it.
Cascading effect was possible by calling master pages from the top of the chain to the bottom.


Your idea is very interesting too, but it wouldnt be called "master pages", it would be called "Templating" and implementation would slitely change. in this case a page contains reference to all the template pages used in that page ( although that could be dynamicaly controlled by overriding the certain properties)

If you want you can work on another attribute called "TemplatePageAttribute" and add the   code that would aquire the template sub-pages into a context page - and we could add your solution as the second chapter of this article Smile I like your idea and if you dont want to do it i would probably spend some time to write that second chapter.

example:

[ContextPage( TemplatePageAliases="templatePage1, templatePage2, templatePage3")]
and we would use the alias as the attribute of the placeholder div
<div id="templatePage" alias="templatePage1">
</div>

and marker attribute of course
[TemplatePage]
General<% ... %> code blocks
Maxis3000
3:19 18 Jan '05  
When i try to use <% ... %> code blocks on my ContextPage, i got an error:

The Controls collection cannot be modified because the control contains code blocks (i.e. <% ... %>).

How to work with <%..%>
?

Thanks
GeneralRe: <% ... %> code blocks
Mykola Dolmatov
17:47 18 Jan '05  
well, i had to switch over to databinding blocks
lets say if you want to display page property name or method you would do something like <%# PagePropertyName %> and then call page.databind() to fill the values....

But if you did use some c# code or VB code in those blocks - sorry, it won't work for youFrown ...
Generalthink i found a "bug"
benblo
15:34 16 Dec '04  
This piece of code (MasterContextPage class) seems strange to me:

for( int j = 0; j < contextCollection.Count; )
{
subControl = contextCollection[j];
if( subControl == null) continue;
contextCollection.Remove(subControl);
contextContainer.Controls.Add( subControl);
}

If subControl ever happens to be null (though i dont really see how this could be but u never know i suppose...), then you hit continue, so contextCollection wont decrement, and as j always stays 0, subControl wont change... and you're caught in an infinite loop. Maybe u meant break instead of continue?

BTW that's the first time i ever saw a for built that way (hope i'm reading it ok), thx for the teaching!
Also note i dont use your framework so i didnt step on the bug, i just happened to reuse this piece of code.
Anyway... happy coding!
GeneralHaving a problem with validators...
moneyshot
7:33 26 Nov '04  
Well I had this problem one other time with a different template system I was using. When you place a button on the form, set the Causes Validation property to true the button does not get the javascript. I found an answer that stated that they validators were being lost at some point and was suggested to add the following code in order to grab the validators then put them back after the code work was done. I am just not sure where to put this code.

// Happens before controls are built and added to the form
ValidatorCollection myValidators = new ValidatorCollection();
this.CopyValidators(Page.Validators, myValidators);

// Happens after controls are built and added to the form
this.CopyValidators(myValidators, Page.Validators);

// Copy Validators method used above
protected void CopyValidators(ValidatorCollection p_src_Validators, ValidatorCollection p_dst_Validators)
{
IEnumerator myEnum = p_src_Validators.GetEnumerator();

Hashtable mySessionValues = new Hashtable();

while(myEnum.MoveNext())
{
p_dst_Validators.Add((IValidator)myEnum.Current);
}


Sigh
GeneralRe: Having a problem with validators...
moneyshot
8:01 26 Nov '04  
Scratch that. Figured out where to add it. In the OnContextPageLoad() method I added

// Happens before controls are built and added to the form
ValidatorCollection myValidators = new ValidatorCollection();
this.CopyValidators(Page.Validators, myValidators);

at the top of the method and

// Happens after controls are built and added to the form
this.CopyValidators(myValidators, Page.Validators);

at the end of the method.

then I put the copyvalidators method at the end of the class. This fixed the issue. May be worth adding this in some form to your framework.
GeneralMissing BaseAttribute.cs in zip on 11/10/2004
Anonymous
13:31 10 Nov '04  
The downloaded zip is missing BaseAttribute.cs in zip on 11/10/2004.


Last Updated 10 Nov 2004 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010