MasterPages are a great addition to the ASP.NET 2.0 feature set, but are not without their quirks. This article will highlight the common problems developers face with master pages, and provide tips and tricks to use master pages to their fullest potential.
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 "Master Pages In ASP.NET 2.0".
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.
<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.
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…
{
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 this post).
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.
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.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.
<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.
<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.
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.
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.
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:
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.
<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.
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.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: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.
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.
<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.
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 “Design Considerations for Cross page Post Backs in ASP.NET 2.0”. Let’s add search functionality to our site by adding a TextBox and Button to the master page.
<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.
{
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.
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:
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:
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.
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.
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.
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:
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.
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.
<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.
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?
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.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.
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.
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:
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’.
<!--
Label1.innerHTML = 'Hello, from script!';
// -->
</script>
One 'solution' is to use the correct client side ID.
<!--
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 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:.
<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.
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.
The ASP.NET runtime will also rebase paths it finds inside of the head tag. Take the following excerpt from a master page:
<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.
<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).
<!-- 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.
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:
{
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 “Themes in ASP.NET 2.0”.
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.
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.
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.
<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.
<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.
<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:
<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.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 “Virtualizing Access to Content: Serving Your Web Site from a ZIP File”.
Conclusions
The one point we should take away from this article is that we shouldn’t treat master pages as the “masters”, but as just another control inside the page. Many design and runtime problems become easier to solve with this method of thinking. We’ve seen how to handle events, how to handle interactions in both directions, and how to avoid problems with JavaScript and relative URLs. In all of these cases we can treat the master page as a user control inside the page, and have a solid solution.
0 comments:
Post a Comment