Master pages
are a great addition to the ASP.NET 2.0 feature set. Master pages help
us build consistent and maintainable user interfaces. Master pages,
however, are not without their quirks. Sometimes master page behavior is
surprising, and indeed the very name master page can be a bit
misleading. In this article, we are going to examine some of the common
problems developers run into when using master pages, and demonstrate
some practical advice for making effective use of master pages. For an
introduction to master pages, see http://www.odetocode.com/Articles/419.aspx
To make use of master pages, we first need to understand how master
pages work. Many of the tips and traps covered later in this article
revolve around understanding the magic behind master pages. Let’s dig
into these implementation details first.
For Internal Use Only
When a web request arrives for an ASP.NET web form using a master
page, the content page (.aspx) and master page (.master) merge their
content together to produce a single page. Let’s say we are using the
following, simple master page.
<%@ Master Language="VB" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:ContentPlaceHolder ID="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.
<%@ Page Language="C#" MasterPageFile="~/Master1.master"
AutoEventWireup="true" Title="Untitled Page" %>
<asp:Content ID="Content1" Runat="Server"
ContentPlaceHolderID="ContentPlaceHolder1" >
<asp:Label ID="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.
Handling the PreInit Event
We can use the @ Page directive and the web.config to specify master
page files for our web forms, but sometimes we want to set the master
page programatically. A page’s MasterPageFile property sets the master
page for the content page to use. If we try to set this property from
the Load event, we will create an exception. In other words, the
following code…
protected void Page_Load(object sender, EventArgs e)
{
MasterPageFile = "~/foo";
}
… creates the following exception.
The 'MasterPageFile' property can only be set in or before the 'Page_PreInit' event.
This exception makes sense, because we know the master page has to
rearrange the page’s control hierarchy before the Init event fires. The
simple solution is to just use the PreInit event, but we probably don’t
want to write the PreInit event handler over and over for each web form
in our application. Chances are good the PreInit event handler will need
to look up the master page name from a database, or a cookie, or from
some user preference settings. We don’t want to duplicate this code in
every webform. A better idea is to create a base class in a class
library project, or in the App_Code directory. (For a Visual Basic
version of the code snippets in this section, see http://odetocode.com/Blogs/scott/archive/2005/12/09/2604.aspx).
using System;
using System.Web.UI;
public class BasePage : Page
{
public BasePage()
{
this.PreInit += new EventHandler(BasePage_PreInit);
}
void BasePage_PreInit(object sender, EventArgs e)
{
MasterPageFile = "~/Master1.master";
}
}
To use this base class, we need to change our code-beside file
classes to inherit from BaseClass instead of System.Web.UI.Page. For web
forms with inline code, we just need to change the Inherits attribute
of the @ Page directive.
<%@ Page Language="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;
public class MasterPageModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.PreRequestHandlerExecute += new EventHandler(context_PreRequestHandlerExecute);
}
void context_PreRequestHandlerExecute(object sender, EventArgs e)
{
Page page = HttpContext.Current.CurrentHandler as Page;
if (page != null)
{
page.PreInit +=new EventHandler(page_PreInit);
}
}
void page_PreInit(object sender, EventArgs e)
{
Page page = sender as Page;
if (page != null)
{
page.MasterPageFile = "~/Master1.master";
}
}
public void 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>
<add name="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.
Content Page to Master Page Interaction
Let’s imagine we want all of the pages in our application to have
some text in a footer area. This seems like the perfect job for a master
page, so we will add a label control to our master.
<form id="form1" runat="server">
<div>
<asp:contentplaceholder id="ContentPlaceHolder1" runat="server">
</asp:contentplaceholder>
</div>
<asp:Label runat="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.
Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As EventArgs)
Dim footer As Label = Master.FindControl("FooterLabel")
If Not footer Is Nothing Then
footer.Text = "Custom footer text!!"
End If
End Sub
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.
Public Property FooterText() As String
Get
Return FooterLabel.Text
End Get
Set(ByVal value As String)
FooterLabel.Text = value
End Set
End Property
The best way to use this property is to place a @ MasterType
directive in our content page. When the ASP.NET compiler sees the @
MasterType directive, it creates a strongly typed Master property in our
Page derived class.
<%@ Page Language="VB" MasterPageFile="~/Master1.master"
AutoEventWireup="true" %>
<%@ MasterType VirtualPath="~/Master1.master" %>
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
Master.FooterText = "Custom footer text"
End Sub
</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;
public abstract class BaseMasterPage : MasterPage
{
public abstract string FooterText
{
get;
set;
}
}
Our master pages must inherit from this base class and override the FooterText property.
<%@ Master Language="VB" Inherits="BaseMasterPage" %>
<script runat="server">
Public Overrides Property FooterText() As String
Get
Return FooterLabel.Text
End Get
Set(ByVal value As String)
FooterLabel.Text = value
End Set
End Property
</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.
<%@ Page Language="VB" MasterPageFile="~/Master1.master"
AutoEventWireup="true" %>
<%@ MasterType TypeName="BaseMasterPage" %>
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As EventArgs)
Master.FooterText = "Use the base class..."
End Sub
</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;
public class BaseMasterPage : MasterPage
{
protected Label FooterLabel;
public string 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.
Master Page To Content Page Interaction
Here is a case where the master part of the master page name
can be misleading. The master page sounds like a good place to put
logic and code that will tell the page how to do something. After all, a
master page is the master, right? We now know that the master page is
just another child control. Ideally, the master page will remain
passive. Instead of telling it’s parent page what to do, the master page should tell a page when something interesting happenes, and let the page decide what to do.
Let’s pretend every page in our application displays a report, and
every page needs a button for users to click and email the report.
Putting a Button and a TextBox inside the master page seems like a
reasonable choice.
<asp:TextBox runat="server" id="EmailAddressBox" />
<asp:Button runat="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;
public class SendEmailEventArgs : EventArgs
{
public SendEmailEventArgs(string toAddress)
{
_toAddress = toAddress;
}
private string _toAddress;
public string ToAddress
{
get { return _toAddress; }
set { _toAddress = value; }
}
}
public delegate void SendEmailEventHandler(
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.
<%@ Master Language="VB" %>
<script runat="server">
Public Event SendEmail As SendEmailEventHandler
Protected Sub SendEmailButton_Click(ByVal sender As Object, _
ByVal e As System.EventArgs)
Dim eventArgs As New SendEmailEventArgs(EmailAddressBox.Text)
RaiseEvent SendEmail(Me, eventArgs)
End Sub
</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.
<%@ Page Language="VB" MasterPageFile="~/Master1.master"
AutoEventWireup="true" %>
<%@ MasterType VirtualPath="~/Master1.master" %>
<script runat="server">
Protected Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs)
AddHandler Master.SendEmail, AddressOf EmailReport
End Sub
Protected Sub EmailReport(ByVal sender As Object, ByVal e As SendEmailEventArgs)
Dim address As String = e.ToAddress
' do work
End Sub
</script>
Master Pages and Cross Page Postbacks
Another common scenario for master pages is to use a cross page post
back. This is when a control on the master page POSTs to a second web
form. For more information on cross page post backs, see “http://www.odetocode.com/Articles/421.aspx”. Let’s add search functionality to our site by adding a TextBox and Button to the master page.
<asp:TextBox runat="server" id="QueryBox" />
<asp:Button runat="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.
public class BaseMasterPage : MasterPage
{
protected Label PageFooter;
protected TextBox QueryBox;
public string QueryText
{
get { return QueryBox.Text; }
}
// ...
SearchResults.aspx will assume the PreviousPage.Master property references a type derived from BaseMasterPage.
Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As EventArgs)
If Not PreviousPage Is Nothing AndAlso _
Not PreviousPage.Master Is Nothing Then
Dim master As BaseMasterPage
master = DirectCast(PreviousPage.Master, BaseMasterPage)
Dim searchTerm As String
searchTerm = master.QueryText
' do search
End If
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.
A Curious Turn of Events
Another master page twist that catches developers off guard is the
order of the page lifecycle events. Let’s say we write the following
code in our web form:
Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As System.EventArgs)
Response.Write("Hello from Page_Load in default.aspx <br>")
End Sub
.. and the following code in our master page:
Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As System.EventArgs)
Response.Write("Hello from Page_Load in Master1.master<br>")
End Sub
Pop quiz: which Response.Write will appear in the output first?
Hint: most ASP.NET events are raised starting at the top of the control tree and working downward.
In this case, “Hello from Page_Load in default.aspx” will appear
before “Hello from Page_Load in Master1.master”, because the content
page’s Load event fires before the master page’s Load event.
Let’s set up another quiz using the following code in our content page.
Protected Sub Page_Init(ByVal sender As Object, _
ByVal e As System.EventArgs)
Response.Write("Hello from Page_Init in default.aspx <br>")
End Sub
... and the following code in our master page.
Protected Sub Page_Init(ByVal sender As Object, _
ByVal e As System.EventArgs)
Response.Write("Hello from Page_Init in Master1.master<br>")
End Sub
Pop quiz: which Init event will fire first?
Earlier we said most ASP.NET events work their way down the tree of controls. The truth is all lifecycle events (Load, PreRender, etc.) work in this fashion except the Init event. The initialization event works from the inside out. Since the master page is inside the content page, the master page’s Init event handler will fire before the content page’s Init event handler.
Obviously, problems will occur if the content page’s Load event
handler depends on the master page's Load event to finish some work or
initialize a reference. If you find yourself with this problem, or are
worried about the order of events when a master page is involved, you
might be too tightly coupled to the master page. Consider our earlier
approach of using a custom event when when something interesting happens
in the master page, and let the content page subscribe to the event and
take action. This approach achieves greater flexibility.
Headers, Scripts, and Meta Tags, Too
Generally, master pages will take care of including the HTML head
tag. The HTML head tag can include a <title> tag (to set the page
title), one or more <script> tags (to include JavaScript
libraries), and one or more <meta> tags (to include meta data
about the page). A content page will often need to modify or augment the
contents of the head tag. The title tag is a good example, because the
master page can’t set the title for each content page in an application.
Only the content pages know what thier title will be. Fortunately,
ASP.NET provides a public property on the Page class, and we can set a
content page’s title declaratively in the @ Page directive.
<%@ Page Language="VB" MasterPageFile="~/Master1.master"
AutoEventWireup="true" Title="Home"
%>
If we want to add script or meta tags from a content page, we have
more work to do. Here is an example of injecting a redirection meta
tag:
Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As System.EventArgs)
Dim metaTag As New HtmlMeta
metaTag.HttpEquiv = "Refresh"
metaTag.Content = "2;URL=http://www.OdeToCode.com"
Header.Controls.Add(metaTag)
End Sub
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.
Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As System.EventArgs)
Dim cssLink As New HtmlLink()
cssLink.Href = "~/styles.css"
cssLink.Attributes.Add("rel", "stylesheet")
cssLink.Attributes.Add("type", "text/css")
Header.Controls.Add(cssLink)
End Sub
We can also add markup inside the head tag using an
HtmlGenericControl, which provides TagName, InnerText, InnerHtml, and
Attributes properties.
Header Place Holders
There is another approach we can use to modify the header, which does
have one drawback. The ContentPlaceHolder and Content controls will
merge even when we place a ContentPlaceHolder control outside of the
<form> tag. Take the following master page excerpt as an example.
<head runat="server">
<title>Untitled Page</title>
<asp:ContentPlaceHolder id="headerPlaceHolder" runat="server" />
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
</asp:ContentPlaceHolder>
</div>
<asp:Label runat="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:Content ID="HeaderContent" runat="server"
ContentPlaceHolderID="headerPlaceHolder">
<link rel="stylesheet" type="text/css" href="customstyles.css" />
</asp:Content>
<asp:Content ID="Content1" Runat="Server"
ContentPlaceHolderID="ContentPlaceHolder1" >
<asp:Label ID="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.
A Page Directive Approach
A third approach is possible which provides the same flexibility and
convenience of the Title attribute. For example, what if we wanted to
set the meta keywords of a page in the @ Page directive?
<%@ Page Language="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;
public class BasePage : Page
{
public BasePage()
{
Init += new EventHandler(BasePage_Init);
}
void BasePage_Init(object sender, EventArgs e)
{
if (!String.IsNullOrEmpty(MetaKeywords))
{
HtmlMeta metaTag = new HtmlMeta();
metaTag.Name = "Content";
metaTag.Content = MetaKeywords;
Header.Controls.Add(metaTag);
}
}
private string _metaKeywords;
public string MetaKeywords
{
get { return _metaKeywords; }
set { _metaKeywords = value; }
}
}
FindControl, JavaScript, and Naming Containers
It’s important for us to understand why the following code throws a null reference exception.
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As System.EventArgs)
Page.FindControl("Label1").Visible = False
End Sub
</script>
<asp:Content ID="Content1" Runat="Server"
ContentPlaceHolderID="ContentPlaceHolder1" >
<asp:Label ID="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.
Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As System.EventArgs)
Dim content As ContentPlaceHolder
content = Page.Master.FindControl("ContentPlaceHolder1")
Dim label As Label
label = content.FindControl("Label1")
label.Visible = False
End Sub
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 “http://www.odetocode.com/Articles/116.aspx”.
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’.
<script type="text/javascript">
<!--
Label1.innerHTML = 'Hello, from script!';
// -->
</script>
One 'solution' is to use the correct client side ID.
<script type="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.
Dim script As String = "[Label1ID].innerHTML = 'boo!';"
Dim scriptKey As String = "SayBoo"
Dim addScriptTags As Boolean = True
Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As EventArgs)
script = script.Replace("[Label1ID]", Label1.ClientID)
ClientScript.RegisterStartupScript( _
Me.GetType(), scriptKey, script, addScriptTags _
)
End Sub
Break Some URLs
Once again, let’s think back to the beginning of the article. At
runtime, the master page and the content page are in the same control
hierarchy – the master page is essentially a user control inside the
content page. At design time, however, the master page and content page
are two different entities. In fact, the master page and content page
may live in different directories. During design time, it's easy to put
URLs and relative paths into our master pages, but we have to be careful
when using relative paths. Take the following master page excerpt as an
example:.
<div>
<img src="logo.gif" alt="Company Logo" />
<asp:ContentPlaceHolder ID="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.
<img src="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:Image ImageUrl="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:
<head runat="server">
<title>Untitled Page</title>
<link href="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.
<head runat="server">
<style type="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).
<body background="logo.gif" runat="server">
<!-- the background for the body tag will break -->
<form id="form1" runat="server">
<div id="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=<%= ResolveClientUrl("logo.gif") %> >
When working with image paths in embedded styles, it’s often a good
idea to move the style definition into a .css file. The ASP.NET runtime
will rebase the path it finds inside a link tag, so we won’t have any
problems locating the stylesheet from any webform. Take the following
style definition in a .css file:
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.
Master Pages and Themes
Master pages, being just another control inside a page, do not have a
separate theme applied. Master pages use the theme specified by the
page that is using them. For an introduction to themes and skins in
ASP.NET 2.0, see “http://www.odetocode.com/Articles/423.aspx”.
Here is one question that comes up: how do we specify a control skin
so that the skin only applies to controls on the master page? There is
no direct method to pull this trick off, but ASP.NET themes do have the
concept of skin IDs. There are two types of skins: default skins, and
skins with a SkinID attribute. A default skin will apply to any control
with the same type as the skin, but a skin with a SkinID will only apply
to controls with the same type and SkinID.
As an example, let’s say we want to control a logo graphic in our
application with the theme and skin infrastructure. We can define a skin
for the logo like the following.
<asp:Image ID="Image1" runat="server" ImageUrl="Images/logo.gif" SkinID="logo" />
Notice the skin uses a relative path, so we can have a different logo
graphic underneath each theme we define. ASP.NET will rebase the path
to the gif file. The master page only needs to use the following markup.
<asp:Image ID="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”.
Nesting Master Pages
It’s possible for a page to specify a MasterPageFile that itself
consists only of Content controls. The master page in this scenario
would in turn specify another master page as its master. The master
pages are nested, but carry out the same steps described in the
beginning of the article. The child master page will first copy the
content page’s content into its ContentPlaceHolder controls. Then the
parent master page will copy the nested master page’s content into its
own ContentPlaceHolder controls. In the end, the Page object will still
be the top object in a control hierarchy that renders as HTML.
Although nested master pages work at runtime, they do not work in the
Visual Studio 2005 designer. If we try to open a content page in design
view and the content page uses a nested master page design, the
designer will display an error message.
Design view does not support creating or editing nested master pages. To create or edit nested master pages, use Source view.
There is a trick to working around this problem.
Let’s suppose we have our top master page (Master1.master) defined as follows.
<%@ Master Language="VB" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:ContentPlaceHolder ID="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.
<%@ Master Language="VB" MasterPageFile="~/Master1.master" %>
<asp:Content runat="server" ID="Content1"
ContentPlaceHolderID="ContentPlaceHolder1">
<h3>Nested Content</h3>
<asp:contentplaceholder id="NestedContent" runat="server">
</asp:contentplaceholder>
</asp:Content>
Finally, a content page which uses Nested.master as its MasterPageFile.
<%@ Page Language="VB" MasterPageFile="~/Nested.master" %>
<asp:Content ID="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:
<%@ Page Language="VB" MasterPageFile="" %>
<asp:Content ID="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;
public class BaseContentPage : Page
{
protected override void OnPreInit(EventArgs e)
{
base.OnPreInit(e);
PagesSection pagesConfig =
ConfigurationManager.GetSection("system.web/pages")
as PagesSection;
MasterPageFile = pagesConfig.MasterPageFile;
}
}
Sharing Master Pages
Many people want to create a single master page, or set of master
pages to use across multiple applications. Unfortunately, there is no
built-in capability to share master pages, and this article will only
provide some advice. The ultimate goal is the ability to modify a master
page once, and have the changes reflected in multiple applications with
the least effort.
The first alternative is to copy shared master page files into a
single location on an IIS web server. Each application can then create a
virtual directory as a subdirectory and point the virtual directory to
the real directory of master pages. The applications can then set the
MasterPageFile property of a page to the name of the virtual directory,
plus the name of the master page file. When we drop an updated master
page file into the real directory, the new master page will appear in
all the applications immediately.
A second approach is to use a version control system to share a set
of master page files across multiple projects. Most source control /
version control systems support some level of “share” functionality,
where a file or folder can appear in more than one project. When a
developer checks in an updated master page file, the other projects will
see the change immediately (although this behavior is generally
configurable). In production and test, each application would need to be
redeployed for the update master page to appear.
Finally, the VirtualPathProvider in ASP.NET 2.0 can serve files that
do not exist on the file system. With the VirtualPathProvider, a set of
master pages could live in database tables that all applications use.
For an excellent article on the VirutalPathProvider, see “http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaspp/html/vpp_vga.asp”.