ASP.NET Master Pages Tips and Tricks

14 June 2007
by Dan Wahlin

With the release of ASP.NET 2.0, developers were given a simple and effective way to apply a consistent layout across multiple pages in a website. By creating a file with a .master extension that defined a website's overall layout template and referencing it with the Page directive's MasterPageFile attribute, website development and maintenance took a step forward in the direction of greater productivity.

Master pages have been around for over one and a half years, so I won't cover the fundamentals of creating and using them, as many tutorials and books have already been written about the topic. Instead, I'll focus on a few tips and tricks that can be applied when using master pages. To start, let's examine how the MasterType directive can be used to reference master page controls in a strongly-typed manner from a content page.

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

  4. Select Build | Publish Website from the VS.NET menu.
  5.  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.
  6. Figure 8: using the Publish Web Site tool to create an assembly from a master page.

  7. 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.
  8. 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.
  9. Add a web.config file into the website and add the following within the <system.web> begin and end tags.
  10. <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.

  11. 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).
  12. Remove all code within the new master page and add the following at the top. It should be the only code in the page.
  13. <%@ 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.

  14. 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. As mentioned before, I would advise using Reflector to take a look.

Conclusion

Master pages provide a great way to apply a consistent layout to an ASP.NET 2.0 website, resulting in better productivity and reduced maintenance. In this article you've seen various tips and tricks that can be used with master pages such as accessing master page controls in a strongly-typed manner using the MasterType directive, creating base master page classes, and working with nested master pages. You've also seen one potential technique for sharing master pages across IIS applications. Additional samples are available with this article's downloadable code.


© Simple-Talk.com