Wednesday, September 24, 2008

.NET 3.5 Master Pages

Introduction:
This article will look at Master Pages, a built in feature of ASP.Net that was introduced in .NET 2.0 and has been enhanced in the current 3.5 version. This article will not cover every aspect of Master Pages but instead is intended to be an introduction. We will examine how to create a Master Page, content pages and hook them together. We will also look at how to setup several mechanisms for communication between the pages and finally go over a few pitfalls to avoid. The writer assumes the read has a basic understanding of how to create ASP.Net web applications using Visual Studio 2005 or later.

The Call for Master Pages:
Although Master Pages is a relatively new technology, the need for the concept is as old as web development itself. "How do I easily give my pages a consistent look and feel, not only with colors and fonts but with layout and even some functionality?" In the past, we would accomplish this with server side includes, user controls, and other creative solutions. With the introduction of Master Pages, .NET web developers now have a method for developing web pages with consistent layouts and common functionality that is both easy to code and a snap to maintain.

How Master Pages Work:
The job of a master page is to provide a layout or "shell" for the content of your website. By the master page handling the details of the layout (various screen areas, menus, advertisements, footers), the content page can focus on one thing: content. At the server level, when the content page is invoked, the master page and the content page are merged to form one cohesive page. This page is then delivered to the client without the client ever knowing the difference. As far as the browser is concerned, it is one page.

Getting Started:
To get started with Master Pages, create a web application using Visual Studio. To that web application you will need to add at least one "Master Page" and one "Web Content Form". In your solution explorer, click on "Add > New Item" and you should find each of these file types. Create the Master Page first. Once that is done, when you create the Web Content Form, a dialog will pop up allowing you to select the master page to associate with the new content page. The IDE will automatically do the initial hookup between the two. So what exactly got created?

At the top of the master page, you will notice a new directive:

<%@ Master Language="C#" AutoEventWireup="true"... %>

Instead of a Page directive you now have a Master directive. This will identify the page as a master page. The rest of the page is fairly innocuous. It should look just like any other ASPX file markup - the only difference is that you will have a default ContentPlaceHolder that looks like this:

<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
</asp:ContentPlaceHolder>

This is the spot where the content page will put its information. When you look at the content page, however, it looks quite different. At the top, you will find the normal page directive but now, the only tag allowed in a content page is the <asp:Content> tag. It looks something like this:

<asp:Content ID="SampleContent" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">

Whatever is placed inside of the content tag will become the content on the Master Page. This content tag is where you will place HTML markup, ASPX controls and other text you want to appear on the page. One of the nice abilities of Visual Studio IDE is that you can edit the content in the designer and see the master page at the same time. When editing the content page, you are not allowed to edit the master page but you can still see it in the background, which is a helpful tool during development.

Auto Hookup in the Web.Config:
One way to associate master pages with content pages is through the Page directive as mentioned above, which links a particular content page with a particular master page. If your site uses several different master pages, this is a good way to specify which master page each content page uses. However, if your site has only one primary master page, you can declare a global master page that all undeclared content pages use. You can easily set this up in the Web.Config file by setting the masterPageFile attribute of the pages element as shown below. (Note: There may be much more code in the Web.Config file but only the relevant sections and attributes are shown.)

<configuration>
<system.web>
<pages masterPageFile="~\Global.master">
</pages>
</system.web>
</configuration>

By setting the masterPageFile attribute of the pages element in web.config, any content page that is not specifically tied to a master page will use the master page specified here.

Communication between Master Page and Content Page:
Now that you have the basic look and feel of the master page and content page defined, you will need to add functionality. Since the master page is part of every content page, you may want to localize some functionality in the master page that it can be accessed by all the content pages. Also, you may want to change the appearance or context of the master page, depending on the particular content page that is being loaded. Let’s take a look at a couple of examples.

Suppose you have a menu control on the master page. This is a common control to place on a master page since you typically want the menu to be available to all content pages. However, one of your requirements is to change one of the menu items depending on which page is being loaded. You can easily access the menu control from the content page by using the FindControl method of the content page’s master page. Within the content page, you can add the following code to the Page_Load method:

//Alter the Menu
Menu mnu = (Menu)(Master.FindControl("Menu1"));
if (mnu != null)
{
mnu.Items[0].Text = "Custom Menu";
}

In this example, Menu1 is the name of the control on the master page. The FindControl method will locate the control and return a menu object. We then cast the object to a Menu and check to see that we have received a valid Menu reference (e.g. mnu is not null). With a valid reference in hand, we can then access the methods of the menu object and modify it just as if we were in the master page. There are some drawbacks to this method. Primarily, if the menu name ever gets changed, this will not be identified by the compiler which can cause a maintenance problem.

There is a second method for accessing information in the master page. We can early bind attributes so that any anomalies will be caught during compile time. Simply expose whatever information you want exposed to content pages in the master page class. For example, you may have a drop down list and you want it to be stored on the master page but you want the current selection to be made available for consumption in the content page. The best way to handle this is to expose the current selection as a property of the master page. In the master page, create the following property.

public string Selection
{
get
{
return cboTest.SelectedValue.ToString();
}
}

Note that the return type of the property is not a DropDownList object, but a string object. This is because we are not interested in exposing the control as much as we are interested in the control’s selected item. This allows us to abstract the call and if we decide later to use a third party control or a different method for selecting the data, you will not have to change all of the content pages that consume this data. The next step is to access this data from the content page. Before you can do this, there is one change that you will need to make to the content page. In order for the content page to be able to recognize the properties added to the master page, we need to tell the content page which class to use for this. Use the @MasterType directive to set the strong type for the content page’s master page, as accessed through the Master property. In the markup for the content page, add the following tag:

<%@ MasterType VirtualPath="~/Example.master" %>

Substitute the actual master page file name where it says Example.master. With that in place, we can access the above defined property with the following code in the content page.

Label1.Text = Master.Selection;

This will access the Selection property of the Example master class and assign the results to a label on the content page. Above, you saw the example of how to declare a global master page. In order to strongly type the master page to the content page, you must include the @MasterType directive in each content page. Some have used inheritance to get around this which Microsoft has confirmed and is considering a change for future versions of .NET.

Relative Paths:
One of the confusing parts of master pages is how it handles relative paths. If a content page and its associated master page are in the same directory, then there aren’t going to be any issues related to relative paths. However, if the two are in different directories, the resultant page is going to default to the content page’s location to resolve any relative path links. For example, let’s say you have a master page in the root directory and a content page in a subdirectory called \Content. Also, both of these pages reference a graphic called HorizontalLine.gif. This graphic is in a directory called \Images. The content page might refer to this resource as...

<img src="../Images/HorizontalLine.gif"/>

The Master page, might refer to this as ...

<img src="Images/HorizontalLine.gif"/>

However, once the master page and content page are merged, the references in the master page will fail due to the page loading in the context of the content page’s current location. There are several ways to fix this.

1. Reference as many resources as you can from a CSS style sheet. All references in the style sheet are based on the style sheet’s current location.

2. Create references with server controls rather than plain HTML. You can optionally use the tilde (~) character as the start of the path. If the resource is processed on the server side, the references are automatically resolved. See note below.

3. Design your directory structure where the relative paths are not an issue.

Note: If using .NET 3.5, you do not need the tilde character. In this version of the .NET framework, relative paths will resolve themselves as long as the resource is from a server control. To take a quick look at this, suppose we have the following directory structure:

In the MasterPages directory, we decide to store all of our master pages. Within those master pages, we use images that are stored in the subdirectory MasterPageImages. Now suppose we have two content pages, one stored in the MasterPages directory itself and one stored in the Main directory. The Master Page has a reference to an image that looks like this:

<asp:Image runat="server" ImageUrl="MasterPageImages/LoginBackground.jpg" />

Note that the control does not include the tilde. Also note that it is relative to the master page itself. If we invoke a content page that is in the same directory as the Master page itself, the resultant image tag will look like this:

<img src="MasterPageImages/LoginBackground.jpg" style="border-width:0px;" />

The location is relative to both the master page and the content page. Now, if you invoke the content page that is in the Main directory, this is what the image tag looks like:

<img src="MasterPages/MasterPageImages/LoginBackground.jpg" style="border-width:0px;" />

Note that even though the Image control is in the master, the path automatically adjusts relative to the content page. This is handled by the server. By not having to rely on the tilde character to identify the root, you can contain the master pages and their resources anywhere and even move them to another project without having to adjust paths.

Multiple Content Sections:
A master page is not limited to a single content section. You can have as many content sections as you wish. The content pages have the option of specifying a content section for each entry in the master page. However, if a content page specifies a content section, that section must exist in the master page or an error will occur.

One common use for this would be to place Meta tags on your page. Another use would be to include scripts on the master page that the content page might need. Remember, if you declare a contentPlaceHolder on the master, you can opt to fill it in from the content page or not. If you remember, the master page contains the actual <HEAD> tag. This section is where the Meta tags would be placed. You can have a section for normal content, and you can have an additional section for the Meta Tags. In your master page, declare the following <HEAD> section.

<head id="Head1" runat="server">
<title>Example Master</title>
<link href="~/application_styles.css" rel="stylesheet" type="text/css" />
<asp:ContentPlaceHolder ID="MasterMeta" runat="server">
</asp:ContentPlaceHolder>
<meta http-equiv="expires" content="0">
<asp:ContentPlaceHolder ID="MasterScript" runat="server">
</asp:ContentPlaceHolder>
</head>

Note the two content place holders. This could be accomplished with one but for clarity sake, I have made it into two. Now in the content page, include the following markup.



<asp:Content ID="MetaContent"
ContentPlaceHolderID="MasterMeta" runat="server">
<meta name="keywords" content="example, master, pages" />
<meta name="description" content="Something out of this world" />
</asp:Content>
<asp:Content ID="Script"
ContentPlaceHolderID="MasterScript" runat="server">
<script type="text/javascript" language="javascript">
function doSomething()
{
alert('We are Doing something');
}
</script>
</asp:Content>

Note that the <HEAD> tag in the master page has the runat="server" attribute defined. This is necessary because the merging of the master and content pages happens on the server.

Events:
While working with Master Pages, I found that the Page_Load firing sequence is not very intuitive. The content Page_Load event fires before the master’s Page_Load event fires. This is important if the master does initialization that the content page relies on. There are other ways around this by using other events and exposing specific initialization methods. Please see links below to reference the appropriate sequence of events.

Conclusion:
Master Pages can be a very valuable tool for consolidating formatting and layout information for web applications written in ASP.Net. As a built in feature of .NET, creating content pages and associating them with Master Pages is very easy. Since it is a built in feature of .NET, Visual Studio integrates with the technology by presenting a seamless development environment for Master Pages. Consolidation, Code Reuse, and standardization of layout are just some of the benefits of using Master Pages technology.

Additional Links:
ASP.NET Master Pages Overview
http://msdn.microsoft.com/en-us/library/wtxbf3hh.aspx

@ MasterType Directive
http://msdn.microsoft.com/en-us/library/ms228274.aspx

Events in ASP.NET Master and Content Pages
http://msdn.microsoft.com/en-us/library/dct97kc3.aspx

No comments: