Wednesday, 8 February 2012

URLs in Master Pages (C#)

URLs in Master Pages (C#)

 

Part 3

Introduction

In all the examples we've seen thus far, the master and content pages have been located in the same folder (the website's root folder). But there's no reason why the master and content pages must be in the same folder. You can certainly create content pages in subfolders. Similarly, you might create a ~/MasterPages/ folder where you place your site's master pages.
One potential issue with placing master and content pages in different folders involves broken URLs. If the master page contains relative URLs in hyperlinks, images, or other elements, the link will be invalid for content pages that reside in a different folder. In this tutorial we examine the source of this problem as well as workarounds.

The Problem with Relative URLs

A URL on a web page is said to be a relative URL if the location of the resource it points to is relative to the web page's location in the website's folder structure. Any URL that does not start with a leading forward slash (/) or a protocol (such as http://) is relative because it is resolved by the browser based on the location of the web page that contains the URL.
For example, our website has an ~/Images/ folder with a single image file, PoweredByASPNET.gif. The master page file Site.master has an <img> element in the footerContent region with the following markup:
<div id="footerContent">
 
<img src="Images/PoweredByASPNET.gif" alt="Powered by ASP.NET!" />
</div>
The src attribute value in the <img> element is a relative URL because it does not start with / or http://. In short, the src attribute value tells the browser to look in the Images subfolder for a file named PoweredByASPNET.gif.
When visiting a content page, the above markup is sent directly to the browser. Take a moment to visit About.aspx and view the HTML source sent to the browser. You will find that the exact same markup in the master page was sent to the browser.
<div id="footerContent">
 
<img src="Images/PoweredByASPNET.gif" alt="Powered by ASP.NET!" />
</div>
If the content page is in the root folder (as is About.aspx) everything works as expected because there is an Images subfolder relative to the root folder. However, things break down if the content page is in a different folder than the master page. To illustrate this, create a subfolder named Admin. Next, add a content page named Default.aspx to the Admin folder, making sure to bind the new page to the Site.master master page.
Note: In the Specifying the Title, Meta Tags, and Other HTML Headers in the Master Page tutorial we created a custom base page class named BasePage that automatically set the content page's title (if it was not explicitly assigned). Don't forget to have the newly created page's code-behind class derive from BasePage so that it can utilize this functionality.
After you have created this content page, your Solution Explorer should look similar to Figure 1.
A New Folder and ASP.NET Page Have Been Added to the Project
Figure 01: A New Folder and ASP.NET Page Have Been Added to the Project
Next, update the Web.sitemap file to include a new <siteMapNode> entry for this lesson. The following XML shows the complete Web.sitemap markup, which now includes the addition of a third <siteMapNode> element.
<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
 
<siteMapNode url="~/Default.aspx" title="Home">
 
<siteMapNode url="~/About.aspx" title="About the Author" />
 
<siteMapNode url="~/MultipleContentPlaceHolders.aspx" title="Using Multiple ContentPlaceHolder Controls" />
 
<siteMapNode url="~/Admin/Default.aspx" title="Rebasing URLs" />
 
</siteMapNode>
</siteMap>
The newly created Default.aspx page should have four Content controls corresponding to the four ContentPlaceHolders in Site.master. Add some text to the Content control referencing the MainContent ContentPlaceHolder and then visit the page through a browser. As Figure 2 shows, the browser cannot find the PoweredByASPNET.gif image file. What's going on here?
The ~/Admin/Default.aspx content page is sent the same HTML for the footerContent region as was the About.aspx page:
<div id="footerContent">
 
<img src="Images/PoweredByASPNET.gif" alt="Powered by ASP.NET!" />
</div>
Because the <img> element's src attribute is a relative URL, the browser attempts to look for an ImagesAdmin/Images/PoweredByASPNET.gif. folder relative to the web page's folder location. In other words, the browser is looking for the image file
Figure 02: The PoweredByASPNET.gif Image File Cannot Be Found (Click to view full-size image)

Replacing Relative URLs with Absolute URLs

The opposite of a relative URL is an absolute URL, which is one that starts with a forward slash (/) or a protocol such as http://. Because an absolute URL specifies the location of a resource from a known fixed point, the same absolute URL is valid in any web page, regardless of the web page's location in the website's folder structure.
To remedy the broken image shown in Figure 2, we need to update the <img> element's src attribute so that it uses an absolute URL instead of a relative one. To determine the correct absolute URL, visit one of the web pages in your website and examine the Address bar. As the Address bar in Figure 2 shows, the fully qualified path to the web application is http://localhost:3908/ASPNET_MasterPages_Tutorial_04_CS/. Therefore, we could update the <img> element's src attribute to either of the following two absolute URLs:
  • /ASPNET_MasterPages_Tutorial_04_CS/Images/PoweredByASPNET.gif
  • http://localhost:3908/ASPNET_MasterPages_Tutorial_04_CS/Images/PoweredByASPNET.gif
Take a moment to update the <img> element's src attribute to an absolute URL using one of the forms shown above and then visit the ~/Admin/Default.aspx page through a browser. This time the browser will correctly find and display the PoweredByASPNET.gif image file (see Figure 3).
Figure 03: The PoweredByASPNET.gif Image is Now Displayed (Click to view full-size image)
While hard coding in the absolute URL works, it tightly couples your HTML to the website's server and folder location, which may change. Using an absolute URL of the form http://localhost:3908/... is brittle because the port number preceding localhost is selected automatically each time Visual Studio's built-in ASP.NET Development Web Server is started. Similarly, the http://localhost part is only valid when testing locally. Once the code is deployed to a production server, the URL base will change to something else, like http://www.yourserver.com. The absolute URL in the form /ASPNET_MasterPages_Tutorial_04_CS/... also suffers from the same brittleness because oftentimes this application path differs between development and production servers.
The good news is that ASP.NET offers a method for generating a valid relative URL at runtime.

Using ~ and ResolveClientUrl

Rather than hard code an absolute URL, ASP.NET allows page developers to use the tilde (~) to indicate the root of the web application. For example, earlier in this tutorial I used the notation ~/Admin/Default.aspx in the text to refer to the Default.aspx page in the Admin folder. The ~ indicates that the Admin folder is a subfolder of the web application's root.
The Control class's ResolveClientUrl method takes a URL and modifies it to a relative URL appropriate for the web page on which the control resides. For example, calling ResolveClientUrl("~/Images/PoweredByASPNET.gif") from About.aspx returns Images/PoweredByASPNET.gif. Calling it from ~/Admin/Default.aspx, however, returns ../Images/PoweredByASPNET.gif.
Note: Because all ASP.NET server controls derive from the Control class, all server controls have access to the ResolveClientUrl method. Even the Page class derives from the Control class, meaning that you can use this method directly from your ASP.NET pages' code-behind classes.

Using ~ in the Declarative Markup

Several ASP.NET Web controls include URL-related properties: the HyperLink control has a NavigateUrl property; the Image control has an ImageUrl property; and so on. When rendered, these controls pass their URL-related property values to ResolveClientUrl. Consequently, if these properties contain a ~ to indicate the root of the web application, the URL will be modified to a valid relative URL.
Keep in mind that only ASP.NET server controls transform the ~ in their URL-related properties. If a ~<img src="~/Images/PoweredByASPNET.gif" />, the ASP.NET engine sends the ~ to the browser along with the rest of the HTML content. The browser assumes that the ~ is part of the URL. For example, if the browser receives the markup <img src="~/Images/PoweredByASPNET.gif" /> it assumes there is a subfolder named ~ with a subfolder Images that contains the image file PoweredByASPNET.gif. appears in static HTML markup, such as
To fix the image markup in Site.master, replace the existing <img> element with an ASP.NET Image Web control. Set the Image Web control's ID to PoweredByImage, its ImageUrl property to ~/Images/PoweredByASPNET.gif, and its AlternateText property to "Powered by ASP.NET!"
<div id="footerContent">
 
<asp:Image ID="PoweredByImage" runat="server" ImageUrl="~/Images/PoweredByASPNET.gif"
   
AlternateText="Powered by ASP.NET!" />
</div>
After making this change to the master page, revisit the ~/Admin/Default.aspx page again. This time the PoweredByASPNET.gif image file appears in the page (see Figure 3). When the Image Web control is rendered it uses the ResolveClientUrl method to resolve its ImageUrl property value. In ~/Admin/Default.aspx the ImageUrl is converted into an appropriate relative URL, as the following snippet of HTML source shows:
<div id="footerContent">
 
<img id="ctl00_PoweredByImage" src="../Images/PoweredByASPNET.gif" alt="Powered by ASP.NET!" />
</div>
Note: In addition to being used in URL-based Web control properties, the ~ can also be used when calling the Response.Redirect and Server.MapPath methods, among others. Also, the ResolveClientUrlFritz Onion's blog entry Using ResolveClientUrl in Markup. method may be invoked directly from an ASP.NET or master page's declarative markup, if needed; see

Fixing the Master Page's Remaining Relative URLs

In addition to the <img> element in the footerContent that we just fixed, the master page contains one more relative URL that requires our attention. The topContent region includes the link "Master Pages Tutorials," which points to Default.aspx.
<div id="topContent">
 
<a href="Default.aspx">Master Pages Tutorials</a>
</div>
Because this URL is relative, it will send the user to the Default.aspx page in the folder of the content page they are visiting. To have this link always point to Default.aspx in the root folder we need to replace the <a> element with a HyperLink Web control so that we can use the ~ notation.
Remove the <a> element markup and add a HyperLink control in its place. Set the HyperLink's ID to lnkHome, its NavigateUrl property to ~/Default.aspx, and its Text property to "Master Pages Tutorials."
<div id="topContent">
 
<asp:HyperLink ID="lnkHome" runat="server" NavigateUrl="~/Default.aspx"
   
Text="Master Pages Tutorials" />
</div>
That's it! At this point all the URLs in our master page are properly based when rendered by a content page regardless of what folders the master page and content page are located in.

Automatic URL Resolution in the <head> Section

In the Creating a Site-Wide Layout Using Master Pages tutorial we added a <link> to the Styles.css<head> region: file in the
<head runat="server">
 
<title>Untitled Page</title>
 
<asp:ContentPlaceHolder id="head" runat="server">
 
</asp:ContentPlaceHolder>
 
<link href="Styles.css" rel="stylesheet" type="text/css" />
</head>
While the <link> element's href attribute is relative, it's automatically converted to an appropriate path at runtime. As we discussed in the Specifying the Title, Meta Tags, and Other HTML Headers in the Master Page tutorial, the <head> region is actually a server-side control, which enables it to modify the contents of its inner controls when it is rendered.
To verify this, revisit the ~/Admin/Default.aspx page and view the HTML source sent to the browser. As the snippet below illustrates, the <link> element's href attribute has been automatically modified to an appropriate relative URL, ../Styles.css.
<head>
 
<title>
 Default
 
</title>
 
<link href="../Styles.css" rel="stylesheet" type="text/css" />
</head>

Summary

Master pages very often include links, images, and other external resources that must be specified via a URL. Because the master page and content pages might not exist in the same folder, it is important to abstain from using relative URLs. While it is possible to use hard coded absolute URLs, doing so tightly couples the absolute URL to the web application. If the absolute URL changes - as it often does when moving or deploying a web application - you'll have to remember to go back and update the absolute URLs.
The ideal approach is to use the tilde (~) to indicate the application root. ASP.NET Web controls that contain URL-related properties map the ~ to the application root at runtime. Internally, the Web controls use the Control class's ResolveClientUrl method to generate a valid relative URL. This method is public and available from every server control (including the Page class), so you can use it programmatically from your code-behind classes, if needed.
Happy Programming!

No comments :