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.
<%@ 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
this post).
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: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.
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"
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; 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 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: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 “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’.
<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 “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.
<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; } }
Using the MasterType Directive
Pages that reference controls in a master page, such as a Label
in the header, or Menu on the left or right of a
website template, typically do so by using the Page class's
Master property along with the FindControl()
method, as shown in Figure 1.
Label lbl = this.Master.Page.FindControl("lblHeader") as Label; if (lbl != null) { lbl.Text = "Welcome from the content page!"; }
Figure 1: controls in a master page can be accessed by using the
Master property combined with the FindControl()
method.
While this approach certainly works, any misspellings in the quotes will not be
caught by the compiler, resulting in a runtime error or a null object reference
being returned. Fortunately, a strongly-typed solution is available that doesn't
involve casting the Master property to the base
class of the master page in order to access its members (keep in mind that any server
controls defined in the master page won't be accessible even after a cast is performed,
because they're marked as protected by default).
In cases where a control defined in a master page needs to be exposed to one or
more content pages in a strongly-typed manner, a public property with a
get block can be added into the master page class as shown in Figure
2. The get block returns a
Label control instance named lblHeader.
public Label HeaderLabel { get { return lblHeader; } }
Figure 2: exposing a Label control in a master page
through a public property.
A content page can reference members defined in the custom master page class by
adding the MasterType directive immediately under
the Page directive:
<%@ MasterType VirtualPath="~/Templates/WebsiteMasterPage.master" %>
This causes the ASP.NET compiler to use the custom master page class for the type
of the Page class's Master property as opposed to the default
MasterPage class located in the System.Web.UI namespace. As a result,
the public property defined in the master page can be directly accessed from the
content page in a strongly-typed manner, as shown in Figure 3.
protected void Page_Load(object sender, EventArgs e) { this.Master.HeaderLabel.Text = "Label updated using MasterType " + "directive with VirtualPath attribute."; }
Figure 3: accessing a master page's public property from a content page by using
the MasterType directive.
Using the MasterType property not only results in
less code being written across multiple content pages, but also leads to better
performance and eliminates the need to pass quoted values to
FindControl().
Creating Master Page Base Classes
Developers who need to dynamically change master pages on the fly during the Page's
PreInit event will quickly discover that using the
MasterType, along with the
VirtualPath attribute, will not work. This is because the
VirtualPath value is 'hard coded' into the content page. However,
another solution exists that can be used in situations where multiple master pages
are in play.
In cases where the same public property must be defined in multiple master pages
(such as a Label control in a website header), a
base master page class can be created that derives from MasterPage,
as shown in Figure 4. This class can be added in the App_Code
folder.
public abstract class BaseMasterPage : MasterPage { public abstract Label HeaderLabel { get; } }
Figure 4: creating a custom master page file class with a single public abstract
property.
By defining the BaseMasterPage class as abstract,
it can't be created directly and can only serve as the base for another class. By
defining the HeaderLabel property as abstract, master
pages that derive from BaseMasterPage must provide
an implementation for the property. Figure 5 shows an example of deriving a master
page class from BaseMasterPage and implementing
the abstract HeaderLabel property.
public partial class Templates_InheritedMasterPage : BaseMasterPage
public override Label HeaderLabel
{
get { return
lblHeader; }
}
protected void Page_Load(object
sender, EventArgs e)
{
//Provide default text in case content page doesn't set any
if (String.IsNullOrEmpty(lblHeader.Text))this.lblHeader.Text = DateTime.Now.ToLongDateString();}
}
}
Figure 5: deriving from BaseMasterPage and implementing an abstract property.
Pages that need to access the HeaderLabel property,
but don't want to reference a specific master page using the
MasterType's VirtualPath attribute, can
use the TypeName attribute instead, as shown next:
<%@ MasterType TypeName="BaseMasterPage" %>
The compiler will apply the class defined by the TypeName
attribute to the Page class's Master property allowing
strongly-typed access to the HeaderLabel property
from a content page, as shown earlier in Figure 3. The downside of this approach
is that any custom controls defined in a concrete master page class won't be accessible
through Intellisense™ and will have to be accessed using FindControl().
However, any master page that derives from BaseMasterPage
will expose a HeaderLabel property, allowing master
pages to be dynamically loaded in PreInit and used.
This technique can of course be used in more advanced scenarios where multiple controls
need to be exposed to content pages.
Handling Nested Master Page Design Issues
Master pages can be nested inside of other master pages in cases where an overall
site's layout template needs to contain a child template (see the sample code for
an example of nesting master pages). While nesting master pages is useful in some
situations, it presents a problem when trying to use the Visual Studio .NET 2005
design surface to drag and drop controls onto a content page. This problem is resolved
in the next release of VS.NET (currently called Orcas).
There are a few different ways to get around the nested master page design-time
issue. One potential solution is to temporarily change the
MasterPageFile attribute's value to empty strings on the Page directive.
Although you won't be able to see how the layout template defined in the master
page looks when combined with the content page, you'll be able to drag and drop
controls onto the content page while in design view. However, you'll have to remember
to update the MasterPageFile attribute with the
proper master page file path before moving the page to test or production environments.
Another solution is to leverage a lesser known aspect of the Page directive. Custom
properties defined in an ASP.NET's code-behind class can be referenced in the Page
directive as attributes (I first learned about this trick from Microsoft's Scott
Guthrie). This feature can be used to provide a run-time reference to a master page
and get around the VS.NET nested master page designer issue, as no master page is
defined until the page is actually run.
Figure 6 shows a base class named BasePage that
derives from System.Web.UI.Page and defines a RuntimeMasterPageFile
property. BasePage overrides the Page's
PreInit event and dynamically assigns the MasterPageFile
property to the value contained in the RunTimeMasterPageFile
property.
public class BasePage : System.Web.UI.Page {
private string _RuntimeMasterPageFile;
public string RuntimeMasterPageFile {
get {
return _RuntimeMasterPageFile;
}
set {
_RuntimeMasterPageFile = value;
}
}
protected override void OnPreInit(EventArgs e) {
if (!String.IsNullOrEmpty(RuntimeMasterPageFile)) {
this.MasterPageFile = RuntimeMasterPageFile; }
base.OnPreInit(e);
}
}
Figure 6: creating a base class that derives from Page and defines a single property
named RuntimeMasterPageFile. This property is used
to specify the master page file that should be used at runtime.
A page that derives from BasePage can then assign
the MasterPageFile attribute of the Page directive
to empty strings (to avoid the designer issue mentioned earlier) but then define
the master page file that should be used at runtime by adding a
RuntimeMasterPageFile attribute as shown next:
<%@ Page AutoEventWireup="true" CodeFile="WorkingWithNestedMasterAndBasePage.aspx.cs" CodeFileBaseClass="BasePage" Inherits="WorkingWithNestedMasterAndBasePage" Language="C#" MasterPageFile="" RuntimeMasterPageFile="~/Templates/NestedMasterPage.master" Title="Nested Master Page Demo" %>
Defining the RuntimeMasterPageFile attribute will
cause the associated property in BasePage to be
assigned a value which is then used during PreInit
to assign a value to the MasterPageFile property.
Although every page that references a nested master page has to derive from BasePage for this trick to work, it's one potential
solution to nested master pages that prevents having to temporarily remove the MasterPageFile attribute value to edit a content page
in design view.
Sharing Master Pages across IIS Applications
The MasterPage class available in ASP.NET 2.0 derives
from UserControl and just like user controls, master
pages can't be shared across IIS applications. There are a few different solutions
that have been proposed, such as setting up virtual directories in each IIS application
that point to the same physical folder, but there is a way to share master pages
across applications with a little work on your part without resorting to duplicating
virtual directories across multiple websites. By leveraging the VS.NET 2005 Publish
Web Site tool it's possible to create an assembly that contains all of the master
page HTML code and C# or VB.NET code, give the assembly a strong name, and install
it into the Global Assembly Cache (GAC).
I consider this trick more of a hack, but it's something you can try out if/when
the situation requires it. There are several steps involved, so a step-by-step approach
follows, as well as issues to watch out for when performing the steps. I originally
wrote about this some time ago on my blog at
http://weblogs.asp.net/dwahlin.
1. Create an empty Website in VS.NET 2005. Delete everything in it including App_Data,
Default.aspx, and web.config (if it exists).
2. Add a master page into the website. A simple master page file is shown in Figure
7.
<%@ Master Language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat="server"> <div> Header <br /> <asp:contentplaceholder id="ContentPlaceHolder1" runat="server"> </asp:contentplaceholder> <br /> Footer </div> </form> </body> </html>
Figure 7: a simple master page file that defines a ContentPlaceHolder
control.
3. Select Build | Publish Website from the VS.NET menu.
4. On the screen that follows, select a target location, and check all of the checkboxes
shown in Figure 8. Note that the image shown in Figure 8 references a strong name
key file named keyfile.snk that was created using
the sn.exe command-line tool that ships with .NET. This is required in order to
install assemblies into the GAC. The following syntax can be used to create the
key file (run it using the Visual Studio .NET 2005 command prompt):
sn.exe -k keyfile.snk.

Figure 8: using the Publish Web Site tool to create an assembly from a master page.
5. After the publish operation completes, open the new website in VS.NET 2005 (named
MasterDemo in the example above). You should see
a new assembly (with a strange name) in the Bin
folder. This assembly is your master page in compiled form.
6. Install the assembly into the GAC using gacutil.exe or drag-and-drop it into
c:\Windows\Assembly using Windows Explorer. Once
you've done this, delete the original assembly as well as the newly created XML
files associated with it from the website.
7. Add a web.config file into the website and add the following within the <system.web> begin and end tags.
<compilation debug="true"> <assemblies> <add assembly="App_Web_masterpagebase.master.cdcab7d2, Version=0.0.0.0, Culture=neutral, PublicKeyToken=cceb8435cfc68486" /> </assemblies> </compilation>
You'll need to change the name of the assembly to the name that is generated for
your project (the one you added into the GAC) and change the
PublicKeyToken to the one you see in the GAC. Note that the assembly
attribute value shouldn't wrap at all. I didn't give my base master page a version
for simplicity, but you can do so by applying the [assembly: AssemblyVersion("1.0.0.0")]
attribute to the master page code-behind class. Note that if you're using the Web
Application Project feature of VS.NET 2005 you can give your master page assembly
a friendlier name. I'll leave that as an exercise for the reader.
8. Add a master page into the website, but don't create a code-behind page for it(you
can, but it's not needed in this case since the master page is only used to reference
the one installed in the GAC).
9. Remove all code within the new master page and add the following at the top.
It should be the only code in the page.
<%@ Master Language="C#" Inherits="ASP.masterpagebase_master" %>
If you named the original master page (the one created in step 2) differently, then
you'll need to change the Inherits value. Use the
VS.NET object browser to see the name of the class within the .dll generated in
step 4.
10. Create a content page that references MasterPage.master
(the one you created in the previous step). The default ContentPlaceHolderID
is ContentPlaceHolder1, so use that in the <asp:Content> tag unless you gave the id a different
name in step 2.
After completing these steps, any IIS application can share the same master page
used by other IIS applications by placing the empty MasterPage.master
file into the application and updating the web.config file to point to the master
page assembly in the GAC. The downfall of this approach is that you have to recompile
the base master page and put it back into the GAC each time you need to make a change
and the design time support is lacking. There may be other issues as well that haven't
been discovered yet, so perform proper testing before assuming this technique will
work for your particular situation.
In working with more complex master pages you may see a 'could not find string resource'
error come back when using this approach. If you use Reflector (http://www.aisto.com/roeder/dotnet)
to analyze the code generated when the master page is compiled into the assembly,
you'll likely see a call to a method named CreateResourceBasedLiteralControl()
in the code rather than seeing the actual HTML from the master page being embedded
into the assembly. If you strip out some of the whitespace in the HTML it should
eliminate the call to CreateResourceBasedLiteralControl()
that the Publish Web Site tool added and compile correctly. For example, change
the following:
<td align="left" valign="top" height="45"> <www:HtmlOutput ID="egovHeader" runat="Server" XmlSource="/XML/SiteLinks.xml" XsltSource="/XSLT/Header.xslt" LanguageCookieName="EgovCookie/Language" /> </td>
To the following (all in one line):
<td align="left" valign="top" height="45"><www:HtmlOutput ID="egovHeader" runat="Server" XmlSource="/XML/SiteLinks.xml" XsltSource="/XSLT/Header.xslt" LanguageCookieName="EgovCookie/Language" /></td>
If you try this technique and continue to get a string resource error, you'll probably
have to play around with your HTML in the base master page until no
CreateResourceBasedLiteralControl() calls are made in the generated code
How Master Pages Work
Master pages actually consist of two pieces, the master page itself
and one or more content pages.
Master Pages
A master page is an ASP.NET file with the extension .master (for example, MySite.master)
with a predefined layout that can include static text, HTML elements, and server
controls. The master page is identified by a special
@ Master directive that replaces the
@ Page directive that is used for ordinary .aspx pages. The directive
looks like the following.
<%@ Master Language="VB" %>
<%@ Master Language="C#" %>
The @ Master directive can contain most of the same directives that a
@ Control
[ http://msdn2.microsoft.com/en-us/library/d19c0t4b(VS.80,printer).aspx ]
directive can contain. For example, the following master-page directive includes
the name of a code-behind file, and assigns a class name to the master page.
<%@ Master Language="VB" CodeFile="MasterPage.master.vb" Inherits="MasterPage" %>
C#
Copy Code
<%@ Master Language="C#" CodeFile="MasterPage.master.cs" Inherits="MasterPage" %>
In addition to the @ Master directive, the master page also contains all
of the top-level HTML elements for a page, such as html, head, and
form. For example, on a master page you might use an HTML table for the layout,
an img element for your company logo, static text for the copyright notice,
and server controls to create standard navigation for your site. You can use any
HTML and any ASP.NET elements as part of your master page.
Replaceable Content Placeholders
In addition to static text and controls that will appear on all pages, the master
page also includes one or more
ContentPlaceHolder
controls. These placeholder controls define regions where replaceable content will
appear. In turn, the replaceable content is defined in content pages. After you
have defined the ContentPlaceHolder controls, a master page might look like
the following.
<% @ Master Language="VB" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML
1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html >
<head runat="server" >
<title>Master page title</title>
</head>
<body>
<form id="form1" runat="server">
<table>
<tr>
<td><asp:contentplaceholder id="Main" runat="server" /></td>
<td><asp:contentplaceholder id="Footer" runat="server" /></td>
</tr>
</table>
</form>
</body>
</html>
C#
Copy Code
<%@ Master Language="C#" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML
1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html >
<head runat="server" >
<title>Master page title</title>
</head>
<body>
<form id="form1" runat="server">
<table>
<tr>
<td><asp:contentplaceholder id="Main" runat="server" /></td>
<td><asp:contentplaceholder id="Footer" runat="server" /></td>
</tr>
</table>
</form>
</body>
</html>
Content Pages
You define the content for the master page's placeholder controls by creating individual
content pages, which are ASP.NET pages (.aspx files and, optionally, code-behind
files) that are bound to a specific master page. The binding is established in the
content page's @ Page directive by including a
MasterPageFileattribute that points to the master page to be used. For example, a content page
might have the following @ Page directive, which binds it to the Master1.master
page.
Visual Basic
Copy Code
<%@ Page Language="VB" MasterPageFile="~/MasterPages/Master1.master" Title="Content
Page" %>
C#
Copy Code
<%@ Page Language="C#" MasterPageFile="~/MasterPages/Master1.master" Title="Content
Page"%>
In the content page, you create the content by adding
Content
controls and mapping them to ContentPlaceHolder controls on
the master page. For example, the master page might have content placeholders called
Main and Footer. In the content page, you can create two Content
controls, one that is mapped to the ContentPlaceHolder control Main
and the other mapped to the ContentPlaceHolder control Footer, as
shown in the following figure.
Replacing placeholder content
After creating Content controls, you add text and controls to them. In a
content page, anything that is not inside the Content controls (except script
blocks for server code) results in an error. You can perform any tasks in a content
page that you do in an ASP.NET page. For example, you can generate content for a
Content control using server controls and database queries or other dynamic
mechanisms.
A content page might look like the following.
<% @ Page Language="VB" MasterPageFile="~/Master.master" Title="Content
Page 1" %>
<asp:Content ID="Content1" ContentPlaceHolderID="Main" Runat="Server">
Main content.
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="Footer" Runat="Server" >
Footer content.
</asp:content>
<% @ Page Language="C#" MasterPageFile="~/Master.master" Title="Content Page 1" %>
<asp:Content ID="Content1" ContentPlaceHolderID="Main" Runat="Server">
Main content.
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="Footer" Runat="Server" >
Footer content.
</asp:content>
The @ Page directive binds the content page to a specific master page, and
it defines a title for the page that will be merged into the master page. Note that
the content page contains no other markup outside of the Content controls.
(The master page must contain a head element with the attribute runat="server"
so that the title setting can be merged at run time.)
You can create multiple master pages to define different layouts for different parts
of your site, and a different set of content pages for each master page.
Advantages of Master Pages
Master pages provide functionality that developers have traditionally
created by copying existing code, text, and control elements repeatedly; using framesets;
using include files for common elements; using ASP.NET user controls; and so on.
Advantages of master pages include the following:
- They allow you to centralize the common functionality of your pages so that you
can make updates in just one place.
- They make it easy to create one set of controls and code and apply the results to
a set of pages. For example, you can use controls on the master page to create a
menu that applies to all pages.
- They give you fine-grained control over the layout of the final page by allowing
you to control how the placeholder controls are rendered.
- They provide an object model that allows you to customize the master page from individual
content pages.
Run-time Behavior of Master Pages
At run time, master pages are handled in the following sequence:
- Users request a page by typing the URL of the content page.
- When the page is fetched, the @ Page directive is read. If the directive
references a master page, the master page is read as well. If this is the first
time the pages have been requested, both pages are compiled.
- The master page with the updated content is merged into the control tree of the
content page.
- The content of individual Content controls is merged into the corresponding
ContentPlaceHolder control in the master page.
- The resulting merged page is rendered to the browser.
The process is illustrated in the following diagram.
Master pages at run time
From the user's perspective, the combined master and content pages are a single,
discrete page. The URL of the page is that of the content page.
From a programming perspective, the two pages act as separate containers for their
respective controls. The content page acts as a container for the master page. However,
you can reference public master-page members from code in the content page, as described
in the next section.
Note that the master page becomes a part of the content page. In effect, the master
page acts in much the same way a user control acts — as a child of the content page
and as a container within that page. In this case, however, the master page is the
container for all of the server controls that are rendered to the browser. The control
tree for a merged master and content page looks something like this:
Page
Master Page
(Master page markup and controls)
ContentPlaceHolder
Content page markup and server controls
(Master page markup and controls)
ContentPlaceHolder
Content page markup and server controls
(Master page markup and controls)
This diagram is simplified; if the content page does not have corresponding Content
controls, the master page might also have markup and controls in the
ContentPlaceholder
controls.
In general, this structure has no effect on how you construct your pages or program
them. However, in some cases, if you set a page-wide property on the master page,
it can affect the behavior of the content page, because the master page is the closest
parent for the controls on the page. For example, if you set the
EnableViewState
property on the content page to true but set the same property to false
in the master page, view state will effectively be disabled because the setting
on the master page will take priority.
Master Page and Content Page Paths
When a content page is requested, its content is merged with the master
page, and the page runs in the context of the content page. For example, if you
get the
CurrentExecutionFilePathproperty of the
HttpRequest
object, whether in content page code or in master page code, the path represents
the location of the content page.
The master page and content page do not have to be in the same folder. As long as
the MasterPageFile attribute in the content page's @ Page directive
resolves to a .master page, ASP.NET can merge the content and master pages into
a single rendered page.
Referencing External Resources
Both the content page and master page can contain controls and elements that reference
external resources. For example, both might contain image controls that reference
image files, or they might contain anchors that reference other pages.
The context for the merged content and master pages is that of the content page.
This can affect how you specify URLs for resources, such as image files and target
pages, in anchors.
Server Controls
In server controls on master pages, ASP.NET dynamically modifies the URLs of properties
that reference external resources. For example, you might put an
Image
control on a master page and set its
ImageUrl property to be relative to the master page. At run time, ASP.NET will
modify the URL so that it resolves correctly in the context of the content page.
ASP.NET can modify URLs in the following cases:
- The URL is a property of an ASP.NET server control.
- The property is marked internally in the control as being a URL. (The property is
marked with the attribute
UrlPropertyAttribute
In practical terms, ASP.NET server control properties that are commonly used
to reference external resources are marked in this way.
Other Elements
ASP.NET cannot modify URLs on elements that are not server controls. For example,
if you use an img element on a master page and set its src attribute
to a URL, ASP.NET will not modify the URL. In that case, the URL will be resolved
in the context of the content page and create the URL accordingly.
In general, when working with elements on master pages, it is recommended that you
use a server control, even for elements that do not require server code. For example,
instead of using an img element, use an Image server control. That
way, ASP.NET can resolve URLs correctly and you can avoid maintenance issues that
might arise if you move the master or content page.
Master Pages and Themes
You cannot directly apply an ASP.NET theme to a master page. If you
add a theme attribute to the @ Master directive, the page will raise an error
when it runs.
However, themes are applied to master pages under these circumstances:
- If a theme is defined in the content page. Master pages are resolved in the context
of content pages, so the content page's theme is applied to the master page as well.
- If the site as a whole is configured to use a theme by including a theme definition
in the
pages Element (ASP.NET Settings
Schema) element.
Scoping Master Pages
You can attach content pages to a master page at three levels:
- At the page level You can use a page directive in each content page
to bind it to a master page, as in the following code example.
<%@ Page Language="VB" MasterPageFile="MySite.Master" %>
<%@ Page Language="C#" MasterPageFile="MySite.Master" %>
- At the application level By making a setting in the pages element
of the application's configuration file (Web.config), you can specify that all ASP.NET
pages (.aspx files) in the application automatically bind to a master page. The
element might look like the following.
Copy Code
<pages masterPageFile="MySite.Master" />
If you use this strategy, all ASP.NET pages in the application that have Content
controls are merged with the specified master page. (If an ASP.NET page does not
contain Content controls, the master page is not applied.)
- At the folder level This strategy is like binding at the application
level, except that you make the setting in a Web.config file in one folder only.
The master-page bindings then apply to the ASP.NET pages in that folder.
Setting Page Title, Description Meta Tags Dynamically Using XML file.
<!-- Add the following code in head section of master page -->
<!-- Remember to set runat=server property of head control in master page-->
<title id="metaTitle" runat="server"></title>
<meta name="metaDesc" content="description" id="metaDesc" runat="server" />
<meta name="metaKeyword" content="keys" id="metaKeyword" runat="server" />
Sample XML File
<?xml version="1.0" standalone="yes"?>
<Pages>
<Page>
<!-- your page title -->
<Title>your page title comes here</Title>
<!-- your .aspx file name for example -->
<PageName>default.aspx</PageName>
<!-- your page description -->
<description>Page Description</description>
<keywords>page keywords </keywords>
<COPYRIGHT>BitWise</COPYRIGHT>
<GENERATOR>BitWise Offshore Generator</GENERATOR>
<AUTHOR>Pushkar Adsule</AUTHOR>
</Page>
</Pages>
SetMetaContent() function to set Title, Description and Keywords
Dynamically. Call SetMetaContent() in
Page_Load of Master Page.
public void SetMetaContent()
{
string pageDefault = ConfigurationManager.AppSettings["defaultPage"].ToString();
XmlDocument doc1;
if (Cache["PageXml"] == null)
{
doc1 = new XmlDocument();
doc1.Load(Server.MapPath(@"App_Data\Page.xml"));
//xml document added to cache and set its cache dependency to file dependency
// note: Keep Xml File in App_Data folder for Security reasons.
Cache.Insert("PageXml", doc1, new System.Web.Caching.CacheDependency
(Server.MapPath(@"App_Data\Page.xml")));
}
else
{
doc1 = (XmlDocument)Cache["PageXml"];
}
XPathNavigator xpathNav = doc1.CreateNavigator();
//Compile the XPath expression
XPathExpression xpathExpr = xpathNav.Compile("/Pages/Page[PageName='" +
Request.Path.ToString().Replace("/", "").Trim().ToLower() + "']");
XPathNodeIterator nodeIter = xpathNav.Select(xpathExpr);
// if page information not found in xml file use default page title, keyword and description
if (nodeIter.Count == 0)
{
xpathExpr = xpathNav.Compile("/Pages/Page[PageName='" + pageDefault.Trim().ToLower() + "']");
nodeIter = xpathNav.Select(xpathExpr);
}
while (nodeIter.MoveNext())
{
xpathNav = nodeIter.Current;
if (xpathNav.MoveToFirstChild())
{
do
{
switch (xpathNav.Name.ToUpper())
{
case "TITLE":
metaTitle.Text = HttpUtility.HtmlEncode(xpathNav.Value);
break;
case "DESCRIPTION":
metaDesc.Content = HttpUtility.HtmlEncode(xpathNav.Value);
break;
case "KEYWORDS":
metaKeyword.Content = HttpUtility.HtmlEncode(xpathNav.Value);
break;
}
}
while ((xpathNav.MoveToNext()));
}
}
}
|