You are on page 1of 28

Master Page Use in ASP.

NET
Problem. You need to use master pages along with code-behind in your ASP.NET website. Master pages can be combined with code-behind in C# to simplify and ease maintenance of your complex site. Solution. Here we look at how to use master pages in ASP.NET and get started with examples.

1. Understanding master pages in ASP.NET


First, in the examples, I use the Website project type in Visual Studio. This is recommended as it is a simpler and more streamlined project type. If you are developing a Web Application project, you may need to navigate slightly different menus and dialogs. Item Master page Usage in ASP.NET File extension

Stores global page elements that occur .Master on every content page Stores page-specific elements which are Content page .aspx put into master page Master page code Can change master page after it acquires .aspx.cs behind content

2. What you should put on the master page


The master page serves as a template for the other content pages on the site. The master page has some code-behind methods, and it could auto-generate the page titles for the pages and some H1 headers for each content page. Here are some likely things you will want on your master page.

Navigation The master page also might contain a TreeView for navigation and a footer with your contact info. Styles It uses markup for the site layout, and contains some CSS styles and JavaScript code. You can use inline CSS in your master page, for an alternative to an external stylesheet. Images or scripts Your site's logo is a perfect thing to put in your master page. You can also put code such as Google Analytics, which is implemented as a small piece of JavaScript.

3. How can I make a master page?


You must go to the Website menu in Visual Studio, and then add a new item. Master Page will appear in that list, so just select it and proceed as normal. You should be familiar enough with Visual Studio to do this quickly.

4. How master pages work

The ContentPlaceHolder markup tag is where content page content is inserted into each page. So, now we want to make some content pages. How do we do that? Now, look at your new master page file. You will see some tags in the master page. One of the most important ones is as follows:
<asp:ContentPlaceHolder ID="MainContent" runat="server" />

5. How can I make a content page?


Content pages are made in the exact same way that master pages are made. Go to the Website menu, then add new item, and select Web Form. That's MS-speak for "Content Page". Check the "Select Master Page" checkbox, and finally select your master page.

6. How content pages work


Next I want to show you an example of a content page and walk you through some parts of it. You should have a content page now and it will have some special markup in it. It will contain several lines of markup similar to the following.
<%@ Page Language="C#" MasterPageFile="~/DotNetPerls.Master" Title="Untitled Page" %> <script runat="server"> </script> <asp:Content ID="Content1" ContentPlaceHolderID="MainContent" Runat="Server"> </asp:Content>

What's the ID? The ID is Content1, and it is very important when using the content page with the specified master page. ContentPlaceHolderID The ContentPlaceHolderID is "MainContent". It must match the ID="MainContent" part of the ContentPlaceHolder tag in the Master Page. Always remember runat="server" As always, you must add runat="server", which indicates that this tag will be "run" on the server before being sent to the browser.

7. How you can use master pages and content pages together
In the master page, you can change various elements picked up from content pages in the codebehind. The master page transforms itself and takes on the properties of the content pages. Then, you can use code in the master pages to change parts of the master page that were retrieved from the content pages.
using using using using using System; System.Collections.Generic; System.Diagnostics; System.Web; System.Web.UI;

public partial class _Default : MasterPage { // This code belongs in the *master.cs file. You can usually open this

// by clicking on the little + box next to your *.master file. protected void Page_Load(object sender, EventArgs e) { // Using code-behind, we can modify each page's title according to logic. // Let's say you want each title on your site to have the words "Your Site: " // at the start. Try out the following code, and put regular page titles // in the content pages. this.Page.Title = "Your Site: " + this.Page.Title; // This will turn "Content Page Title" into "Your Site: Content Page Title" // on every content page. } }

8. Summary
You can dynamically generate pages, leading to fewer typos, with less content to manage. The master page, content page and code-behind model can simplify and streamline your site. Codebehind can separate complex logic from page markup.

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. Lets 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. Lets 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 serverside controls are the form tag (form1) and the ContentPlaceHolder (ContentPlaceHolder1). Lets 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 pages children with itself.

The master pages 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 pages PreInit event, but before the content pages 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. Ive found it useful to only think of master pages as masters during design time. When the application is executing, its 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 pages 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 pages control hierarchy before the Init event fires. The simple solution is to just use the PreInit event, but we probably dont 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 dont 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 doesnt want its 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 dont 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 dont have a MasterPageFile property). We hook the pages 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 applications web.config.
<httpModules> <add name="MyMasterPageModule" type="MasterPageModule"/> </httpModules>

Abstract Interaction
Now its 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, lets look at how the content page can interact with the master page.
Content Page to Master Page Interaction

Lets 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 pages 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, lets 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 doesnt 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. Weve 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 doesnt 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 lets 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 dont 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 classs 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 its parent page what to do, the master page should tell a page when something interesting happenes, and let the page decide what to do. Lets 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 dont 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 well end up changing all of our pages. The third approach decouples the master page and content page nicely. The page wont need to know what controls are on the master page, and the master page doesnt 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 dont 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, Lets 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 weve 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 forms 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. Lets assume our SearchResults.aspx page does not use a master page, and we dont 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 doesnt 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. Lets 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 pages Load event fires before the master pages Load event. Lets 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 pages Init event handler will fire before the content pages Init event handler. Obviously, problems will occur if the content pages 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 cant 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 pages 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 isnt 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. Youll 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


Its 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? Lets 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 doesnt 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 wont be searching inside of MasterPage control. course, we dont 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, its 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 dont 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

Name Mangling

A naming container also mangles its childrens 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 its 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, wed never want to hardcode the client ID into a script. Typically well 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, lets 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" />

Weve 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 doesnt catch everything. If we included our style sheet with the following code, the runtime wont rebase the relative href.
<head runat="server"> <style type="text/css" media="all"> @import "styles/styles.css"; </style> </head>

Also, the runtime doesnt 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, its 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 wont 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.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, lets 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


Its 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 pages content into its ContentPlaceHolder controls. Then the parent master page will copy the nested master pages 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. Lets 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 cant 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 doesnt 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 using using using System; System.Web.UI; System.Web.Configuration; 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; } }

Sharing Master Pages


Many people want to create a single master page, or set of master pages to use across multiple applications. Unfortunately, there is no built-in capability to share master pages, and this article will only provide some advice. The ultimate goal is the ability to modify a master page once, and have the changes reflected in multiple applications with the least effort. The first alternative is to copy shared master page files into a single location on an IIS web server. Each application can then create a virtual directory as a subdirectory and point the virtual directory to the real directory of master pages. The applications can then set the MasterPageFile

property of a page to the name of the virtual directory, plus the name of the master page file. When we drop an updated master page file into the real directory, the new master page will appear in all the applications immediately. A second approach is to use a version control system to share a set of master page files across multiple projects. Most source control / version control systems support some level of share functionality, where a file or folder can appear in more than one project. When a developer checks in an updated master page file, the other projects will see the change immediately (although this behavior is generally configurable). In production and test, each application would need to be redeployed for the update master page to appear. Finally, the VirtualPathProvider in ASP.NET 2.0 can serve files that do not exist on the file system. With the VirtualPathProvider, a set of master pages could live in database tables that all applications use. Conclusions The one point we should take away from this article is that we shouldnt treat master pages as the masters, but as just another control inside the page. Many design and runtime problems become easier to solve with this method of thinking. Weve seen how to handle events, how to handle interactions in both directions, and how to avoid problems with JavaScript and relative URLs. In all of these cases we can treat the master page as a user control inside the page, and have a solid solution.

Master Pages in ASP.NET 2.0

So you have heard of Master pages but havent really gotten around to research what is all about? If this is you, read on! Master Pages enables you the developer to create a consistent look and feel for your web application. For most of us, we either create a header and footer on each and every webpage or we create a header and footer control and then add those controls to each and every .aspx page. Ofcourse some might have seen the pitfall in this method and weaved their own custom page that dynamically adds both the header and footer to their pages automagically. Well the good news is this is now built in, and here is a quick and simply overview of how MasterPages works in ASP.NET 2.0.

Master Pages
A master page is similar to a .aspx page except that it has its own unique extension, .master. Furthermore, a master page contains a new Master directive, along with a ContentPlaceHolder control. The master directive tells us that this page is a master page, and the ContentPlaceHolder control is a placeholder (as the name suggests) for a content area where your content pages will inject their content into the template. Keep in mind your master page can contain multiple ContentPlaceHolder tags, your content pages will simply reference the ID of the ContentPlaceHolder to ensure the content is injected into the correct area of the template. So putting this knowledge to work, lets create a very simply Master Page:
<%@ Master Language=C# %> <script language=c# runat=server> // your code here you wish! </script> <html> <head><title>Master Pages Tutorial on CSharpFriends!</title> <body> <form runat=server> <asp:ContentPlaceHolder ID=ContentPlaceHolder1 runat=server> <h1>Look Mom, default content that will render only if not overridden on the content page!</h1> </asp:ContentPlaceHolder> </form> </body> </html>

So you will notice the master page has that master directive I mentioned earlier, along with the ContentPlaceHolder. You will notice that within the ContentPlaceHolder control there is some content. This content will render in any content page that doesnt reference this ContentPlaceHolders ID, so in effect you can have default content on all your content pages which is overridden if you have content. Once you have your master page, you obviously want to create content pages using your master page as a template. Your content page will be like any other .aspx page you have created in the past, except that it will reference the master page in the Page Directive and it will contain a Content Control and

between this control will be all your content or at least the all the content for the corresponding ContentPlaceHolder contron in your master page.
<%@ Page Language=C# MasterPageFile=~/csharpfriends.master %> <asp:Content id=content1 ContentPlaceHolderID=ContentPlaceHolder1 runat=server> <h1>Look Mom, actual content on my content page! Looks like Im going to make it after all!</h1> </asp:Content>

As I mentioned earlier, your master page can contain numerous content areas using ContentPlaceHolder controls, just make sure to match up the ContentPlaceHolders ID in your pages. Basically want happens is that when your .aspx page renders, it will cycle through all the Content controls and match up the PlaceHolder in the master page.

Master Page Configuration Options


As it is now, it is possible to create content pages that dont use the master page by simply NOT referencing the master page. This could be something you want, or it could be something you dont want. To force pages to use the master page layout you can modify you web.config file by adding this XML tag. Keep in mind that even though a master page is forced on all pages, if the .aspx page references another master page file it will override your web.config settings.
<configuration> <system.web> <pages masterPageFile=csharpfriends.master /> </system.web> </configuration>

This tutorial on master pages is for beginners, hings can get more complicated with master pages, such as programmatically including master pages in your content pages, nesting master pages and how events work etc. These topics might come in future articles so keep a eye out for them.

Summary
In this brief article, I outlined the benefits of using Master Pages, the differences between a master page and a regular aspx file along with a simple implementation and possible configuration options you have.

How to: Create a Minimal Master Page...


One of the first tasks that you must complete when configuring a Microsoft Office SharePoint Server 2007 Web site is to create one or more master pages. A master page contains references to elements that you can share across multiple pages in an Office SharePoint Server 2007 site, such as navigation, search controls, logon controls, and banner images. A master page can also contain the cascading style sheet (CSS) and ECMAScript (JScript, JavaScript) references that define the overall look and feel of your site. Commonly, every siteand therefore every pagein your site collection uses the same master page to present a consistent user experience across the entire site collection. Depending on your needs, you can use a different master page for one or for all of the sites in your site hierarchy to distinguish the various areas of your portal. Master Page Galleries

When you provision a site collection in Office SharePoint Server 2007, the system creates a master page gallery that contains all the master pages and page layouts for that site collection. If the site collection uses either the Publishing Portal or Collaboration Portal templates, the master page gallery includes several master pages that are provided with Office SharePoint Server 2007, such as BlueBand.master. These master pages are located in the path
C:\Program%20Files\Common%20Files\Microsoft%20Shared\web%20server%20extension s\12\TEMPLATE\FEATURES\PublishingLayouts\MasterPages\, with other example .master

pages. You can use any of these master pages as they are, or you can customize them fully to create unique branding for your site. Why Start with a Minimal Master Page Creating and completing a master page to begin your SharePoint site customization takes planning and time. If you can, you want to prevent having to rewrite or back out code you don't need in your master page. This topic shows you how to create a minimal master page that includes only the minimal functionality that Office SharePoint Server 2007 requires so that you have a stable platform upon which to build your own master pages. Creating a minimal master page can help you avoid the time-consuming process of backing code out of a pre-existing .master page such as BlueBand.master, or removing functionality and then building it back in when your customization needs change again. This topic supports using the Minimal Master Page described as a Site Master Page in Office SharePoint Server 2007. It does not support using the Minimal Master Page described in this topic as a System Master Page in Office SharePoint Server 2007. Using this content with Windows SharePoint Services 3.0 is not explicitly supported. You can, of course, create a master page from scratch. However, we generally do not recommend this because a truly empty master page does not include all the content placeholders that the Office SharePoint Server 2007 page model needs to work correctly. The sample code in the following procedure includes only what the Office SharePoint Server 2007 page model requiresnecessary content placeholders and controls to work with the page layouts that are included in a default Office SharePoint Server 2007 installation. Office SharePoint Server 2007 requires a master page that includes a title, branding, logon functionality, search functionality, breadcrumb functionality, and basic structural elements such as page areas, separators, borders, consoles, and description placeholders. The following procedure uses Office SharePoint Designer 2007 as the master page design environment. You can, however, use a text editor, a Web editor such as Microsoft Office SharePoint Designer 2007, or an integrated development environment (IDE) such as Microsoft Visual Studio 2005 to create a master page. The master pages included with Office SharePoint Server 2007 are based on the SPWeb.CustomMasterUrl property of the SPWeb class in Windows SharePoint Services.

To create a minimal master page


1. Open SharePoint Designer. 2. On the File menu, click New, point to SharePoint Content, and then click the Page tab. 3. Double-click Master Page to create a new master page.

4. Click Design to show the master page in design view. You should see header and left margin areas and several content placeholders in the master page. 5. Click Code to show the master page in code view. 6. Copy the following code into the master page.
Xml Copy Code
<%-- Identifies this page as a .master page written in Microsoft Visual C# and registers tag prefixes, namespaces, assemblies, and controls. -%> <%@ Master language="C#" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <%@ Import Namespace="Microsoft.SharePoint" %> <%@ Register Tagprefix="SPSWC" Namespace="Microsoft.SharePoint.Portal.WebControls" Assembly="Microsoft.SharePoint.Portal, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register Tagprefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register Tagprefix="PublishingWebControls" Namespace="Microsoft.SharePoint.Publishing.WebControls" Assembly="Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register Tagprefix="PublishingNavigation" Namespace="Microsoft.SharePoint.Publishing.Navigation" Assembly="Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register TagPrefix="wssuc" TagName="Welcome" src="~/_controltemplates/Welcome.ascx" %> <%@ Register TagPrefix="wssuc" TagName="DesignModeConsole" src="~/_controltemplates/DesignModeConsole.ascx" %> <%@ Register TagPrefix="PublishingVariations" TagName="VariationsLabelMenu" src="~/_controltemplates/VariationsLabelMenu.ascx" %> <%@ Register Tagprefix="PublishingConsole" TagName="Console" src="~/_controltemplates/PublishingConsole.ascx" %> <%@ Register TagPrefix="PublishingSiteAction" TagName="SiteActionMenu" src="~/_controltemplates/PublishingActionMenu.ascx" %> <%-- Uses the Microsoft Office namespace and schema. --%> <html> <WebPartPages:SPWebPartManager runat="server"/> <SharePoint:RobotsMetaTag runat="server"/> <%-- The head section includes a content placeholder for the page title and links to CSS and ECMAScript (JScript, JavaScript) files that run on the server. --%>

<head runat="server"> <asp:ContentPlaceHolder runat="server" id="head"> <title> <asp:ContentPlaceHolder id="PlaceHolderPageTitle" runat="server" /> </title> </asp:ContentPlaceHolder> <Sharepoint:CssLink runat="server"/> <asp:ContentPlaceHolder id="PlaceHolderAdditionalPageHead" runat="server" /> </head> <%-- When loading the body of the .master page, SharePoint Server 2007 also loads the SpBodyOnLoadWrapper class. This class handles .js calls for the master page. --%> <body onload="javascript:_spBodyOnLoadWrapper();"> <%-- The SPWebPartManager manages all of the Web part controls, functionality, and events that occur on a Web page. --%> <form runat="server" onsubmit="return _spFormOnSubmitWrapper();"> <wssuc:Welcome id="explitLogout" runat="server"/> <PublishingSiteAction:SiteActionMenu runat="server"/> <PublishingWebControls:AuthoringContainer id="authoringcontrols" runat="server"> <PublishingConsole:Console runat="server" /> </PublishingWebControls:AuthoringContainer> <%-- The PlaceHolderMain content placeholder defines where to place the page content for all the content from the page layout. The page layout can overwrite any content placeholder from the master page. Example: The PlaceHolderLeftNavBar can overwrite the left navigation bar. --%> <asp:ContentPlaceHolder id="PlaceHolderMain" runat="server" /> <asp:Panel visible="false" runat="server"> <%-- These ContentPlaceHolders ensure all default SharePoint Server pages render with this master page. If the system master page is set to any default master page, the only content placeholders required are those that are overridden by your page layouts. --%> <asp:ContentPlaceHolder id="PlaceHolderSearchArea" runat="server"/> <asp:ContentPlaceHolder id="PlaceHolderTitleBreadcrumb" runat="server"/> <asp:ContentPlaceHolder id="PlaceHolderPageTitleInTitleArea" runat="server"/> <asp:ContentPlaceHolder id="PlaceHolderLeftNavBar" runat="server"/> <asp:ContentPlaceHolder ID="PlaceHolderPageImage" runat="server"/> <asp:ContentPlaceHolder ID="PlaceHolderBodyLeftBorder" runat="server"/> <asp:ContentPlaceHolder ID="PlaceHolderNavSpacer" runat="server"/> <asp:ContentPlaceHolder ID="PlaceHolderTitleLeftBorder" runat="server"/> <asp:ContentPlaceHolder ID="PlaceHolderTitleAreaSeparator" runat="server"/> <asp:ContentPlaceHolder ID="PlaceHolderMiniConsole" runat="server"/> <asp:ContentPlaceHolder id="PlaceHolderCalendarNavigator" runat ="server" /> <asp:ContentPlaceHolder id="PlaceHolderLeftActions" runat ="server"/> <asp:ContentPlaceHolder id="PlaceHolderPageDescription" runat ="server"/> <asp:ContentPlaceHolder id="PlaceHolderBodyAreaClass" runat ="server"/> <asp:ContentPlaceHolder id="PlaceHolderTitleAreaClass" runat ="server"/> <asp:ContentPlaceHolder id="PlaceHolderBodyRightMargin" runat="server" />

</asp:Panel> </form> </body> </html>

7. On the File menu, click Save As, provide a unique file name with the .master extension, and then save the file to the master page gallery (/_catalogs/masterpage) in your site collection.

You might also like