ASP.NET 2.0 and MCMS – Site Navigation – Part 1 (last updated: 13.01.2006)


This article replaces my earlier article about MCMS Navigation Providers which I have written while ASP.NET 2.0 was in Beta 2. After Beta 2 a couple of things have changed which prevent the earlier code from working properly with the final version of ASP.NET 2.0.


ASP.NET 2.0 provides new concepts to implement site navigation based on new server controls and the Site Map Provider concept. The controls and the provider work together and allow the implementation of a very flexible and scalable solution for site navigation.


The SiteMapProvider represents the data layer while the controls represent the presentation layer of the navigation. The SiteMapProvider provides information about the different elements in the navigation structure through SiteMapNode objects which need to be populated by the provider with the relevant information from the underlaying datasource. SiteMapNode allows to provide a unique Key, a Title, a URL and a Description. In addition during creation of the SiteMapNode a unique key for the Node has to be provided.


For MCMS the datasource is usually the channel structure. A SiteMapProvider for MCMS would have to read the information about the requested channel item from the MCMS repository and has to return a SiteMapNode object that contains the relevant information about this channel item:


     ChannelItem ci = …;
     SiteMapNode smn = new SiteMapNode(this, ci.Guid); // we use the GUID as the Key for the Node.
     smn.Url = ci.Url; 
     smn.Title = ci.DisplayName; 
     smn.Description = ci.Description;


The code above shows how to create a SiteMapNode based on a MCMS channel item. Here we are using the GUID of the channel item as unique key, the Url property for the Navigation URL. In addition we copy the Description and DisplayName properties to the Description and Title properties of the SiteMapNode object. Instead of the GUID we could also have used the Path property but as this property will later be used to retrieve the associated channel item from the repository it’s better to use the GUID as a Searches.GetByGuid method call is quicker than a Searches.GetByPath method call.


A custom SiteMapProvider for MCMS should implement at least the following methods:



  • public override SiteMapNode FindSiteMapNode(string GuidOrUrl)
    This method returns a SiteMapNode that is identified by a specific posting or channel Guid or an Url.
    Note: this method was different in my earlier version of the SiteMapProvider as the behavior was different in ASP.NET 2.0 Beta 2. In the beta the input parameter was always an URL. Now the Key value of the SiteMapNode – which represents the GUID of the MCMS object – or an Url can be is passed in.
  • public override SiteMapNode FindSiteMapNode(HttpContext context)
    This method returns a SiteMapNode for the node in the provided HttpContext.
    Note: this method is new in my SiteMapProvider implementation compared with the version in my previous article.
  • public override SiteMapNodeCollection GetChildNodes(SiteMapNode node)
    This method returns a collection of SiteMapNodes for all child objects of a given node.
  • public override SiteMapNode GetParentNode(SiteMapNode node)
    This method returns the parent node of a given node.
  • protected override SiteMapNode GetRootNodeCore()
    This method returns the root node of the Site Tree.

A SiteMapProvider can implement some more methods but only the methods above are really required for the navigation controls shipped with ASP.NET 2.0 to work correct with MCMS 2002.


A very basic SiteMapProvider for MCMS which can be used by the ASP.NET 2.0 navigation controls would look like the following:


using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using Microsoft.ContentManagement.Publishing;

namespace StefanG.SiteMapProviders
{
    public class MCMSSiteMapProvider : SiteMapProvider
    {

        // Here we copy the relevant information from the channel item into the SiteMapNode 
        // object. We are using the Display Name as the Title and the GUID of the channel item 
        // as the unique key for the SiteMapNode object
        // This allows easy lookup of the channel item in the MCMS repository.
        protected SiteMapNode GetSiteMapNodeFromChannelItem(ChannelItem ci)
        {
            SiteMapNode smn = null;
            if (ci != null)
            {
                smn = new SiteMapNode(this, ci.Guid);
                smn.Url = ci.Url;
                smn.Title = ci.DisplayName;
                smn.Description = ci.Description;
            }
            return smn;
        } 

        // Return a SiteMapNode for a given MCMS channel item identified by GUID or Url
        public override SiteMapNode FindSiteMapNode(string GuidOrUrl)
        {
            ChannelItem ci = null;
            if (GuidOrUrl.StartsWith(“{“))
                ci = CmsHttpContext.Current.Searches.GetByGuid(GuidOrUrl) as ChannelItem;
            else
                ci = EnhancedGetByUrl(CmsHttpContext.Current, GuidOrUrl);
            return GetSiteMapNodeFromChannelItem(ci);
        }


        // Return a SiteMapNode for the given HttpContext. 
        // As this context is always the current context it is fine to return the current MCMS channel item.
        public override SiteMapNode FindSiteMapNode(HttpContext context)
        {
            ChannelItem ci = CmsHttpContext.Current.ChannelItem;
            return GetSiteMapNodeFromChannelItem(ci);
        } 

        // Return a collection of SiteMapNodes for all child elements of the current channel.  
        // Here both Channels and Postings are returned. This method can be tailored to your needs.
        public override SiteMapNodeCollection GetChildNodes(SiteMapNode node)
        {
            SiteMapNodeCollection smnc = new SiteMapNodeCollection();

            Channel channel = CmsHttpContext.Current.Searches.GetByGuid(node.Key) as Channel;
            if (channel != null)
            {
                ChannelCollection cc = channel.Channels;
                cc.SortByDisplayName();
                foreach (Channel c in cc)
                {
                    smnc.Add(GetSiteMapNodeFromChannelItem(c));
                }

                PostingCollection pc = channel.Postings;
                pc.SortByDisplayName();
                foreach (Posting p in pc)
                {
                    smnc.Add(GetSiteMapNodeFromChannelItem(p));
                }
            }
            return smnc;
        } 

        // Return the SiteMapNode for the parent object of the current Node. 
        // This method is called by the SiteMapPath control (the one that can be used as Breadcrumb controls) 
        // Here we go up to the root channel. This method can be tailored to your needs  
        // if you would to return only parts of the path
        public override SiteMapNode GetParentNode(SiteMapNode node)
        {
            ChannelItem ci = CmsHttpContext.Current.Searches.GetByGuid(node.Key) as ChannelItem; 
            ChannelItem parent = null; 
            if (ci == null)  
            { 

                parent = CmsHttpContext.Current.Channel;
            } 

            else
            { 

                parent = ci.Parent;
            } 
            return GetSiteMapNodeFromChannelItem(parent);
        } 

        // Return the SiteMapNode for the root object of the tree. Here we return the root channel 
        // This method can be tailored to your needs if you would like to start at a different level.
        protected override SiteMapNode GetRootNodeCore()
        {
            Channel root = CmsHttpContext.Current.RootChannel;
            return GetSiteMapNodeFromChannelItem(root);
        }

        // Helper function to check if the “Map Channel name to Host Header name” 
        // feature is enabled or not
        private bool MapChannelToHostHeaderEnabled(CmsContext ctx) 
        { 
            return (ctx.RootChannel.UrlModePublished == “http://Channels/”); 
        } 
 
        // Replacement for the Searches.GetByUrl method as the original one
        // does not work correct with host header mapping enabled.
        // details: http://support.microsoft.com/?id=887530
        private ChannelItem EnhancedGetByUrl(CmsContext ctx, string Url) 
        { 
            if (MapChannelToHostHeaderEnabled(ctx)) 
            { 
                string Path = HttpUtility.UrlDecode(Url); 
                Path = Path.Replace(“http://”“/Channels/”); 
                if (!Path.StartsWith(“/Channels/”)) 
                    Path = “/Channels/” + 
                           HttpContext.Current.Request.Url.Host + Path; 
                if (Path.EndsWith(“.htm”)) 
                    Path = Path.Substring(0, Path.Length – 4); 
                if (Path.EndsWith(“/”)) 
                    Path = Path.Substring(0, Path.Length – 1); 
                return (ChannelItem)(ctx.Searches.GetByPath(Path)); 
            } 
            else 
                return ctx.Searches.GetByUrl(Url); 
        } 

    }
}


To use the above SiteMapProvider you need to add the code above to a C# class library project and compile it into a DLL. Then add the provider to your ASP.NET 2.0 template project web.config file as follows:


    <system.web>
        <siteMap defaultProvider=”MCMSSiteMapProvider” enabled=”true>
            <providers>
                <add name=”MCMSSiteMapProvidertype=”StefanG.SiteMapProviders.MCMSSiteMapProvider, MCMSSiteMapProvider“/>
            </providers>
        </siteMap>
    </system.web>


That’s all! Now the ASP.NET 2.0 navigation controls can use this SiteMapProvider. No further coding is required! It is possible to add multiple different SiteMapProviders to your site. This makes sense if your have different kind of controls where some items should be shown or hidden based on your business needs. E.g. one provider should only return channels with a specific custom property. To achieve this implement a second provider that checks for these properties in the GetChildNode method and bind this SiteMapProvider explicitly to your control.


ASP.NET 2.0 ships with three new controls that can be used for site navigation:



  • SiteMapPath
  • Menu
  • TreeView

The SiteMapPath control – which behaves like the Woodgrove Breadcrumb control – requires a SiteMapProvider and cannot be used without it. Just drag a SiteMapPath control on your template or channel rendering script and your MCMS bread crumb is ready – if you configured the SiteMapProvider above in your web.config.


The Menu control is a nice multi level fly out control implemented using client side javascript. This is similar to the top navigation in Woodgrove. The Tree View control is similar to the left navigation control in Woodgrove.


To use the Menu and the TreeView control with our SiteMapProvider you first need to drag a SiteMapDataSource object to your template or channel rendering script. Either explicitly configure the SiteMapProvider to be used using the SiteMapProvider property or let this property blank and the configured default provider will be used. Then drop the TreeView or Menu control to your template or channel rendering script and configure the SiteMapDataSource you dropped earlier as the datasource to use for the control.


One hint for the TreeView control: you should ensure that child nodes are populated on demand – otherwise all nodes are retrieved when the page is first requested which can slow down your MCMS site significantly if your provider enumerates the whole repository. To do this you need to set the PopulateOnDemand property in the TreeNode Databinding properties to true.


In the next article I will discuss an additional problem with the TreeView control which only occurs in the final version of ASP.NET 2.0 and not with the Beta-2 bits and which prevents the TreeView control from working correct on a MCMS template or channel rendering script.

Comments (38)

  1. Thomas van der Heijden says:

    Thanks for this. Works like a charm. One thing you should know is that "FindSiteMapNode(string GUID)" still has some issues when you set the "StartingNodeUrl" property of the SiteMapDataSource. Entering an url obviously doesn’t work, but neither does a GUID because VS.NET 2005 thinks it’s a relative path and prepends the parent path. So I ended up writing a little GUID or Url detector anyway.

  2. Stefan_Gossner says:

    Hi Thomas,

    thanks for the info! I haven’t used this property till now – that’s why I did not yet run into this issue.

    I adjusted the article now to work correct in this situation. Please have a look if the issue is resolved now.

    Cheers,

    Stefan

  3. Thomas van der Heijden says:

    Nice one. I found one other small bug. I’m consuming Sitemap data using my own controls, so this might not happen with the standard controls.

    When creating a new page based on a template that uses SiteMap controls, I found that CMS fails to find the node passed to "GetParentNode" (Searches.GetByGuid(node.Key) returns null).

    There is no check on this so the following "ci.parent" will throw an exception. Fixed it with a simple wrapper.

    Also you might want to make "GetSiteMapNodeFromChannelItem" virtual. I found it much easier to override this method in my extension then to target the other methods.

    I wrote my own extension to this class to deal with my site. Basically it folds channels that contain only a single posting into one node, so the channel looks like a posting to all sitemap controls. This is very useful when you want to apply specific security to only one posting (which you can’t without a seperate channel) without stuffing up the layout on your sitemap controls.

    Cheers,

    Thomas

  4. Stefan_Gossner says:

    I just updated this SiteMapProvider. The GetParentNode contained a bug which prevented it from working correct for the SiteMapPath control when a new Posting was created.

    Edward, thanks for this hint!

  5. Willie says:

    I was trying to get this to work, i have the DLL generated, the way i have the channels setup i have 3 different sites 2 of which i already completed i wanted to use the nav controls for the 3rd. When i put on the menu control with the data source the page loads and no errors are reported, but it appears that the page is not reading any javascript files for the menu control. I can call another page from the server not going throught he CMS engine and it works fine. In my master page i do have a robottags for CMS as well. I also tried to just use a web.sitemap provider which i switchd the default to the XmlSiteMapProvider but either way it appears the javascript files do not get redered correctly. Any advice on this matter? I will switch back to my function that creates the navigation but the nav controls will be so much easier and much more flexible then the classic method.

    FYI i am trying to do the nav on the master page and i do have the directives on the top. Everything else is working great.

    ALSO the channel name is not the same as the directory in IIS i am not sure if that makes a difference, i thought it didnt’ matter when being passed through the CMS engine but in reference to the javascript issue i am now thinking it may. I did test it by changing the name to the same but that didnt’ seem to matter.

  6. Stefan_Gossner says:

    Hi Willi,

    this sounds interesting! Please send me a mail to webmasterATstefan-gossner.de to let us follow up offline. (Please replace AT with @)

    Cheers,

    Stefan

  7. Jason Olsan says:

    Thanks for the info on that control :) It works great :)

    However, I’ve discovered that there seems to be a problem utilizing the menu control (any site map provider) when using CMS and Master Pages. I haven’t figured out why, but I thought it might be worth noting. I think that’s what Willie might be referencing (if he has Master Pages).

    Jason

  8. Jason Olsan says:

    I should state that I seem to be having a problem with the Menu control and Master Pages. It might be a fluke :)

  9. Stefan_Gossner says:

    I have found a possible reason for the problems with the menu control. It does not work correct if the following tag is included in the web.config:

    <xhtmlConformance mode="Legacy"/>

    Removing this tag resolves the issue.

    This affects as well normal ASP.NET webform projects and MCMS template projects.

  10. Jason Olsan says:

    That fixed it exactly. Thank you :)

  11. Gaya says:

    Hi Stefan,

      This can be used with ASP 2.0 but I have ASP1.1 with VS 2003. Is there a control to create the sitemap for ASP 1.1??

    Thanks,

    Gaya

  12. Stefan_Gossner says:

    Hi Gaya,

    ASP.NET 1.1 does not support site maps and does not ship with any controls that use site maps.

    You would need to reinvent the wheel.

    I would suggest to upgrade your site to ASP.NET 2.0 and VS.NET 2005.

    Cheers,

    Stefan

  13. daniel says:

    Hi Stefan,

    Is there a way for me to start the root node of the site map on a different level? I am able to make the sitemap, its just that I want the root channel to start on a different level..

    Please help..

    Thanks.

  14. Caroline says:

    Hi Daniel,

    Yes, you would want to change the GetRoodNodeCore method to something like this:

    // returns the first channel under the rootchannel

    Channel root = CmsHttpContext.Current.RootChannel.Channels[0];

    I hope this was helpfull.

    Greetings,

    Caroline

  15. daniel says:

    Hi Caroline,

    Thanks for your reply. I was thinking that this is just a parameter I have to specify to a method. Is this possible?

    Hi Daniel,

    Yes, you would want to change the GetRoodNodeCore method to something like this:

    // returns the first channel under the rootchannel

    Channel root = CmsHttpContext.Current.RootChannel.Channels[0];

    I hope this was helpfull.

    Greetings,

    Caroline

  16. Stefan_Gossner says:

    Hi Daniel,

    a provider does not expose a method to call directly. What you could do is use application settings in your web.config and then read this application settings from inside your provider.

    Cheers,

    Stefan

  17. Angel del Olmo says:

    Hi Stefan,

    The provider returns the channels in alfabetical order?

    Thanks,

    Angel

  18. Stefan_Gossner says:

    Hi Angel,

    in the code I use SortByDisplayName. So it returns first the channels in alphabetical order of the display name, then the posting in alphabetical order of the display name.

    You can adjust this to your needs by modifying the sort instructions in the GetChildNodes method.

    Cheers,

    Stefan

  19. Anonymous says:

    In the first part of this article I discussed how to implement a SiteMapProvider for an MCMS website….

  20. charles says:

    getting a missing assembly / dependencies error

  21. Stefan_Gossner says:

    Hi Charles,

    this is not sufficient information.

    Feel free to follow up with me via email.

    Thanks,

    Stefan

  22. Steve says:

    Getting following exception:

    System.FormatException: Input string was not in a correct format. at line 43:

    Line 41:         public override SiteMapNode FindSiteMapNode(HttpContext context)

    Line 42:         {

    Line 43:             ChannelItem _channelItem = CmsHttpContext.Current.ChannelItem;

    Line 44:             return GetNodeFromChannel(_channelItem);

    Line 45:         }

    I am using MCMS SP1a with ASP.NET 2.0 webproject. I have created a class library with the above MCMSSiteMapProvider. Please advice.

  23. Stefan_Gossner says:

    Hi Steve,

    MCMS SP1a does not support ASP.NET 2.0. You need to use MCMS 2002 SP2.

    Beside that: it sounds as if CmsHttpContext.Current.ChannelItem is null. Means that you added this code to a page which is neither a MCMS template nor a channel rendering script. This will not work.

    Cheers,

    Stefan

  24. Anonymous says:

    Now after MCMS 2002 SP2 has been released I would like to point you again to an article series I have…

  25. Harold says:

    I’m using CMS with the host headers mapping feature. But when I create a new page an exception is thrown. I found that the above code doesn’t detect host headers correctly for a new page. I changed the line:

    return (ctx.RootChannel.UrlModePublished == "http://Channels/&quot;);  

    into:

    return (ctx.RootChannel.UrlModePublished.StartsWith("http://Channels/&quot;);  

    and now it works fine!

    By the way: great articles on CMS and .NET 2.0!

    Cheers, Harold

  26. Stefan Goßner says:

    Hi Harold,

    thanks for the update!

    Cheers,

    Stefan

  27. Mike says:

    Stephan,

    I am implementing this as well, and similar to charles (above), I’m getting a runtime error regarding the assembly.  Here is the error:

    Could not load file or assembly ‘MCMSSiteMapProvider’ or one of its dependencies. The system cannot find the file specified.

    The only difference is I changed the namespace from "StefanG.SiteMapProviders" to one of my own.

    Thanks…

    Mike H.

  28. Stefan Goßner says:

    Hi Mike,

    sounds as if your provider DLL is not named MCMSSiteMapProvider but a different name.

    Cheers,

    Stefan

  29. Mike says:

    Thanks, Stefan.

    It didn’t occur to me until I read your response that this was to be built as a separate project.  It’s all built now, ready to consume.

    Mike

  30. Liza says:

    Hi Stefan,

    I know this is a bit out of scope, but can you perhaps point me in the right direction of how to bind a SiteMapProvider explicitly to my control?

    /Liza

  31. Stefan Goßner says:

    Hi Liza,

    the navigation controls coming with ASP.NET have a property to assign the SiteMapProvider to use.

    Cheers,

    Stefan

  32. Liza says:

    Thanks.

    I have an other problem now though. I have used your SiteMapProvider with a custom navigation control, but it seems that Urls are rendered as published – even when I’m in edit mode. When I switch to edit site, the Urls get rendered correct, but as soon as I click the first node all Urls get rendered as publish mode again which means that I can not actually navigate in edit mode. Is this expected behavior or is it my bad?

  33. kiko says:

    Hi Stefan,

    Using a SiteMapPath control, the breadcrumb always  starts at "Channels >". Even though I’ve modified GetRootNodeCore to return a first-level Channel, like /Channels/SomeSite.

    Do I need to change another method? Like GetParentNode?

  34. Stefan Goßner says:

    Hi Kiko,

    the answer is yes.

    Cheers,

    Stefan

  35. Anonymous says:

    As some of you already noticed: GotDotNet is now down and the code samples previously hosted there have

  36. Ankit says:

    Hi Stefan,

    Just needed some of your suggestion on the following query:

    I have designed a MCMS Website, and it follows the concept of Master/Content

    Page where content pages are nothing but MCMS Templates,the site was looked

    by one of our Quality Analysis guy and he came up with queries on using

    Tables instead of DIV with CSS and with a logic that a Page with DIV is

    faster than a page with Table.I have no clue about that logic but is the

    logic of having a DIV is good or a table, the reason why we used Table is to

    have a structred layout with AMC(Auhtoring Mode Container) and

    PMC(Presentation Mode Container) in one MCMS Template, can any one help me

  37. Stefan Goßner says:

    Hi Ankit,

    there are different opinions on this. Current sites usually implement div rather than table for usability reasons as sites with DIVs can easier be read by (e.g.) blind reader machines.

    Cheers,

    Stefan