Using your SharePoint site Master Page on all application pages

So I found this article, http://weblogs.asp.net/soever/archive/2006/11/14/SharePoint-2007_3A00_-using-the-masterpage-from-your-site-in-custom-_5F00_layouts-pages.aspx, which explains how to use your SharePoint site’s master page when you create a custom application page in the _layouts virtual directory. At this point in time I don’t really have any custom application pages yet but I liked the idea of when I theme a site that all pages, including application pages in the _layouts virtual directory, use the same master page. By default all application pages use the application.master master page. After finding this article, http://www.odetocode.com/Articles/450.aspx, and a lot of playing around I believe I have found a solution. I created a HttpModule that on a page’s preinit, it checks to see if the page is in the _layouts virtual directory, and if so, it will assign it the master page url from the current sharepoint site. Here’s my code:

using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Configuration;
using System.Data.SqlClient;
using System.Data;
using Microsoft.SharePoint;

public class ApplicationMasterPage : 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) return;
		if (page.MasterPageFile == null) return;
		HttpContext context = HttpContext.Current;
		string currUrl = context.Request.Url.ToString();
		string basepath = currUrl.Substring(0, currUrl.IndexOf(context.Request.Url.Host) + context.Request.Url.Host.Length);
		//Use RawUrl, otherwise it will always use the root web.
		currUrl = context.Request.RawUrl;
		if (currUrl.ToLower().Contains("/_layouts/") &&
			page.MasterPageFile.ToLower().EndsWith("application.master"))
		{
			currUrl = currUrl.Substring(0, currUrl.ToLower().IndexOf("/_layouts/"));
			SPSite site = null;
			SPWeb web = null;
            try
            {
				site = new SPSite(basepath + currUrl);
				web = site.OpenWeb(currUrl);

				if (String.IsNullOrEmpty(web.MasterUrl) == false)
	            {
					page.MasterPageFile = web.MasterUrl; 
					page.Load += new EventHandler(page_Load);
				}
			}
            finally
            {
                if (web != null)
                {
                    web.Dispose();
                }
                if (site != null)
                {
                    site.Dispose();
                }
            }
		}
	}

	public void Dispose()
	{
	}

	void page_Load(object sender, EventArgs e)
	{
		Page page = sender as Page;
		if (page == null) return;

		try
		{
			ContentPlaceHolder placeHolderTitleBreadcrumb = page.Master.FindControl("PlaceHolderTitleBreadcrumb") as ContentPlaceHolder;
			SiteMapPath contentMap = placeHolderTitleBreadcrumb.FindControl("ContentMap") as SiteMapPath;
			contentMap.CssClass = "ms-hidden";
		}
		catch { }

		try
		{
			ContentPlaceHolder placeHolderSearchArea = page.Master.FindControl("PlaceHolderSearchArea") as ContentPlaceHolder;
			placeHolderSearchArea.Visible = false;
		}
		catch { }
	}

}

Be sure to register the HttpModule in the web.config of your sharepoint site.

This did require me to add extra contentplaceholders into my default.master. Here are the two extra placeholders I had to add:

PlaceHolderPageDescriptionRowAttr
PlaceHolderPageDescriptionRowAttr2

To add these placeholders into my default.master I replaced the following code:

<PlaceHolder id="MSO_ContentDiv" runat="server">
<table id="MSO_ContentTable" width=100% height="100%" border="0" cellspacing="0" cellpadding="0" class="ms-propertysheet">
<tr>
<td class='ms-bodyareaframe' valign="top" height="100%">
<A name="mainContent"></A>
<asp:ContentPlaceHolder id="PlaceHolderPageDescription" runat="server"/>
<asp:ContentPlaceHolder id="PlaceHolderMain" runat="server">
</asp:ContentPlaceHolder>
</td>
</tr>
</table>
</PlaceHolder>

with

<PlaceHolder id="MSO_ContentDiv" runat="server">
<table id="MSO_ContentTable" width=100% height="100%" border="0" cellspacing="0" cellpadding="0" class="ms-propertysheet">
<TR valign="top" <asp:ContentPlaceHolder id="PlaceHolderPageDescriptionRowAttr" runat="server"/> >
<TD class="ms-descriptiontext" width="100%">
<asp:ContentPlaceHolder id="PlaceHolderPageDescription" runat="server"/>
</TD>
</TR>
<TR <asp:ContentPlaceHolder id="PlaceHolderPageDescriptionRowAttr2" runat="server"/>>
<TD ID=onetidMainBodyPadding height="8px"><IMG SRC="/_layouts/images/blank.gif" width=1 height=8 alt=""></TD>
</TR>
<tr>
<td class='ms-bodyareaframe' valign="top" height="100%">
<A name="mainContent"></A>
<asp:ContentPlaceHolder id="PlaceHolderMain" runat="server">
</asp:ContentPlaceHolder>
</td>
</tr>
</table>
</PlaceHolder>

I also had to change my default breadcrumbs code in the default.master to match the application.master. I replaced the following code:

<asp:contentplaceholder id="PlaceHolderTitleBreadcrumb" runat="server">
	<asp:sitemappath id="ContentMap" runat="server" sitemapprovider="SPContentMapProvider" skiplinktext="" nodestyle-cssclass="ms-sitemapdirectional"></asp:sitemappath> &nbsp;
</asp:contentplaceholder>

with

<asp:contentplaceholder id="PlaceHolderTitleBreadcrumb" runat="server">
	<asp:SiteMapPath SiteMapProvider="SPXmlContentMapProvider" SkipLinkText="" NodeStyle-CssClass="ms-sitemapdirectional" runat="server"/>

	<asp:SiteMapPath SiteMapProvider="SPContentMapProvider" id="ContentMap" SkipLinkText="" NodeStyle-CssClass="ms-sitemapdirectional" runat="server"/>
</asp:contentplaceholder>

————————————————————————————————–

Update (02/23/08): I’ve changed my default.master and my httpmodule around a lot since posting this. If you don’t compile a dll you will need to place the ApplicationMasterPage.cs file in a App_Code directory of your root site and in a App_Code directory at C:Program FilesCommon FilesMicrosoft SharedWeb Server Extensions12TEMPLATEIMAGES. You can download my latest files below:

default.master

ApplicationMasterPage.cs

————————————————————————————————–

All of the code and changes ended up with a site settings page that looks like this.

Using Features to enable Drop-Down Menus in Team Sites

I recently was trying to enable the top nav bar to be a drop down menu of all of the sub sites. I found this article http://sharingpoint.blogspot.com/2007/02/wss-v3-drop-down-menus-in-team-sites.html which told me everything I needed to know. Instead of modifying the default.master and the TopNavBar.ascx (for the application.master) to change the SPNavigationProvider to SPSiteMapProvider, I decided I wanted each teamsite to have the option and I’m reluctant from changing SharePoint system files (see http://www.graphicalwonder.com/?p=532), so I put this part into a feature. Here’s my code below:

The first file is Feature.xml

<Feature  Id="22b94164-5348-11dc-8314-0800200c9a66"
          Title="All Sites in Site Collection Navigation"
          Description="Enable all sites in the site collection top navigation bar."
          Version="12.0.0.0"
          Scope="Web"
          Hidden="false"
    DefaultResourceFile="core"
    xmlns="http://schemas.microsoft.com/sharepoint/">
    <ElementManifests>
        <ElementManifest Location="NavigationSiteSettings.xml"/>
    </ElementManifests>
</Feature>

The second file is NavigationSiteSettings.xml

<Elements
    xmlns="http://schemas.microsoft.com/sharepoint/">
    <!-- Top Nav -->
 <Control 
        Sequence="25"
        Id="TopNavigationDataSource"
  ControlClass="System.Web.UI.WebControls.SiteMapDataSource"
  ControlAssembly="System.Web, version=2.0.3600.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
        <Property Name="ID">topSiteMap</Property>        
        <Property Name="SiteMapProvider">SPSiteMapProvider</Property>
        <Property Name="ShowStartingNode">true</Property>
    </Control>
    <HideCustomAction
        Id="TopNav"
        HideActionId="TopNav"
        GroupId="Customization"
        Location="Microsoft.SharePoint.SiteSettings" />
</Elements>

I still needed to modify the default.master and TopNavBar.ascx (you don’t need to edit the TopNavBar.ascx if you are implementing my Using your SharePoint site Master Page on all application pages) so that more than one level is shown. I haven’t figured out a way around that part yet. Does anyone else know a way to override the SharePoint.AspMenu control?

————————————————————————————————————
Update (09/21/07): I have since found out that apparently the SPSiteMapProvider relies on the user having permissions to view the top-level site in the site collection. If the user does not have access to this top-level site, then they will be prompted for authentication and won’t be able to access the site. Using Reflector I was able to find the code where they are finding the top-level site without checking for permissions first.

I ended up having to write my own SiteMapProvider that did the correct security checking and trimming while still allowing access to the site.

On a side note, while I was looking into this issue, I also noticed that the Global Breadcrumbs (which also uses the SPSiteMapProvider) doesn’t do security trimming. So I wrote a SiteMapProvider for the Global Breadcrumbs as well that did security trimming. There doesn’t seem to be a delegate control for the Global Breadcrumbs like there is for the top nav so I couldn’t write a feature to override the default. So I decided to just change my default.master and in combination with Using your SharePoint site Master Page on all application pages will use my custom SiteMapProvider for the Global Breadcrumbs.

————————————————————————————————————
Update (05/19/08): Some people have asked for the source code of my custom site map providers. You can download my latest files below:

AllSitesInSiteCollectionNavTrimmed.cs

AllSitesBreadcrumbsTrimmed.cs

FYI, this may not be the most efficient code and I have noticed some caching issues with it sometimes. Other than that it seems to work great.