SiteMapProvider for C1

Topics: General
Nov 5, 2010 at 12:48 PM

Since i tend to prefer the SiteMap class in ASP.Net for building my navigations etc. i've quickly implemented a simple provider for C1. It works for my needs but it haven't been tested for all kinds of edge cases, multiple languages, multiple websites etc. To use this provider you would of course have to register it in your web.config under system.web/siteMap.

One important note is that since a CompositeC1SiteMapNode is created adhoc all the time, you should not use == but .Equals or (node1.Key == node2.Key) to test if two nodes are the same. Ie would (SiteMap.CurrentNode == SiteMap.CurrentNode) return false while SiteMap.CurrentNode.Equals(SiteMap.CurrentNode) will return true.

Code goes as follows and consists of two classes

public class CompositeC1SiteMapNode : SiteMapNode
{
    public CompositeC1SiteMapNode(SiteMapProvider provider, PageNode node) : base(provider, node.Id.ToString())
    {
        this.Title = node.MenuTitle;
        this.Url = node.Url;
    }

    public override string ToString()
    {
        return this.Title;
    }

    public bool Equals(CompositeC1SiteMapNode obj)
    {
        return this.Key == obj.Key;
    }

    public override bool Equals(object obj)
    {
        if (obj is CompositeC1SiteMapNode)
        {
            return Equals((CompositeC1SiteMapNode)obj);
        }

        return base.Equals(obj);
    }

    public override int GetHashCode()
    {
        return this.Key.GetHashCode();
    }

    public static bool operator ==(CompositeC1SiteMapNode a, CompositeC1SiteMapNode b)
    {
        if (Object.ReferenceEquals(a, b)) return true;
        if ((object)a == null || (object)b == null) return false;

        return a.Key == b.Key;
    }

    public static bool operator !=(CompositeC1SiteMapNode a, CompositeC1SiteMapNode b)
    {
        return !(a == b);
    }
}

public class CompositeC1SiteMapProvider : SiteMapProvider
{
    public override SiteMapNode FindSiteMapNodeFromKey(string key)
    {
        var id = new Guid(key);
        var node = getNavigator().GetPageNodeById(id);

        return new CompositeC1SiteMapNode(this, node);            
    }

    public override SiteMapNode FindSiteMapNode(HttpContext context)
    {
        var node = getNavigator().CurrentPageNode;

        return new CompositeC1SiteMapNode(this, node);
    }

    public override SiteMapNode FindSiteMapNode(string rawUrl)
    {
        throw new NotImplementedException();
    }

    public override SiteMapNodeCollection GetChildNodes(SiteMapNode node)
    {
        var pageNode = getNavigator().GetPageNodeById(new Guid(node.Key));

        var childs = pageNode.ChildPages.Select(p => new CompositeC1SiteMapNode(this, p)).ToArray();
            
        if (childs.Length == 0)
        {
            return SiteMapNodeCollection.ReadOnly(new SiteMapNodeCollection());
        }

        return SiteMapNodeCollection.ReadOnly(new SiteMapNodeCollection(childs));
    }

    public override SiteMapNode GetParentNode(SiteMapNode node)
    {
        var page = getNavigator().GetPageNodeById(new Guid(node.Key));
        var parent = page.ParentPage;

        if (parent == null) return null;

        return new CompositeC1SiteMapNode(this, parent);
    }

    protected override SiteMapNode GetRootNodeCore()
    {
        var node = getNavigator().CurrentHomePageNode;

        return new CompositeC1SiteMapNode(this, node);
    }

    private SitemapNavigator getNavigator()
    {
        SitemapNavigator navigator = null;

        var ctx = HttpContext.Current;
        if (ctx == null)
        {
            return new SitemapNavigator(new DataConnection());
        }

        navigator = ctx.Items["sitemap_navigator"] as SitemapNavigator;
        if (navigator == null)
        {
            ctx.Items["sitemap_navigator"] = navigator = new SitemapNavigator(new DataConnection());
        }

        return navigator;
    }
}

Nov 5, 2010 at 1:02 PM

Another approach is to load all nodes and maintain an internal list of SiteMapNodes in the provider, which then would have to respond to changes in the underlaying datalayer... this way you can return the same SiteMapNode-object everytime you call SiteMap.Provider.FindNodeByKey(...). It just feels a bit like double-work, since the SitemapNavigator in C1 is already doing this work.

Another approach again is to cache the returned nodes, so the same node can be returned on subsequent calls. But we would still have to listen to the datalayer so nodes can be returned from the cache when removed from the underlaying SitemapNavigator.

Any suggestions?

Dec 10, 2010 at 2:25 AM

Not much feedback here :(

I decided to go for the approach where i basically recreate the whole sitemap structure from C1's sitemapnavigator into my SiteMapProvider and flush's it whenever there is changes to the underlying datalayer by subscribing to data events. It works fine and im using it in production environments.

I uploaded the full code, customized a bit to remove dependencies from the rest of our frameworks. Find it as patch 7704 in the patch list http://compositec1.codeplex.com/SourceControl/PatchList.aspx. Use it as it is or find inspiration, and please comment if you find it useful. There is also a UrlFilterModule in there that makes the url's prettier and relies heavily on the this custom SiteMapProvider implementation. Read more about that in this thread http://compositec1.codeplex.com/Thread/View.aspx?ThreadId=237607

Dec 10, 2010 at 7:54 AM

Thanks for building and sharing this @burningice - this is definitely something we need to get build in to the core.

One little detail - DataConnection is an IDisposable so I would suggest that you ensure Dispose() is called on it by putting it into a using().

Thanks!

Dec 10, 2010 at 8:11 AM

Uhm... if you're referring to the code in my first post you'll see that there are hardly any traces of that left in the latest patch (zip file) i've submitted. In there i'm making sure to use the dataconnection in a using()-block (CompositeC1SiteMapProvider.cs line 46)