Wednesday, May 16, 2007

Creating Dynamic Breadcrumbs in SharePoint ASPX Pages

If you already played with custom layouts pages in SharePoint you probably noticed that the breadcrumbs are missing. In a recent post Vincent Rothwell explains how to enable the breadcrumbs in ASPX pages placed in the LAYOUTS folder of SharePoint. Since the stock layouts pages use the SPXmlContentMapProvider, which in turn uses the layouts.sitemap file located in the _app_bin folder, if we add entries for the newly added layouts pages we'll get a rudimentary breadcrumb fixed to one level under the site node. The breadcrumb is in this format: site > custom page. This is one good solution and works fine if this format is satisfactory for your project.

However this is not helping us if we need a dynamic breadcrumb, which will change to represent the logical structure of the site. For example if the ASPX page handles a list item in a specific way, I would like the breadcrumb to properly display the breadcrumbs. For example site > list > folder > item > custom page.

One way to go is to create a custom SiteMapProvider, but this requires changes in the master pages and a bit more code. I was looking for a solution, which works with the standard applications.master page and can be used to modify the breadcrumb of any layouts page.

Another solution is to attach to the SiteMapResolveEventHandler. This is not a secret and there are many samples for regular ASPX pages (Raj thanks for pointing this out!), but in SharePoint context there are several twists, which I would like to illustrate with this sample.

First let's attach our event handler to the provider and do some parameter validation.

private SPWeb web;

private SPListItem currentItem;

protected override void OnInit(EventArgs e)

{

base.OnInit(e);

SiteMap.Providers["SPXmlContentMapProvider"].SiteMapResolve +=

new SiteMapResolveEventHandler(provider_SiteMapResolve);

string listId = Request["listId"];

string itemId = Request["itemId"];

if (string.IsNullOrEmpty(listId) && string.IsNullOrEmpty(itemId))

{

throw new Exception("Invalid item ID or list ID.");

}

else

{

web = SPControl.GetContextWeb(Context);

SPList list = web.Lists.GetList(new Guid(listId), true);

currentItem = list.GetItemById(int.Parse(itemId));

}

}

Since the SiteMapResolve event applies to all pages we want to make sure that we detach the event handler:

protected override void OnUnload(EventArgs e)

{

base.OnUnload(e);

//detach from the static SiteMapResolve event and restore localization mode

SiteMap.Providers["SPXmlContentMapProvider"].EnableLocalization = true;

SiteMap.Providers["SPXmlContentMapProvider"].SiteMapResolve -=

new SiteMapResolveEventHandler(provider_SiteMapResolve);

}

Because this event gets fired on all pages that subscribe to the event, we want to make sure that our code alters only our page. This is why we add the IsSamePage function, which can apply different criteria to determine whether our page is currently loading.

/// <summary>

/// Determines whether the two contexts are equal and, therefore,

/// whether the SiteMap.SiteMapResolve event should be fired.

/// </summary>

protected virtual bool IsSamePage(HttpContext context1, HttpContext context2)

{

//by default, the contexts are considered the same if they

//map to the same file and the same listid/itemid

return ((Server.MapPath(context1.Request.AppRelativeCurrentExecutionFilePath) ==

Server.MapPath(context2.Request.AppRelativeCurrentExecutionFilePath)) &&

(context1.Request.QueryString == context2.Request.QueryString));

}

Now lets do the real work and modify the breadcrumb. In this sample I only add the name of the list, which contains the item and the name of the item itself. A logical extension is to add the folders in the path of an item, but once you get the idea you can change the code the way it fits your needs. The event handler should return the last node in the tree. If an exception occurs, we just ignore all changes.

/// <summary>

/// Overrides the breadcrumbs with a new one in the format

/// [site]>[repository list]>[process folder]>[page title].

/// </summary>

SiteMapNode provider_SiteMapResolve(object sender, SiteMapResolveEventArgs e)

{

if (!IsSamePage(Context, e.Context) || currentItem == null)

{

e.Provider.EnableLocalization = true;

return e.Provider.CurrentNode;

}

SiteMapNode pageTitleNode;

SiteMapNode listNode;

try

{

// Trun off localization, so that the custom nodes do not get overwritten

e.Provider.EnableLocalization = false;

// Clone some node to get a valid node object and modify it.

listNode = e.Provider.RootNode.ChildNodes[0].Clone();

listNode.Url = currentItem.ParentList.DefaultViewUrl;

listNode.Title = currentItem.ParentList.Title;

listNode.ChildNodes = new SiteMapNodeCollection();

// Add as many new nodes as you need here

pageTitleNode = new SiteMapNode(e.Provider, Guid.NewGuid().ToString());

pageTitleNode.Title = Title.Text;

listNode.ChildNodes.Add(pageTitleNode);

pageTitleNode.ParentNode = listNode;

}

catch (Exception ex)

{

return e.Provider.CurrentNode;

}

return pageTitleNode;

}

There are two tricks in this code that I want to point out. The first one is that we cannot add a new root (breadcrumb) node to the provider since the property is read only and our page does not have an entry in layouts.sitemap. This is why we use any of the page nodes defined in the layouts.sitemap file (thus eliminating the need to alter layouts.sitemaps). Since the default format is site > custom page, we get the site node automatically. What we need to do next is to modify the leaf node and extend it with as many node levels as we need. The second caveat is the line where we disable provider localization. This is needed, so that the modified text does not get overwritten.

In the next lines we overwrite the URL and the text for the first node with the URL and the name of the list, which contains the item. Next we add an additional node to display the name of the item.

To make things even easier take out the class from the code-behind file and compile it in a separate assembly, then reference this class from any "List Item" page to get the same breadcrumb behavior.

And finally a link to the source code.

Dovizhdane!

7 comments:

Anonymous said...

Hi Mikhail,
Thanks for your post, it was very helpful. But when I use the code, I get the following breadcrumb:
Team Site > Products > Products > My Page instead of Team Site > Products > My Page. Do you have any idea how I can solve this, perhaps by not performing a Clone node but getting the correct child node?
Kind regards,
Karine

eddietec said...

Hi guys, in order to avoid to code the breadcrumb behavior, you have to do the following:
- Inherets the page from Microsoft.SharePoint.WebControls.LayoutsPageBase.
- Sent the list parameter in the query:
/_layous/mypage.aspx?obj={GUID},1,LISTITEM&List={GUID}

Cheers.

Anonymous said...

Mikhail,

Any chnaace you have step by step directions to implement. I have acces to files to modify, but you post leaves little information for us newbies.

Hope you can help. Thanks

Bhargavi said...

Hi Mikhail, thanks for your post, i have one requirement, generally in the MySite, the breadcrumb displays the SiteCollection Admins name, is there a way i can change that to a custom name?

regards,
Bhargavi

Unknown said...

Hi Mikhail,

Thanks for your post, it was very helpful.

I have one doubt regarding the breadcrumb, is it possible to create dynamic breadcrumbs in Site pages.

I have some site pages that are creating dynamically, but they are missing breadcrumbs!!!

Any suggestion?

Hoping your help

Thanks

Ratheesh C S

Brett Prucha said...

If you create a new SiteMapNode and set its ParentNode to e.Provider.RootNode.ChildNodes[0].ParentNode rather than cloning the node then you don't have to set EnableLocalization to false

Mikhail Dikov said...

Good point, thanks for the feedback.