圓梦

激情燃燒的歲月
  博客园  :: 首页  :: 新随笔  :: 订阅 订阅  :: 管理

ASP.Net 2.0 - Master Pages: Tips, Tricks, and Traps

Posted on 2007-04-27 12:36  szw104  阅读(795)  评论(0编辑  收藏  举报
ASP.Net 2.0 - Master Pages: Tips, Tricks, and Traps

Posted by on 2006年4月11日

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.

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

MasterPage and Page objects before merge

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.

After merge, before content

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.

The final product

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:Label runat="server" ID="FooterLabel"
          
Text="Default footer text"
/>

</
form>

The catch is, some content pages need to override the default footer text. Here is one approach we can use from page’s Page_Load event handler.

 

Protected Sub Page_Load(ByVal sender As Object, _
                        
ByVal e As EventArgs)

  
Dim footer As Label = Master.FindControl("FooterLabel")
  
If Not footer Is Nothing Then
    footer.Text = "Custom footer text!!"
  End If
  
End Sub

Use the above approach with extreme caution. FindControl is fragile, and will return null if someone renames FooterLabel, or removes the control entirely. This problem can't be discovered until runtime. FindControl also has some additional difficulties when INamingContainers are involved - we will discuss this topic later.

A better approach is to establish a formal relationship between the master page and content page, and take advantage of strong typing. Instead of the content page poking around inside the master page, let’s have the master page expose the footer text as a property. We can add the following code to our master page.

 

Public Property FooterText() As String
  Get
    Return FooterLabel.Text
  
End Get
  Set(ByVal value As String)
    FooterLabel.Text = value
  
End Set
End
Property

The best way to use this property is to place a @ MasterType directive in our content page. When the ASP.NET compiler sees the @ MasterType directive, it creates a strongly typed Master property in our Page derived class.

 

<%@ Page Language="VB" MasterPageFile="~/Master1.master"
        
AutoEventWireup="true"  %>
<%
@ MasterType VirtualPath="~/Master1.master" %>

<script runat="server">
  
  
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)

    Master.FooterText =
"Custom footer text"
    
  
End Sub
  
</script>

This code is a cleaner and doesn’t depend on the magic string “FooterLabel”. If anyone ever removes the control from the master page, or renames the control, we will have compilation errors instead of runtime problems.

What if we have 2 different master pages in the application? In this scenario, we have a problem, because the VirtualPath attribute supports only a single master page. We’ve tightly coupled our page to a specific master. If we assign a MasterPageFile that does not match the MasterType, the runtime will throw an exception.

Unable to cast object of type 'ASP.master2_master' to type 'ASP.master1_master'.

Fortunately, the @ MasterType directive doesn’t require us to use a VirtualPath, we can also specify a type name. Once again we will turn to inheritance to solve this problem. If all the content pages expect their master pages to have footer text, then let’s define a base class for the master pages to inherit.

We can take one of two approaches with the base class. One approach is to use an abstract (MustInherit) base class:

 

using System.Web.UI;

public abstract class BaseMasterPage : MasterPage
{
    
public abstract string FooterText
    {
        
get;
        
set;
    }
}

Our master pages must inherit from this base class and override the FooterText property.

 

<%@ Master Language="VB" Inherits="BaseMasterPage" %>

<script runat="server">

  Public Overrides Property FooterText() As String
    Get
      Return FooterLabel.Text
    
End Get
    Set(ByVal value As String)
      FooterLabel.Text = value
    
End Set
  End Property

</
script>

Now our page can use any master page that inherits from BaseMasterPage. All we need is an @ MasterType directive set to the base class. Instead of using a VirtualPath attribute, we use a TypeName attribute and specify the name of the base class.

 

<%@ Page Language="VB" MasterPageFile="~/Master1.master"
        
AutoEventWireup="true"  %>
<%
@ MasterType TypeName="BaseMasterPage" %>

<script runat="server">
  
  
Protected Sub Page_Load(ByVal sender As Object, _
                          
ByVal e As EventArgs)

    Master.FooterText =
"Use the base class..."
    
  
End Sub
  
</script>

The second approach is to use a concrete base class. This approach is possible only if we are sure every master page will have a label with an ID of “FooterLabel”.

 

using System.Web.UI;
using System.Web.UI.WebControls;

public class BaseMasterPage : MasterPage
{
    
protected Label FooterLabel;
    
public string FooterText
    {
        
get
        {
            
return FooterLabel.Text;
        }
        
set
        {
            FooterLabel.Text =
value;
        }
    }
}

With the above approach we can remove code from our master page – we don’t need to define the FooterText property. If we are using code-beside files instead of inline script, we need to use CodeFileBaseClass=”BaseMasterPage” in the @ Master directive to ensure ASP.NET can wire up the base class’s Label field with the Label control.

Master Page To Content Page Interaction

Here is a case where the master part of the master page name can be misleading. The master page sounds like a good place to put logic and code that will tell the page how to do something. After all, a master page is the master, right? We now know that the master page is just another child control. Ideally, the master page will remain passive. Instead of telling it’s parent page what to do, the master page should tell a page when something interesting happenes, and let the page decide what to do.

Let’s pretend every page in our application displays a report, and every page needs a button for users to click and email the report. Putting a Button and a TextBox inside the master page seems like a reasonable choice.

 

<asp:TextBox runat="server" id="EmailAddressBox" />
<
asp:Button runat="server" ID="SendEmailButton"
            
OnClick="SendEmailButton_Click" />

What happens when the user clicks the button? We can choose from the following options:

  • Handle the Click event in the master page, and have the master page email the report.
  • Expose the Button and TextBox as public properties of the master page, and let the content page subscribe to the click event (and email the report).
  • Define a custom SendEmail event, and let each page subscribe to the event.

The first approach can be ugly because the master page will need to call methods and properties on the page. Master pages are about layout, we don’t want to clutter them with knowledge of reports and specific pages.

The second approach is workable, but it tightly couples the page to the master. We might change the UI one day and use a DropDownList and a Menu control instead of a TextBox and Button, in which case we’ll end up changing all of our pages.

The third approach decouples the master page and content page nicely. The page won’t need to know what controls are on the master page, and the master page doesn’t have to know anything about reports, or the content page itself. We could start by defining the event in a class library, or in a class file in App_Code.

 

using System;

public class SendEmailEventArgs : EventArgs
{
    
public SendEmailEventArgs(string toAddress)
    {
        _toAddress = toAddress;
    }

    
private string _toAddress;
    
public string ToAddress
    {
        
get { return _toAddress; }
        
set { _toAddress = value; }
    }
   
}

public delegate void SendEmailEventHandler(
        
object sender, SendEmailEventArgs e);

We can raise this event from a master page base class (if we have one), or from the master page itself. In this example, we will raise the event directly from the master page.

 

<%@ Master Language="VB" %>

<script runat="server">

  Public Event SendEmail As SendEmailEventHandler
  
  
Protected Sub SendEmailButton_Click(ByVal sender As Object, _
                                      
ByVal e As System.EventArgs)
    
    
Dim eventArgs As New SendEmailEventArgs(EmailAddressBox.Text)
    
RaiseEvent SendEmail(Me, eventArgs)
    
  
End Sub
  
</script>

 

We'll need to add some validation logic to the master page, but at this point all we need is to handle the event in our page. We could also handle the event from a base page class, if we don’t want to duplicate this code for every page.

 

<%@ Page Language="VB" MasterPageFile="~/Master1.master"
        
AutoEventWireup="true"  %>
<%
@ MasterType VirtualPath="~/Master1.master" %>

<script runat="server">  

  
Protected Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs)
    
AddHandler Master.SendEmail, AddressOf EmailReport
  
End Sub
  
  
Protected Sub EmailReport(ByVal sender As Object, ByVal e As SendEmailEventArgs)
    
    
Dim address As String = e.ToAddress
    
    
' do work
    
  
End Sub
  
</script>

Master Pages and Cross Page Postbacks

Another common scenario for master pages is to use a cross page post back. This is when a control on the master page POSTs to a second web form. For more information on cross page post backs, see “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: