MOSS 2007 WCM Development Part 1 – Custom Breadcrumb control

September 28, 2006 at 11:45 am 30 comments

In the course of working with Beta 2 and now Beta 2 TR, we ran into a few development challenges. Some are easily fixable, some requires more thoughts. In this series of blog entries, I will attempt to document the challenges we ran into and any solutions we devised on the way.

Variation Feature in MOSS

The variation feature in MOSS is created to address symmetrical site hierarchy structure for multiple languages or multiple devices. For instance, if a site needs to provide content in both English and French languages, the variation feature in MOSS can be used to provide:

  • Variable label creation, this sets up labels to be used in a site structure to logically separate the content for different languages.
  • Variation hierarchy creation, this takes an existing hierarchy that you specify (for example from the “/” of the site) and move it under a source label structure, then replicate the same structure to the target label(s).
  • Workflow for content publishing that works with variation labels.

I am not going to go into the details of the variation feature, you can find out all about them from the MSDN site or download the SDK.


Challenge of the default Site Map Path Control

The challenge we ran into after setting up variation labels is that the variation labels are always displayed on the default out-of-the-box site map path control (aka breadcrumb). For example, if the source variation label is “English” and the target variation label is “French”, when a user navigates to the root of the site, the user will automatically be redirected to the proper variation label site. This is done via the Variation Root logic which will detect the locale via the browser, and if the locale matches one of the variation labels, the user will be redirected to that label. And if no variation labels match the locale, then the user will be redirected to the source label. All this is fine except the variation is a site level being created under root, the page that the user is redirected to is no longer at the root of the site. That is, if a user is redirected to the “Français” site, the hierarchy being displayed on the site map will be:

french01.jpg

This is not a desirable condition because:

  • The user wanted to go to the home page of the site, but instead the user is redirected to a sub site under root.
  • The extra variation site structure does not add value to the browsing experience
  • The default.aspx page at the root level can not be navigated to because of the variation root redirect behaviour, and displaying the Home link on the site map which redirects the users back to the variation label level (e.g. Français) can be confusing.

The desired functionality of the site map is simply to skip the rendering of the variation labels so that the net effect of a user navigating to the root will just be silently redirected to the variation label site, but not giving the user a visual clue the actual hierarchy level they are at. That is, if a user is to be redirected to the Français variation site, the site map path should still only render:

french02.jpg

as the current site level to the user because that is the context the user is expecting.


The Solution

To do this I created a custom site map path web control that:

  1. Can trim the variation labels (or any node name for that matters) using the “IgnoreNodeName” attribute.
  2. Can specify the separator string using “Separator” attribute. For example: ” >> “.
  3. Can specify the root node image url using “RootNodeImageUrl” attribute so that we don’t have to render the root collection name.
  4. Can specify not to render the current location as a link using “RenderCurrentNodeAsLink” attribute. When set to false, the current node will be rendered as a label.
  5. Can specify a CSS Style for the current node item using “CurrentNodeStyleCssClass” attribute.

Some of the features are already in the ASP.NET Site Map Path control. The key new features are the first 3 features. The result is a site map path control that allows me to specify which node to skip rendering, as well as giving enough control over how the site map path is rendered.


Development Method

Since MOSS is built on top of ASP.NET 2.0, a web custom control written for an ASP.NET 2.0 web site will also work inside MOSS. To ease development, I first set up an ASP.NET web site that mimics the MOSS 2007 site structure and file structure. Then, I created the web custom control and tested and debugged it inside the ASP.NET web site before deploying it to MOSS. The pieces include:

  • Create a new web site using VS2005.
  • Set up a master page to be used with the content pages, MOSS 2007 makes extensive use of master pages.
  • Set up some sample ASPX pages that use the master page file.
  • Set up a web.sitemap file as the Site Map data source, referencing the sample ASPX pages just created.
  • Create the custom site map path control in a separate project.
  • Register the custom site map path DLL with the master page, and place the control on the master page.
  • Assign values to the different attributes to test the behaviours. Debug then test again.

Once the custom control is working in an ordinary ASP.NET web site, it is time to test it in the MOSS environment. Here are the steps I use:

  1. Copy the DLL of the custom control to the GAC of the MOSS server.
  2. Edit the web.config file of the SharePoint application and add the custom control to the SafeControlList.
  3. Use SharePoint Designer, open a Master Page and save it using a different name to avoid messing up the out-of-the-box master page.
  4. Add a Page directive to register the assembly to the master page and define a tag name.
  5. Add the control to the page by using the defined tag name:control name syntax.
  6. Save the master page and check it in as a major version, and approve the changes to make it public.
  7. Navigate to a site in your MOSS application. Click on Site Action and select Settings | Modify all settings.
    modifyallsitesettings.jpg
  8. Click Master page link under the Look and Feel Group
    masterpage.jpg
  9. Select the newly saved master page, and select the Reset all subsites to inherit this Site Master Page setting checkbox.
    selectmasterpage1.jpg
  10. Click OK to apply the master page.

If all steps are performed successfully, you will see your site having the newly added custom control on the page.

The Result

To illustrate the point, I edited the master page so that the new custom site map path and the original site map path are displayed one on top of the other. As you can see, the top custom site map path is a lot more concise and does not display the unnecessary variation label.

compare.jpg

Updates:
You can view the source code of the custom site map path control:


using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace KamLau.WebControls
{
  [DefaultProperty("RenderCurrentNode")]
  [ToolboxData("")]
  public class CustomBreadcrumb : WebControl
  {
    # region Properties

    /// 
    /// This attribute is created to accomodate using an image (like a house for example) to represent the root node.
    /// If no value is specified, the textual representation of the root node is used.  Otherwise the image is displayed.
    /// 
    private const string defaultRootNodeImageUrl = "";
    private string rootNodeImageUrl = defaultRootNodeImageUrl;
    [Category("Appearance")]
    [DefaultValue(defaultRootNodeImageUrl)]
    [Description("A url of an image to be displayed instead of a text label/hyperlink for the root node.")]
    [Browsable(true)]
    public string RootNodeImageUrl
    {
      get { return rootNodeImageUrl; }
      set { rootNodeImageUrl = value; }
    }

    /// 
    /// The string that will be used as the separator of the breadcrumb nodes.  If no value is specified, ">" will be used.
    /// 
    private const string defaultSeparator = ">";
    private string separator = defaultSeparator;
    [Category("Appearance")]
    [DefaultValue(defaultSeparator)]
    [Description("A string used as separators between breadcrumb nodes.")]
    [Browsable(true)]
    public string Separator
    {
      get { return separator; }
      set { separator = value; }
    }

    /// 
    /// A boolean to indicate if the current node should be rendered as a hyperlink.  Default value for this property is false.
    /// 
    private const bool defaultRenderCurrentNodeAsLink = false;
    private bool renderCurrentNodeAsLink = defaultRenderCurrentNodeAsLink;
    [Category("Appearance")]
    [DefaultValue(defaultRenderCurrentNodeAsLink)]
    [Description("Indicates if the current node should be rendered as hyperlink.")]
    [Browsable(true)]
    public bool RenderCurrentNodeAsLink
    {
      get { return renderCurrentNodeAsLink; }
      set { renderCurrentNodeAsLink = value; }
    }


    private const string defaultCurrentNodeStyleCssClass = "";
    private string currentNodeStyleCssClass = defaultCurrentNodeStyleCssClass;
    [Category("Appearance")]
    [DefaultValue(defaultCurrentNodeStyleCssClass)]
    [Description("The CSS style to use with the current node.")]
    [Browsable(true)]
    public string CurrentNodeStyleCssClass
    {
      get { return currentNodeStyleCssClass; }
      set { currentNodeStyleCssClass = value; }
    }
	
    
    /// 
    /// A string with comma delimited values which will be ignored when the breadcrumb is rendered.  This is especially useful
    /// for trimming the variation labels in MOSS2007, or if any level of the hierarchy in the navigation needs to be skipped.
    /// 
    private const string defaultIgnoreNodeName = "";
    private string ignoreNodeName = defaultIgnoreNodeName;
    [Category("Appearance")]
    [DefaultValue(defaultIgnoreNodeName)]
    [Description("The substrings (name of navigation nodes) in this comma delimited string will not be rendered on the breadcrumb.")]
    public string IgnoreNodeName
    {
      get { return ignoreNodeName; }
      set { ignoreNodeName = value; }
    }


    private const string defaultSiteMapProvider = "AspnetXmlSiteMapProvider";
    private string siteMapProvider = defaultSiteMapProvider;
    [Category("Appearance")]
    [DefaultValue(defaultSiteMapProvider)]
    [Description("Specify the name of the current site map provider")]
    public string SiteMapProvider
    {
      get { return siteMapProvider; }
      set { siteMapProvider = value; }
    }
	
    
    #endregion 
    
    
    Stack LinkStack = new Stack();
    
    /// 
    /// Create HyperLink objects and pop to the stack, recursively going up the hierarchy
    /// 
    /// 
    /// 
    private void TraverseUp(SiteMapNode currentNode, Stack linkStack)
    {
      if (currentNode != null)
      {
        HyperLink currentLink = new HyperLink();
        currentLink.Text = currentNode.Title;
        currentLink.NavigateUrl = currentNode.Url;
        currentLink.Attributes.Add("alt", currentNode.Description);
        linkStack.Push(currentLink);
        if (currentNode.ParentNode != null)
        {
          TraverseUp(currentNode.ParentNode, linkStack);
        }
      }
    }
    
    protected override void RenderContents(HtmlTextWriter output)
    {
      TraverseUp(SiteMap.CurrentNode, LinkStack);
      bool isRoot = true;
      while(LinkStack.Count > 0)
      {
        Label spanTag = new Label(); // use label to wrap  tags around nodes.
        HyperLink nodeLink = (HyperLink) LinkStack.Pop(); // use a stack (FILO) to reverse the order of traversing up the hierarchy

        if (!IsIgnore(nodeLink.Text))
        {
          if (!isRoot)
          {
            Label separatorLabel = new Label();
            separatorLabel.Text = Separator;
            separatorLabel.RenderControl(output);
          }
          if (LinkStack.Count == 0 && !RenderCurrentNodeAsLink)
          {
            if (RootNodeImageUrl != String.Empty && isRoot)
            {
              Image rootNodeImage = new Image();
              rootNodeImage.ImageUrl = RootNodeImageUrl;
              rootNodeImage.RenderControl(output);
            }
            else
            {
              spanTag.Text = nodeLink.Text;
              spanTag.CssClass = CurrentNodeStyleCssClass;
              spanTag.RenderControl(output);
            }
          }
          else
          {
            if (RootNodeImageUrl != String.Empty && isRoot)
            {
              nodeLink.ImageUrl = RootNodeImageUrl;
            }
            spanTag.Controls.Add(nodeLink);
            spanTag.RenderControl(output);
          }
        
          isRoot = false;
        }
      }
    }
    
    /// 
    /// Determine if the node should be ignored.
    /// 
    /// 
    /// 
    private bool IsIgnore(string nodeName)
    {
      bool isIgnore = false;
      string[] ignoreNodeNames;
      if(IgnoreNodeName != String.Empty)
      {
        char[] splitChar = new char[1];
        splitChar[0] = ',';
        ignoreNodeNames = IgnoreNodeName.Split(splitChar);

        foreach (string ignoreString in ignoreNodeNames)
        {
          if (Regex.IsMatch(nodeName, ignoreString, RegexOptions.IgnoreCase))
          {
            isIgnore = true;
            break;
          }
        }
      }
      return isIgnore;
    }
  }
}

Entry filed under: MOSS 2007, Web Custom Control.

How to remove recent projects from Visual Studio Start Page

30 Comments Add your own

  • 1. ssa  |  February 8, 2007 at 8:37 am

    Excellent post

    thanks

    ssa
    http://feeds.feedburner.com/AllAboutSharepoint

    Reply
  • 2. RauSEO  |  April 13, 2007 at 4:30 am

    Very informative article….especially explanation of developing method.

    SharePoint 2007 Experts

    Reply
  • 3. Kunal  |  June 18, 2007 at 6:33 pm

    I am getting an error

    Make sure that the class defined in this code file matches the ‘inherits’ attribute, and that it extends the correct base class (e.g. Page or UserControl).

    when i compile the code can u pls help me out

    Reply
  • 4. Loki  |  June 19, 2007 at 3:26 am

    Extremely useful, but its not working properly for me. Which “SiteMapProvider” do you specify for the web control. Pls help, am stuck with this for a long time. Tried all the Providers defined in the web.config but to no avail. The web control is displaying only the root site and nothing else.

    Reply
  • 5. aspnetcoe  |  June 19, 2007 at 9:00 am

    Hi Kumal, make sure you have added the relevant references to your project. System.Web comes to mind. If you need to discuss your problems further, please email me directly.

    Reply
  • 6. aspnetcoe  |  June 20, 2007 at 8:58 am

    Hi Loki,
    Thank you for your comments on my blog. Let me see if I can guide you to make it work.
    The CustomBreadCrumb is a web server control. So to use it, you need to do the following:
    1. It needs to be compiled as a DLL, strongly named and deployed to the GAC.
    2. When referenced in your master page file, you will add a reference to the control like:
    <@ Register TagPrefix=”WC1″ Namespace=”AspnetCoe.WebControls” Assembly=”AspnetCoe.WebControls” %>
    3. Then inside the master page file where you like to replace the existing site map path, you can use:

    <WC1:CustomBreadcrumb runat=”server” ID=”CustomBreadcrumb1″
    RootNodeImageUrl=”/PublishingImages/home.jpg”
    IgnoreNodeName=”English,Français”
    CurrentNodeStyleCssClass=”breadcrumbCurrent”/>

    I did not specify a custom site map provider. The default “CurrentNavSiteMapProvider” works fine for me.
    I would suggest you place the OOTB breadcrumb and the custom breadcrumb side by side on your master page to compare and see if there is anything else broken.
    Please email me directly should you need additional information.

    Reply
  • 7. cs96ai  |  February 12, 2008 at 8:11 pm

    Great post! Very helpful code.
    Thanks a bunch!
    CS

    Reply
  • 8. Mark  |  March 27, 2008 at 1:56 pm

    Good post but have the same problem as Loki. The control works correctly in a regular asp.net applicaiton. But incorrectly when control is in the sharepoint master page. The only problem is that it renders only the sitemap root node for all pages. A side by side siteMapPath displays the entire path.

    Reply
  • 9. Mark  |  March 28, 2008 at 5:32 am

    And now I know why it renders only the root node. It seems that at least under WSS 3.0, Sitemap.currentnode always returns the root node of the site collection. I had to alter my control to inherrit from SiteMapPath instead of WebControl and use the SiteMapPath control hierarchy as the basis for rendering. In the context of the derived SiteMapPath Control, even its Provider.currentNode is set to the root node. This is discouraging and stinks of intentional crippling of the product.

    Whether this is only true for WSS and not MOSS, I do not know.

    Reply
  • 10. Tom  |  May 27, 2008 at 4:10 am

    Hi,
    very helpul post – thanks a lot for that.
    But I’ve the same problems as Loki and the workaround from Mark didn’t resolve my problem.

    I’ve here an MOSS 2007 installation and it doesn’t matter which Provider i choose in your control, i’m only get the root node of the current web. I’ve also the OOB-Breadcrumb from sharepoint within the masterpage and this works fine. I see all the path to the current site within the OOB-Beradcrumb.

    Are there any helpful hints you can give me?

    thanks a lot
    tom

    Reply
  • 11. jeff sutherlane  |  June 2, 2008 at 6:47 am

    The control render will in my moss, the only problem is that I can not change the css styple of the current node ,no matter how I put the value like that: CurrentNodeStyleCssClass=”breadcrumbCurrent”

    Please advise me how to set the value of current node?

    Thanks

    Reply
  • 12. Rik  |  June 10, 2008 at 7:40 am

    We have company policy not to add components to the GAC, but instead i’d need to load it from the specific site’s BIN folder, could you explain to me what i’d need to do to make this work?

    now i get this error when i put the compiled dll as is in the bin folder, with trust levels set to full:

    Could not load file or assembly ‘AspnetCoe.WebControls’ or one of its dependencies. The located assembly’s manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)

    Reply
  • 13. Bob  |  July 21, 2008 at 2:14 pm

    This is a great post. I noticed today one minor issue. We have two variation labels: EN and FR and a subsite titled My Clients. Since “Clients” has EN in it, this site gets ignored. Other people might have a similar issue, so if you do it might be worthwhile to change the code to match on the whole word.

    Reply
  • 14. Rik  |  October 23, 2008 at 6:38 am

    BOB: it uses a regular expression to match, so all you need to do is add a prefix and postfix to your EN and FR:

    IgnoreNodeName=”^en$”/>

    Reply
  • 15. Rik  |  October 23, 2008 at 6:58 am

    I’d find it very usefull if i was able to remove the last page title from the breadcrumb, as some of our page titles are to long to be displayed in a breadcrumb…

    Does anyone have a solution for getting rid of the link in the breadcrumb trail, to the page you’re currently looking at ?

    Reply
  • 16. Riz  |  December 9, 2008 at 6:36 am

    Hi can you please tell me how to customize a breadcrumb to be able to view the trail for the folders that we can have on a site in MOSS e.g. Subsite > Documents > Folder A > Folder B etc.

    Thanks

    Reply
  • 17. raki  |  December 10, 2008 at 9:14 am

    hi i need to get the page name to be displayed that is if i go into subfolders in a library then the problem appears the folder names not getting diaplayed neither the list names how to get them

    Reply
  • 18. Sameer Dhoot  |  March 3, 2009 at 8:36 am

    I can not get to the page where the code is. Can you plz email me the code.

    Reply
  • 19. Babu  |  April 6, 2009 at 8:36 pm

    I can not get to the page where the code is downloadable. Can you please email me the code.

    Reply
  • 20. Siva Pittu  |  April 7, 2009 at 10:31 pm

    Cannot open the code page. Could you please email me

    Reply
  • 21. Avanti  |  April 14, 2009 at 5:03 am

    I cannot seem to open the code page.. Can you please email it to me.
    Thanks.

    Reply
    • 22. aspnetcoe  |  April 16, 2009 at 1:07 pm

      Please see the updated posting, sorry about forgetting to post the code since I moved web host.

      Reply
  • 23. bharg  |  May 5, 2009 at 10:11 pm

    hi… very good 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?

    Reply
  • 24. Ayonija  |  April 6, 2010 at 3:03 am

    this code is working fine for publishing portal.but when i try to do the same for team site,only root node is displayed.please guide me..what should i change for make it working for team site also.

    Reply
  • 25. tholm  |  July 22, 2010 at 4:33 am

    Worked like a charm and helped me develop a solution that incoorporates subfolder-structure breadcrumb! Thanks for the code!

    Reply
  • 26. Mike  |  August 3, 2010 at 11:26 am

    Thanks a lot!

    Reply
  • 27. free immigration advice  |  May 16, 2013 at 12:59 pm

    Have you ever thought about adding a little bit more
    than just your articles? I mean, what you say is important
    and all. But just imagine if you added some great photos or video clips to give your posts
    more, “pop”! Your content is excellent but with pics and videos, this blog could definitely be one of
    the most beneficial in its niche. Superb blog!

    Reply
  • 28. chronic liver cirrhosis  |  July 10, 2013 at 9:33 am

    liver cirrhosis grade 1, known to the West as” Rhazes”, who clearly distinguished it from smallpox
    and measles.

    Reply
  • 29. qnet network marketing  |  July 21, 2013 at 7:12 am

    When I originally commented I clicked the “Notify me when new comments are added” checkbox and now each time a comment is added I get three
    e-mails with the same comment. Is there any way you can remove me from that service?

    Thanks!

    Reply
  • 30. dental implants cost atlanta ga  |  June 28, 2014 at 5:21 pm

    I know this if off topic but I’m looking into
    starting my own weblog and was wondering what
    all is required to get set up? I’m assuming having a blog like yours
    would cost a pretty penny? I’m not very internet savvy
    so I’m not 100% sure. Any recommendations or advice would be greatly appreciated.

    Thank you

    Reply

Leave a comment

Trackback this post  |  Subscribe to the comments via RSS Feed


September 2006
M T W T F S S
 123
45678910
11121314151617
18192021222324
252627282930  

Feeds

Blog Stats

  • 118,535 hits