Pages

Thursday, August 8, 2013

Master Pages in ASP.NET

A professional web site will have a standardized look across all pages. For example, one popular layout type places a navigation menu on the left side of the page, a copyright on the bottom, and content in the middle. It can be difficult to maintain a standard look if you must always put the common pieces in place with every web form you build. In ASP.NET 2.0, master pages will make the job easier. You’ll only need to write the common pieces once - in the master page. A master page can serve as a template for one or more web forms. Each ASPX web form only needs to define the content unique to itself, and this content will plug into specified areas of the master page layout.

Marking Up Master Pages

To add a master page to a web project, right-click your web project in the Solution Explorer window, select Add New Item, and select the Master Page item type from the Add New Item dialog. The listing below shows a sample master page in source view.
<%@ Master Language="VB" CodeFile="otc.master.vb" Inherits="otc" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
            "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title>Untitled Page</title>
  <link href="~/StyleSheet.css" rel="Stylesheet" type="text/css" />
</head>

<body>
  <form id="form1" runat="server">
    <asp:Menu ID="Menu1" runat="server" Orientation="Horizontal">
      <Items>
        <asp:MenuItem NavigateUrl="~/Default.aspx" Text="Home" Value="Home"/>
        <asp:MenuItem NavigateUrl="~/Experiments.aspx"
              Text="Experiments" Value="Experiments"/>
      </Items>   
    </asp:Menu>

    <div>
      <div id="main">
        <asp:ContentPlaceHolder ID="mainContent" runat="server" />
      </div>
      <div id="right">
        <asp:ContentPlaceHolder ID="sideContent" runat="server" />
      </div>
      <div id="footer">
        <asp:Literal ID="Footer" runat="server" Text="OdeToCode.com" />
      </div>
    </div>

  </form>
</body>
</html>

A master page looks very similar to an ASPX file, except a master page will have a .master file extension instead of a .aspx extension, and uses an @ Master directive instead of an @ Page directive at the top. Master pages will define the <html>, <head>, <body >, and <form>, tags. A new control, theContentPlaceHolder control also appears in our master page. You can have one or more ContentPlaceHolder controls in a master page. ContentPlaceHolder controls are where we want our ASPX web forms to place their content.
Just like any ASPX form, our master page can contain code in a <script> block, or in a code-behind file, and can respond to page lifecycle events. The MasterPage class (from the System.Web.UI namespace) derives from UserControl and will have all of the usual events: Init, Load, PreRender, etc. The following listing shows how we’ve added a Page_Load event handler to modify the color of the menu control on the page, and we’ve also added a property to allow setting and retrieving the text inside the footer.
Partial Class otc
  Inherits System.Web.UI.MasterPage

  ProtectedSub Page_Load(ByVal sender AsObject, _
                          ByVal e As System.EventArgs) _
                          HandlesMe.Load

    IfNot IsPostBack Then
        Menu1.BackColor = Drawing.Color.LemonChiffon
    EndIf

  EndSub


  PublicProperty FooterText() AsString
    Get
        Return Footer.Text
    EndGet
    Set(ByVal value AsString)
        Footer.Text = value
    EndSet
  EndProperty

EndClass

Plugging In Content

The ASPX web forms we use to put content into master pages are plain .aspx files, although with a few differences. When you add a new ASPX web form to a project you’ll have the option of associating the web form with a master page. Doing so will place a MasterPageFile attribute in the @ Page directive for the web form. The attribute points to the .master file you’ve selected (you can have multiple master pages in a web project). The source code below is for a default.aspx file using the otc.master master page we defined earlier.
<%@ Page Language="VB" MasterPageFile="~/otc.master"
    AutoEventWireup="false"
    CodeFile="Default.aspx.vb"
    Inherits="_Default" Title="Untitled Page" %>

<asp:Content ID="Content1" ContentPlaceHolderID="mainContent" runat="Server">
    <h1>mainContent</h1>
    Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Quisque auctor, dui a
    accumsan faucibus, erat dolor rhoncus massa, a hendrerit nulla purus eu dui. Morbi
    eget ligula. Proin sapien augue, sagittis vel, blandit sed, lobortis et, nulla.
    Nullam laoreet blandit erat. Ut egestas. Lorem ipsum dolor sit amet, consectetuer
    adipiscing elit. Pellentesque ut wisi at tellus placerat vestibulum. Aliquam at
    lacus non justo vulputate suscipit. Quisque mattis, arcu sed tempus fermentum, diam
    neque varius elit, vel eleifend nibh urna a pede.
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="sideContent" runat="Server">
    <h4>sideContent</h4>
    Donec hendrerit. Aliquam at massa. Pellentesque sed justo. Donec interdum ipsum
    vitae nisl. Maecenas ac quam non lacus posuere convallis. Mauris nibh. Quisque mattis,
    arcu sed tempus fermentum, diam neque varius elit, vel
</asp:Content>

A web form associated with a master page is called a content page. A content page may only contain markup inside of Content controls. If you try to place any markup or controls outside of the Content controls, you’ll receive a compiler error: “Only Content controls are allowed directly in a content page that contains Content controls.”
Each Content control in a content page maps to exactly one of the ContentPlaceHolder controls in the Master Page. You do not need to define a content control for every ContentPlaceHolder control in the master. If a Content control doesn’t exist for a place holder, the master page will simply render the default content inside of the ContentPlaceHolder of the master page (we did not define any in this example).
The runtime examines the ID of each Content control and finds a ContentPlaceHolder with a matching ID. When the runtime finds the match it will take the markup and controls inside the Content control and insert them into the ContentPlaceHolder of the master page. Even though our Content controls in this example do not contain any server side controls, like GridView controls and Calendar controls, any ASP.NET control can appear inside the Content areas.
One benefit to working with master pages in the Visual Studio IDE is the built-in design support for master pages. The screen below shows our default.aspx form in a design time view. The Content controls are the only editable sections of the form. The master page markup displays in design view, but since we are editing default.aspx and not otc.master in the screen shot, the sections defined by the master page are un-editable and ghosted out. This gives designers and developers a clear idea of content versus the layout structure defined by the master page.
The second screen shot above shows our default.aspx web form in action. We’ve used a .css style sheet (shown below) to position the <div> tags on the page. The approach will give us a great deal of flexibility. We can make changes to the layout of the site simply by editing the CSS or master page, whichever makes the most sense for the change we need to make. In either case, we will never have to duplicate the <div>s on any of our web forms, the layout is controlled entirely in the master page.
body
{
    font-size: smaller;
}

#main
{
    float: left;
    width: 67%;
    background: #fff;
    border-right: 1px solid #000;
    margin-right: 15px;
    padding-bottom: 10px;
    padding-right: 10px;      
}

#footer
{
    clear: both;
    width: 100%;  
    text-align: center;
    font-size: 10px;
    padding: 3px;  
    border-top: 3px solid #333;
    background-color: #cccc99;
}

Programmatically Working With Master Pages

The MasterPage class derives from UserControl. The master page will inject itself as the top control in a page’s control hierarchy by clearing the page’s Controls array and adding itself to into the collection. Doing so includes all of the markup and controls defined inside the master page in the page's control tree. The master page can then walk the Content controls that existed in the web form and bring them back them into the control hierarchy in the appropriate locations. The master page injection happens after the PreInit event fires for a Page object, but before the Init event fires.
This ordering of events outlined above is important if you want to programmatically set the master page to use for a web form with the MasterPageFile property. This property must be set in the PreInit event (or earlier), like the code below. One the master page has injected itself into the control hierarchy it is too late to try to set a new master page for the web form. If you try to set the MasterPageFile property after the PreInit event fires, the runtime will throw an InvalidOperationException.
ProtectedSub Page_PreInit(ByVal sender AsObject, _
                           ByVal e As EventArgs) _
                           HandlesMe.PreInit
    ' we can select a different master page in the PreInit event
    Me.MasterPageFile = "~/otc.master"

EndSub

Another way to interact with a master page is through the Master property of a page object. Let’s say we need to get to the Footer property defined in our master page. There are two approaches to touching this property. The first is to use the Master property of the  System.Web.UI.Page class, which returns a MasterPage reference. In order to get to the Footer property, though, we would have to cast the reference to our derived master page type, like the following code.
CType(Master, otc).FooterText = "New Footer Text"

A second approach, if you know the exact master page file your page will be using at runtime, is to let the ASP.NET page parser generate a strongly typed Master property by adding an @ MasterType directive to your ASPX file, as shown below.
<%@ MasterType VirtualPath="~/otc.master"  %>

The MasterType directive will instruct the runtime to add a new Master property to code-generated file for the page. The new property will return a reference matching the MasterType. With a MasterType directive in place for our web form, we don't need a cast.  For more information on @ MasterType, see, “MasterType in ASP.NET 2.0“.
Master.FooterText = "Simple!"

URL Rebasing In Master Pages

The master page implementation outlined above has another consequence to watch for. Since the master page injects it's controls and markup into the page’s Controls array, any relative URLs in the master page markup may break. The browser will request the web form, not the master page, so all of the URLs will be relative to the web form. When the web form and master page are in different directories, relative URLs may point to the wrong location. To help alleviate relative URL problems, ASP.NET will rebase relative URLs for server-side controls in a master page. Rebasing will build the correct URL to the resource.
As an example, imagine we created our master page in the root directory of a web site, and we have an images subdirectory underneath the root. We might be tempted to add an image to our master page with the following markup.
<img src="images/tree.png" alt="1" />

The above URL will work as long as the web form using the master page is in the root directory. If we attach a web form in another subdiretory to the master page the image URL will break. One way to fix the broken URL is to turn the HTML <img> tag into a server side control by adding runat=”server”. Server side controls have their URLs rebased. Notice the <head> tag in the master page has runat=”server” in place, this will rebase your style sheet links.
<img src="images/tree.png" runat="server" alt="2" />

The following markup demonstrates what will work, and not work from a master page.
<!-- this will break -->
<img src="images/tree.png" alt="1" />

<!-- this wil work -->
<!-- server side control will rebase itself -->
<img src="images/tree.png" runat="server" alt="2" />

<!-- this will work -->
<!-- rooted references will always work, but are fragile -->
<img src="/masterp/images/tree.png" alt="3" />

<!-- this will break -->
<!-- app relative (~) only works on a server side control -->
<img src="~/images/tree.png" alt="4" />

<!-- this will work -->
<img src="~/images/tree.png" runat="server" alt="5" />       

<!-- both of these will work -->
<!-- the second Image will rebase URL -->
<asp:Image ID="Image6" runat="server" ImageUrl="~/images/tree.png" AlternateText="6"  />  
<asp:Image ID="Image7" runat="server" ImageUrl="images/tree.png" AlternateText ="7" />

<div style="background-image: url('images/tree.png');"  >
  My background is relative, and broken<br /><br />
</div>

<div style="background-image: url('/masterp/images/tree.png');"  >
  My background works<br /><br />
</div>

<div style="background-image: url('images/tree.png');" runat="server" >
  My background is broken. URL rebasing doesn't work for embedded style, event
  with server side control. <br /><br />
</div>

Nested Master Pages

You can write a content page that is itself a master page, allowing an arbitrarily deep nesting of master pages. This technique can be useful when your application is broken into sub-applications that need to inherit a common branded look but still be able to define their own customized layout template within the brand. The following listing shows a master page that would serve as a content page for our first master page example.
<%@ Master Language="VB" MasterPageFile="~/otc.master"
    CodeFile="otcchild.master.vb" Inherits="otcchild" %>

<asp:Content ID="Content1" ContentPlaceHolderID="mainContent" runat="Server">
  <div>
    <asp:ContentPlaceHolder ID="main1" runat="server" />
  </div>
  <div>
    <asp:ContentPlaceHolder ID="main2" runat="server" />
  </div>
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="sideContent" runat="Server">
  <asp:ContentPlaceHolder ID="side1" runat="server" />
</asp:Content>


Notice we must obey the rules for a content page in this master page, that is, we can only define content inside of Content controls. The ContentPlaceHolders inside of these controls can then be filled in with web forms addressing the IDs of the nested master page. A web form cannot reach the ContentPlaceHolders in the master page above it’s own master page.
<%@ Page Language="VB" MasterPageFile="~/nested/otcchild.master"
    AutoEventWireup="false" CodeFile="Default.aspx.vb"
    Inherits="_Default" title="Untitled Page" %>

<asp:Content ID="main1" ContentPlaceHolderID="main1" Runat="Server">
  <h4>Content1</h4>
</asp:Content>

<asp:Content ID="main2" ContentPlaceHolderID="main2" Runat="Server">
  <h4>Content2</h4>
</asp:Content>

<asp:Content ID="side1" ContentPlaceHolderID="side1" Runat="Server">
  <h4>Content3</h4>
</asp:Content>

Master Pages and Configuration

There are three ways to associate a web form with a master page. You can use the Master attribute in the @ Page directive, and you can write to the MasterPageFile property in the PreInit event or earlier. We’ve seen examples of both of these techniques. Any web form using the Master attribute of the @ Page directive or setting the MasterPageFile property programmatically will override the web.config settings.
We can also assign a default master page for all web forms in a site in our web.config file using the <pages> element. An example excerpt from web.config is shown below.
<configuration>
  <system.Web>
    <pages master="otc.master" />
  </system.Web>
</configuration>

Of Headers and Meta tags

The last topic we will address is the topic of header information for ASP.NET web forms using a master page. The master page must define <head>, meaning the master page will include the <title> tag, among others. Since the master page does not know the title of the content page that will plug in, it simply sets the title to “untitled”.
Since a content page cannot contain any markup outside of Content controls, there is no way for a content page to use a title tag, but there is now a Title attribute in the @ Page directive. There is also a Title property on the page itself.
For other elements that end up in the header, the Page class now exposes a Header property of type HtmlHead. You can use the HtmlHead reference to modify style sheet settings and add HtmlMeta objects as needed.

Conclusions

Master pages provide a key component to any web application, namely the ability to organize a consistent layout into reusable templates. These master page templates offer full designer support and programmatic interfaces to meet the demands of most applications.

No comments:

Post a Comment