ASP Net Master Pages
ASP Net Master Pages
<htmlxmlns="http://www.w3.org/1999/xhtml">
<headrunat="server">
<title>Untitled Page</title>
</head>
<body>
<formid="form1"runat="server">
<div>
<asp:ContentPlaceHolderID="ContentPlaceHolder1"runat="server">
</asp:ContentPlaceHolder>
</div>
</form>
</body>
</html>
The master page contains some common elements, like a head tag. The most important server-side controls are the form tag (form1) and the
ContentPlaceHolder (ContentPlaceHolder1). Let’s also write a simple web form to use our master page.
<%@PageLanguage="C#"MasterPageFile="~/Master1.master"
AutoEventWireup="true"Title="Untitled Page" %>
<asp:ContentID="Content1"Runat="Server"
ContentPlaceHolderID="ContentPlaceHolder1">
<asp:LabelID="Label1"runat="server"Text="Hello, World"/>
</asp:Content>
The web form contains a single Content control, which in turn is the proud parent of a Label. We can visualize what the object hierarchies would look like at
runtime with the following diagram.
At this point, the page and master page are two separate objects, each with their own children. When it comes time for the master page to do its job, the
master page replaces the page’s children with itself.
The master page’s next step is to look for Content controls in the controls formerly associated with the page. When the master page finds a Content control
that matches a ContentPlaceHolder, it moves the controls into the matching ContentPlaceHolder. In our simple setup, the master page will find a match for
ContentPlaceHolder1, and copy over the Label.
All of this work occurs after the content page’s PreInit event, but before the content page’s Init event. During this brief slice of time, the master page is
deserving of its name. The master page is in control - giving orders and rearranging controls. However, by the time the Init event fires the master page
becomes just another child control inside the page. In fact, the MasterPage class derives from the UserControl class. I’ve found it useful to only think of
master pages as masters during design time. When the application is executing, it’s better to think of the master page as just another child control.
The Pre_Init event we just mentioned is a key event to examine if we want to change the master page file programmatically. This is the next topic for
discussion.
using System;
using System.Web.UI;
publicclassBasePage : Page
{
public BasePage()
{
this.PreInit += newEventHandler(BasePage_PreInit);
}
<%@PageLanguage="C#"MasterPageFile="~/Master1.master"
AutoEventWireup="true"Title="Untitled Page"
Inherits="BasePage" %>
The inheritance approach is flexible. If a specific page doesn’t want it’s master page set, it can choose not to derive from BasePage. This is useful if different
areas of an application use different master pages. However, there may be times when we want an application to enforce a specific master page. It could be
the same type of scenario (we pull the master page name from a database), but we don’t want to depend on developers to derive from a specific base class
(imagine a third party uploading content pages). In this scenario we can factor the PreInit code out of the base class and into an HttpModule.
HttpModules sit in the ASP.NET processing pipeline and can listen for events during the processing lifecycle. Modules are good solutions when the behavior
you want to achieve is orthogonal to the page processing. For instance, authentication, authorization, session state, and profiles are all implemented as
HttpModules by the ASP.NET runtime. You can plug-in and remove these modules to add or discard their functionality. Here is a module to set the
MasterPageFile property on every Page object.
using System;
using System.Web;
using System.Web.UI;
publicclassMasterPageModule : IHttpModule
{
publicvoid Init(HttpApplication context)
{
context.PreRequestHandlerExecute += newEventHandler(context_PreRequestHandlerExecute);
}
publicvoid Dispose()
{
}
}
When the module initializes, it hooks the PreRequestHandlerExecute event. The PreRequestHandlerExecute fires just before ASP.NET begins to execute a
page. During the event handler, we first check to see if ASP.NET is going to execute a Page handler (this event will also fire for .asmx and .ashx files, which
don’t have a MasterPageFile property). We hook the page’s PreInit event. During the PreInit event handler we set the MasterPageFile property. Again, the
event handler might look up the filename from the database, or a cookie, or a session object, which is useful when you give a user different layouts to choose
from.
To use the module, we just need to add an entry to the application’s web.config.
<httpModules>
<addname="MyMasterPageModule"type="MasterPageModule"/>
</httpModules>
Abstract Interaction
Now it’s time to have the master page and content page interact. There are different approaches we can take to achieve interaction, but the best approaches
are the ones that use the master page for what it is: a user control. First, let’s look at how the content page can interact with the master page.
<formid="form1"runat="server">
<div>
<asp:contentplaceholderid="ContentPlaceHolder1"runat="server">
</asp:contentplaceholder>
</div>
<asp:Labelrunat="server"ID="FooterLabel"
Text="Default footer text"/>
</form>
The catch is, some content pages need to override the default footer text. Here is one approach we can use from page’s Page_Load event handler.
EndSub
Use the above approach with extreme caution. FindControl is fragile, and will return null if someone renames FooterLabel, or removes the control entirely.
This problem can't be discovered until runtime. FindControl also has some additional difficulties when INamingContainers are involved - we will discuss
this topic later.
A better approach is to establish a formal relationship between the master page and content page, and take advantage of strong typing. Instead of the content
page poking around inside the master page, let’s have the master page expose the footer text as a property. We can add the following code to our master
page.
<%@PageLanguage="VB"MasterPageFile="~/Master1.master"
AutoEventWireup="true" %>
<%@MasterTypeVirtualPath="~/Master1.master" %>
<scriptrunat="server">
EndSub
</script>
This code is a cleaner and doesn’t depend on the magic string “FooterLabel”. If anyone ever removes the control from the master page, or renames the
control, we will have compilation errors instead of runtime problems.
What if we have 2 different master pages in the application? In this scenario, we have a problem, because the VirtualPath attribute supports only a single
master page. We’ve tightly coupled our page to a specific master. If we assign a MasterPageFile that does not match the MasterType, the runtime will throw
an exception.
Unable to cast object of type 'ASP.master2_master' to type 'ASP.master1_master'.
Fortunately, the @ MasterType directive doesn’t require us to use a VirtualPath, we can also specify a type name. Once again we will turn to inheritance to
solve this problem. If all the content pages expect their master pages to have footer text, then let’s define a base class for the master pages to inherit.
We can take one of two approaches with the base class. One approach is to use an abstract (MustInherit) base class:
using System.Web.UI;
publicabstractclassBaseMasterPage : MasterPage
{
publicabstractstring FooterText
{
get;
set;
}
}
Our master pages must inherit from this base class and override the FooterText property.
<%@MasterLanguage="VB"Inherits="BaseMasterPage" %>
<scriptrunat="server">
</script>
Now our page can use any master page that inherits from BaseMasterPage. All we need is an @ MasterType directive set to the base class. Instead of using a
VirtualPath attribute, we use a TypeName attribute and specify the name of the base class.
<%@PageLanguage="VB"MasterPageFile="~/Master1.master"
AutoEventWireup="true" %>
<%@MasterTypeTypeName="BaseMasterPage" %>
<scriptrunat="server">
EndSub
</script>
The second approach is to use a concrete base class. This approach is possible only if we are sure every master page will have a label with an ID of
“FooterLabel”.
using System.Web.UI;
using System.Web.UI.WebControls;
publicclassBaseMasterPage : MasterPage
{
protectedLabel FooterLabel;
publicstring FooterText
{
get
{
return FooterLabel.Text;
}
set
{
FooterLabel.Text = value;
}
}
}
With the above approach we can remove code from our master page – we don’t need to define the FooterText property. If we are using code-beside files
instead of inline script, we need to use CodeFileBaseClass=”BaseMasterPage” in the @ Master directive to ensure ASP.NET can wire up the base class’s
Label field with the Label control.
<asp:TextBoxrunat="server"id="EmailAddressBox"/>
<asp:Buttonrunat="server"ID="SendEmailButton"
OnClick="SendEmailButton_Click"/>
What happens when the user clicks the button? We can choose from the following options:
Handle the Click event in the master page, and have the master page email the report.
Expose the Button and TextBox as public properties of the master page, and let the content page subscribe to the click event (and email the report).
Define a custom SendEmail event, and let each page subscribe to the event.
The first approach can be ugly because the master page will need to call methods and properties on the page. Master pages are about layout, we don’t want to
clutter them with knowledge of reports and specific pages.
The second approach is workable, but it tightly couples the page to the master. We might change the UI one day and use a DropDownList and a Menu
control instead of a TextBox and Button, in which case we’ll end up changing all of our pages.
The third approach decouples the master page and content page nicely. The page won’t need to know what controls are on the master page, and the master
page doesn’t have to know anything about reports, or the content page itself. We could start by defining the event in a class library, or in a class file in
App_Code.
using System;
publicclassSendEmailEventArgs : EventArgs
{
public SendEmailEventArgs(string toAddress)
{
_toAddress = toAddress;
}
privatestring _toAddress;
publicstring ToAddress
{
get { return _toAddress; }
set { _toAddress = value; }
}
publicdelegatevoidSendEmailEventHandler(
object sender, SendEmailEventArgs e);
We can raise this event from a master page base class (if we have one), or from the master page itself. In this example, we will raise the event directly from
the master page.
<%@MasterLanguage="VB" %>
<scriptrunat="server">
PublicEvent SendEmail As SendEmailEventHandler
EndSub
</script>
We'll need to add some validation logic to the master page, but at this point all we need is to handle the event in our page. We could also handle the event
from a base page class, if we don’t want to duplicate this code for every page.
<%@PageLanguage="VB"MasterPageFile="~/Master1.master"
AutoEventWireup="true" %>
<%@MasterTypeVirtualPath="~/Master1.master" %>
<scriptrunat="server">
' do work
EndSub
</script>
<asp:TextBoxrunat="server"id="QueryBox"/>
<asp:Buttonrunat="server"ID="SearchButton"
PostBackUrl="~/SearchResults.aspx"/>
When the user click the search button, the web request will ultimately arrive at the SearchResults.aspx. How will SearchResults.aspx find the text the user
wants to search for? We could use the PreviousPage.Master property and FindControl to locate the QueryBox TextBox by its ID, but we’ve already
discussed some reasons to avoid FindControl when possible.
What about the exposing the text as a property? It sounds easy, but...
In ASP.NET 2.0, each master page and web form can compile into a separate assembly. Unless we establish a reference between two assemblies, the types
inside each assembly cannot see one another. The @ MasterType directive with a VirtualPath attribute ensures the web form’s assembly will reference the
master page assembly. If our SearchResults.aspx page uses the same @ MasterType directive as the POSTing web form, it will be able to see the master
page type, and life is simple.
Let’s assume our SearchResults.aspx page does not use a master page, and we don’t want to use FindControl. Inheritance is once again a solution to this
problem. We will need a base class (or an interface) defined in App_Code or a class library (all web form and master page assemblies reference the
App_Code assembly). Here is a base class solution.
publicclassBaseMasterPage : MasterPage
{
protectedLabel PageFooter;
protectedTextBox QueryBox;
publicstring QueryText
{
get { return QueryBox.Text; }
}
// ...
SearchResults.aspx will assume the PreviousPage.Master property references a type derived from BaseMasterPage.
' do search
EndIf
While the above approach works pretty, well, you might consider going a step further. Define an interface with a QueryText property and derive a base page
(not master page) class from the interface. The base page class can go to the trouble of getting the text from the master page. Now, SearchResults.aspx
doesn’t have to worry about master pages at all. It can use a cast to get a reference to the interface from the PreviousPage reference, and then ask the
interface for the QueryText. Any type of page can then post to SearchResults, even those without a master page.
EndSub
The Page class contains a public property named Header. Header gives us access to the head tag as a server side control (the head tag in the master page
must include runat=”server” for the Header property to work). We can add style sheets to the header tag, too.
EndSub
We can also add markup inside the head tag using an HtmlGenericControl, which provides TagName, InnerText, InnerHtml, and Attributes properties.
<headrunat="server">
<title>Untitled Page</title>
<asp:ContentPlaceHolderid="headerPlaceHolder"runat="server"/>
</head>
<body>
<formid="form1"runat="server">
<div>
<asp:ContentPlaceHolderID="ContentPlaceHolder1"runat="server">
</asp:ContentPlaceHolder>
</div>
<asp:Labelrunat="server"ID="PageFooter"Text="Default footer text"/>
</form>
</body>
</html>
This master page uses a ContentPlaceHolder inside the head tag. Remember, a Content page isn’t required to provide a Content control for every
ContentPlaceHolder control in a master page. If there is no Content control available for the master to merge into a ContentPlaceHolder, the master page
uses the default content inside of the ContentPlaceHolder. In the above code, we did not specify any default content, but this is a trick to remember if you
want to provide default content with the ability to replace the default content from any given content page.
With the ContentPlaceHolder above, any content page can add additional tags inside the head tag using a Content control.
<asp:ContentID="HeaderContent"runat="server"
ContentPlaceHolderID="headerPlaceHolder">
<linkrel="stylesheet"type="text/css"href="customstyles.css"/>
</asp:Content>
<asp:ContentID="Content1"Runat="Server"
ContentPlaceHolderID="ContentPlaceHolder1">
<asp:LabelID="Label1"runat="server"Text="Hello, World"/>
</asp:Content>
We mentioned there is a drawback to this approach -what is the catch?
The problem is that Visual Studio 2005 believes all ContentPlaceHolder controls should live inside the <form> tag. The ContentPlaceHolder we have inside
the head tag will produce an error message in the Visual Studio Error List window. However, the project will compile and run without any complaints,
exceptions, or error messages. The error appears to be generated by the Visual Studio validation engine. We could disable validation for the project,
however, this disables validation of all HTML mark-up. You’ll have to decide if you can live the spurious validation error message before taking the
ContentPlaceHolder approach.
<%@PageLanguage="VB"MasterPageFile="~/Master1.master"
AutoEventWireup="true"Title="Home"Inherits="BasePage"
MetaKeywords="masterpage ASP.NET"
%>
To use the MetaKeywords attribute in every page of an application, we just need to inherit from a common base class that exposes a MetaKeywords
property. The base class can also inject the meta tag into the page header.
using System;
using System.Web.UI;
using System.Web.UI.HtmlControls;
publicclassBasePage : Page
{
public BasePage()
{
Init += newEventHandler(BasePage_Init);
}
privatestring _metaKeywords;
publicstring MetaKeywords
{
get { return _metaKeywords; }
set { _metaKeywords = value; }
}
}
<scriptrunat="server">
ProtectedSub Page_Load(ByVal sender AsObject, _
ByVal e As System.EventArgs)
Page.FindControl("Label1").Visible = False
EndSub
</script>
<asp:ContentID="Content1"Runat="Server"
ContentPlaceHolderID="ContentPlaceHolder1">
<asp:LabelID="Label1"runat="server"Text="Hello, World"/>
</asp:Content>
FindControl in the above code returns a null (Nothing) reference. Why? Let’s turn to the FindControl documentation on MSDN.
FindControl searches the current naming container for the specified server control.
A naming container is any control that carries the INamingContainer interface. Both the MasterPage and Content controls are naming containers. The key to
using FindControl is to invoke the method on the correct container, because FindControl doesn’t recursively traverse the entire hierarchy of controls.
FindControl only searches inside the current naming container. Using the FindControl method on the Page reference means we won’t be searching inside of
MasterPage control. course, we don’t need to use FindControl in this scenario because our content page will have a Label1 field, but if you do need to use
FindControl for a control in a content page, the following code will be helpful.
EndSub
First, our code has to find the ContentPlaceHolder containing the Label control. We will use the MasterPage control's FindCotnrol method. The MasterPage
inside of our page is the naming container that contains ContentPlaceHolder1. If you are wondering why we are not using the Content1 control, it’s because
no Content controls exist. Remember our early discussion on how master pages work. Master pages copy the controls inside of the Content controls into
ContentPlaceHolder controls. The Content controls get left behind and don’t exist in the control hierarchy.
Once we have a reference to the ContentPlaceHolder control, we use FindControl a second time to locate the Label control. We could shorten all the above
code into a single line:
Master.FindControl(...).FindControl(..).Visible = False
For more details on using FindControl, see “In Search Of ASP.NET Controls”.
Name Mangling
A naming container also mangles its children’s ClientID property. Mangling ensures all ClientID properties are unique on a page. For instance, the ID for
our Label control is “Label1”, but the ClientID of the Label is “ctl00_ContentPlaceHolder1_Label1”. Each level of naming container prepends it’s ID to the
control (the MasterPage control ID in this form is ctl00). Just as we have to be careful with FindControl, we have to be careful with client side script
functions like getElementById. If we emit the following script into our page, it will fail with a JavaScript error: ‘Label1 is undefined’.
<scripttype="text/javascript">
<!--
Label1.innerHTML = 'Hello, from script!';
// -->
</script>
One 'solution' is to use the correct client side ID.
<scripttype="text/javascript">
<!--
ctl00_ContentPlaceHolder1_Label1.innerHTML = 'boo!';// -->
</script>
Of course, we’d never want to hardcode the client ID into a script. Typically we’ll need to build the script dynamically using StringBuilder or String.Format.
Another alternative is to use markers in the script and use a call to String.Replace, like the following.
ClientScript.RegisterStartupScript( _
Me.GetType(), scriptKey, script, addScriptTags _
)
EndSub
<div>
<imgsrc="logo.gif"alt="Company Logo"/>
<asp:ContentPlaceHolderID="ContentPlaceHolder1"runat="server">
</asp:ContentPlaceHolder>
</div>
As long as the master page and the web form live in the same directory, the company logo will display in the browser. When the master page and web form
live in different directories, the image will not appear. The browser requests knows nothing about master pages. The browser will interpret any relative paths
it finds in the HTML as being relative to the webform. If our logo and master page files are in the root directory, but the web form is in a subdirectory, the
browser will ask for logo.gif from the same subdirectory. The server will respond with a 404 (file not found) error.
The good news is, the ASP.NET runtime does provide a feature called “URL rebasing”. The runtime will try to “rebase” relative URLs it finds on server-
side controls inside a master page. This means the following relative path will work, no matter where the master page and web form live.
<imgsrc="logo.gif"alt="Company Logo"runat="server"/>
We’ve added a runat=”server” attribute to the image tag, making the <img> a server-side control. When the master page file and logo are in the root
directory, but the web form is in a subdirectory, the ASP.NET runtime will rebase the relative path it finds in the src attribute to point to the root of the
website.
The following code will also work, because we are using a server-side Image object.
<asp:ImageImageUrl="logo.gif"runat="server"/>
The ASP.NET runtime will also rebase paths it finds inside of the head tag. Take the following excerpt from a master page:
<headrunat="server">
<title>Untitled Page</title>
<linkhref="styles/styles.css"type="text/css"rel="stylesheet"/>
</head>
If we request a webform from a subdirectory, the runtime will catch the href inside the link tag and rebase the URL to "../styles/styles.css". However, the
runtime doesn’t catch everything. If we included our style sheet with the following code, the runtime won’t rebase the relative href.
<headrunat="server">
<styletype="text/css"media="all">
@import "styles/styles.css";
</style>
</head>
Also, the runtime doesn’t rebase URLs inside of embedded styles, and not all attributes are covered (the background attribute, for instance).
<bodybackground="logo.gif"runat="server">
<!-- the background for the body tag will break -->
<formid="form1"runat="server">
<divid="Div1"style="background-image: url('logo.gif');"runat="server">
<!-- My background is also broken. -->
</div>
If you need to use a relative path in an area where the runtime does not provide the rebasing feature, you can compute a client side URL using
ResolveClientUrl and passing a relative path. ResolveClientUrl, when called from inside a master page, will take into account the location of the master
page, the location specified in the HTTP request, and the location specified by the relative path parameter to formulate the correct relative path to return.
body
{
background-image:url('images\logo.gif');
}
Relative paths are safe inside a .css file because the browser will always request logo.gif relative to the location of the stylesheet.
<asp:ImageID="Image1"runat="server"SkinID="logo"/>
Different logos can exist theme, and the skin we defined will only apply to Image controls with a SkinID of “logo”.
<%@MasterLanguage="VB" %>
<htmlxmlns="http://www.w3.org/1999/xhtml">
<headrunat="server">
<title>Untitled Page</title>
</head>
<body>
<formid="form1"runat="server">
<div>
<asp:ContentPlaceHolderID="ContentPlaceHolder1"runat="server">
</asp:ContentPlaceHolder>
</div>
</form>
</body>
</html>
Then, we create a second master page (Nested.master) that uses master1.master as a master page.
<%@MasterLanguage="VB"MasterPageFile="~/Master1.master" %>
<asp:Contentrunat="server"ID="Content1"
ContentPlaceHolderID="ContentPlaceHolder1">
<h3>Nested Content</h3>
<asp:contentplaceholderid="NestedContent"runat="server">
</asp:contentplaceholder>
</asp:Content>
Finally, a content page which uses Nested.master as its MasterPageFile.
<%@PageLanguage="VB"MasterPageFile="~/Nested.master" %>
<asp:ContentID="Content1"ContentPlaceHolderID="NestedContent"Runat="Server">
</asp:Content>
If we attempt to view this content page in design view, Visual Studio will produce the error message shown earlier. If we really want to use the designer with
our content page, we can leave the MasterPageFile attribute empty, like in the following code:
<%@PageLanguage="VB"MasterPageFile="" %>
<asp:ContentID="Content1"ContentPlaceHolderID="NestedContent"Runat="Server">
</asp:Content>
We can’t just drop the MasterPageFile attribute from the @ Page directive, because the designer will raise a different error (“Content controls are allowed
only in content page that references a master page”). The empty attribute appears to trick the designer into allowing us into design mode.
At runtime, however, the page will throw an exception because it doesn’t have a master file. We can avoid the exception by programmatically setting the
MasterPageFile property at runtime. We know we will need to set the master page before or during the PreInit event. The following code reads the
masterPageFile attribute from the <pages> section of web.config. By putting the code into a base class, we can cover all the content pages in an application.
using System;
using System.Web.UI;
using System.Web.Configuration;
using System.Configuration;
publicclassBaseContentPage : Page
{
protectedoverridevoid OnPreInit(EventArgs e)
{
base.OnPreInit(e);
PagesSection pagesConfig =
ConfigurationManager.GetSection("system.web/pages")
asPagesSection;
MasterPageFile = pagesConfig.MasterPageFile;
}
}