Custom SiteMapProvider Incorporates QueryString Reliance
posted on Sunday, November 27, 2005 by bobby @ 4:30 pm
This is a follow-up post related to my troubles with the SiteMap control and related provider. The original post can be found here

I emailed Scott Guthrie to ask about a solution to the problem. He directed me to Danny Chen who answered my question. It is, in fact, possible to do what I want to do and here's how:

I was onto the right path w/ the SiteMap.SiteMapResolve event. I'm not sure why I was recieving a "Request collection is unavailable" error, but it went away. The code Danny sent me would update the url of the current node and parent node with the current querystring. While this got me started, I wanted to give a little more control to the siteMapNodes in the web.sitemap file to choose which variables, if any, they are reliant on. This is an example of a siteMapNode that relies on a querystring variable to function:
Copy code to clipboard in IE or select code for Firefox
<siteMapNode url="~/hotels/details.aspx" title="Hotel Information" reliantOn="HotelID, UserID" />


The siteMapNode specified above relies on HotelID and UserID, which are variables that would be in the querystring. If they don't exist in the querystring, they are ignored.

The following code is a custom SiteMapProvider that derives from XmlSiteMapProvider.

Copy code to clipboard in IE or select code for Firefox
public class SmartSiteMapProvider : XmlSiteMapProvider
{
    public override void Initialize(string name, NameValueCollection attributes)
    {
        base.Initialize(name, attributes);
        this.SiteMapResolve += new SiteMapResolveEventHandler(SmartSiteMapProvider_SiteMapResolve);
    }

    SiteMapNode SmartSiteMapProvider_SiteMapResolve(object sender, SiteMapResolveEventArgs e)
    {
        if(SiteMap.CurrentNode == null)
            return null;

        SiteMapNode temp;
        temp = SiteMap.CurrentNode.Clone(true);
        Uri u = new Uri(e.Context.Request.Url.ToString());

        SiteMapNode tempNode = temp;
        while(tempNode != null)
        {
            string qs = GetReliance(tempNode, e.Context);
            if(qs != null)
                if(tempNode != null)
                    tempNode.Url += qs;

            tempNode = tempNode.ParentNode;
        }

        return temp;
    }

    private string GetReliance(SiteMapNode node, HttpContext context)
    {
        //Check to see if the node supports reliance
        if(node["reliantOn"] == null)
            return null;

        NameValueCollection values = new NameValueCollection();
        string[] vars = node["reliantOn"].Split(",".ToCharArray());

        foreach(string s in vars)
        {
            string var = s.Trim();
            //Make sure the var exists in the querystring
            if(context.Request.QueryString[var] == null)
                continue;

            values.Add(var, context.Request.QueryString[var]);
        }

        if(values.Count == 0)
            return null;

        return NameValueCollectionToString(values);
    }

    private string NameValueCollectionToString(NameValueCollection col)
    {
        string[] parts = new string[col.Count];
        string[] keys = col.AllKeys;

        for(int i = 0; i < keys.Length; i++)
            parts[i] = keys[i] + "=" + col[keys[i]];

        string url = "?" + String.Join("&", parts);
        return url;
    }
}


Still not sure why I get a StackOverflowException when I try to access the ChildNodes property of the root node when I override the BuildSiteMap method, but now that I have this working, I care not.

Updated 12/8/2005 1:05:10 PM

Danny Chen from Microsoft was nice enough to explain the reason behind the stack overflow expcetion I was experiencing. Click here to read that post.


CommentsComments
posted on Friday, January 06, 2006  by Ian @ 2:50 AM

Article seems good, really needs some instructions on how to use it after you create the class, add the reliantOn="DocType=3" into the sitemap node tag, how do you get the class to add the variable and the value to the query string. Any help would be greatly appreciated.

Ian
posted on Friday, January 06, 2006  by bobby @ 3:15 AM

Hey Ian,

Once the class is built, the only configuration you should have to make is setting the provider as the default site map provider. You can do this in the web.config file, or on the actual SiteMap control instance.

Once that is done, you just add the "reliantOn" attribute to your sitemap nodes. The value of the attribute is the name of the querystring variable.

Given this url: http://www.somesite.com?UserID=12345&Action=update,
My sitemap node might look like this: reliantOn="UserID". You can also provide a comma delimmited list of variables, like this: reliantOn="UserID, Action".

No other configuration should be necessary. The provider will automatically extract the values of the querystring variables from the current querystring.
posted on Wednesday, February 01, 2006  by Anonymous @ 10:09 AM

Lovely work.

However how do I maintian a UserID reliance, for example, when continuing down into child nodes.

Let's say the sitemap looke like this:

<siteMapNode title="Level1" url="~/L1.aspx" reliantOn="id" >
<siteMapNode title="Level2" url="~/L2.aspx" reliantON=id></siteMapNode>
</siteMapNode>

When accessing Level1, it works fine, but when progressing to Level2, them level1 has lost its reliance.

Edmund
posted on Wednesday, February 01, 2006  by Anonymous @ 12:28 PM

It seems I have mistaken with my initial observation posted an hour ago. The SiteMap works fine.

What confused me is the fact that I have both a SiteMap and a Menu on the same page, and I was looking at the menu links instead of the SiteMap.... :-(

Going through the code explains why the Menu does not work as. It actually looks forward from the starting node in the sitemap file, while the provider checks backward, as it should for such a bread crumb.

So, is there an easy change for a new SmartMenuSiteMapProvider that will walk forward to add the reliance?

Thanks, Edmund
posted on Thursday, February 02, 2006  by Anonymous @ 12:03 AM

Thanks for reply, but maybe I dont' understand something. I see how I can specify multiple variables to pass in the query string. Is there a way where I can set the value of the variables in the sitemap node tag. The links live in a global navigation and short cut into an existing system, so the values are not assigned on any of the pages with the navigation, thus my reasoning for wanting to assign the value in the global navigation allowing me to shortcut into the existing sytem. Thanks again for the reply, if I'm missing something obvious please point me in the right direction.

Ian
posted on Thursday, February 02, 2006  by bobby @ 12:15 AM

No, this approach does not allow you to specify the actual value for the querystring variable. The value is determined by the live querystring. Can you just specify the querystring in the actual NavigateUrl attribute?

As far as supporting the Menu control, I have not used it much and have not spent any time trying to make this approach compatible w/ it. I don't anticipate writing a version to work w/ the Menu control any time soon.
posted on Thursday, March 16, 2006  by Rick @ 11:25 AM

Forgive a newbie question, but I'm trying to implement your example code and when I run the page I get an exception thrown: "The SiteMapProvider 'SmartSiteMapProvider' cannot be found."

I created a new class file in my project, copied in the code from your post, set the SiteMapProvider attribute for my SiteMapPath and added the reliantOn attribute into my SiteMap xml file where appropriate. What glaringly obvious thing am I missing?
posted on Sunday, April 16, 2006  by bobby @ 3:40 PM

Make sure you secify the fully qualified class name for SmartSiteMapProvider in the web.config file. Be sure to include the namespace.

Other than that, it should work.
posted on Wednesday, May 24, 2006  by Trevor @ 12:18 PM

How do you add a 'fully qualified class name for SmartSiteMapProvider in the web.config file'?
posted on Friday, August 04, 2006  by Mario Delicata, UK @ 7:22 AM

Hi, in respect to your previous question -:

"No, this approach does not allow you to specify the actual value for the querystring variable. The value is determined by the live querystring. Can you just specify the querystring in the actual NavigateUrl attribute? "

The answer is yes you can and its really simple just set out the url field within the sitemap as you usually would and just replace the '&' characters for 'amp;' in the url field and the static querystring values will be there as always.

If anybody is looking for the VB version you can find it below, just paste it into a .vb file and it will autoformat:

Copy code to clipboard in IE or select code for Firefox
Public Class SmartSiteMapProvider
    Inherits XmlSiteMapProvider

    Public Overrides Sub Initialize(ByVal name As String, ByVal attributes As NameValueCollection)

        MyBase.Initialize(name, attributes)
        Dim resolveHandler As New SiteMapResolveEventHandler(AddressOf SmartSiteMapProvider_SiteMapResolve)
        AddHandler Me.SiteMapResolve, resolveHandler

    End Sub

    Function SmartSiteMapProvider_SiteMapResolve(ByVal sender As Object, ByVal e As SiteMapResolveEventArgs) _
    As SiteMapNode

        If (SiteMap.CurrentNode Is Nothing) Then Return Nothing

        Dim this As New XmlSiteMapProvider

        Dim temp As SiteMapNode
        temp = SiteMap.CurrentNode.Clone(True)
        Dim u As Uri = New Uri(e.Context.Request.Url.ToString())

        Dim tempNode As SiteMapNode = temp
        While Not tempNode Is Nothing

            Dim qs As String = GetReliance(tempNode, e.Context)

            If Not qs Is Nothing Then

                If Not tempNode Is Nothing Then

                    tempNode.Url += qs

                End If

                tempNode = tempNode.ParentNode
            End If

        End While

        Return temp

    End Function

    Private Function GetReliance(ByVal node As SiteMapNode, ByVal context As HttpContext) As String

        'Check to see if the node supports reliance
        If node("reliantOn") Is Nothing Then Return Nothing

        Dim values As NameValueCollection = New NameValueCollection
        Dim vars() As String = node("reliantOn").Split(",".ToCharArray())

        Dim s As String
        For Each s In vars

            Dim var As String = s.Trim()
            'Make sure the var exists in the querystring
            If context.Request.QueryString(var) Is Nothing Then Continue For

            values.Add(s, context.Request.QueryString(var))

        Next

        If values.Count = 0 Then Return Nothing

        Return NameValueCollectionToString(values)

    End Function

    Private Function NameValueCollectionToString(ByVal col As NameValueCollection) As String

        Dim parts(col.Count) As String
        Dim keys() As String = col.AllKeys

        Dim i As Integer
        For i = 0 To keys.Length
            parts(i) = keys(i) + "=" + col(keys(i))
        Next

        Dim url As String = "?" + String.Join("&", parts)

        Return url

    End Function

End Class

posted on Monday, August 21, 2006  by Dinia Oussama  @ 6:42 AM

in web.config code looks like the following :
<siteMap defaultProvider="SmartSiteMapProvider" enabled="true">
<providers>
<clear />
<add name="SmartSiteMapProvider" type="SmartSiteMapProvider" siteMapFile="web.sitemap" securityTrimmingEnabled="true" />
</providers>
</siteMap>

this is if the SmartSiteMapProvider class is not contained in any namespaces.
for the query string in the web.sitemap file:

<siteMapNode url="ActionsList.aspx" title="Acitons List" roles="GeneralRole" reliantOn="dost_id" >

this is very nice and easy to implement.

i'll add a link to a sample app using this provider for newbies.

thanks a lot for the post, this helps.
posted on Monday, August 21, 2006  by Sticky @ 10:25 PM

in the VB version line 40

tempNode = tempNode.ParentNode

needs to go below the End If...otherwise you get an infinite loop.
posted on Wednesday, September 13, 2006  by Ciro @ 2:33 PM

How to use the SmartSiteMapProvider? Help me... Very thanks
posted on Sunday, September 17, 2006  by Steve @ 12:51 PM

I spent two days on this one. Found a good article on it here:

http://msdn.microsoft.com/msdnmag/issues/06/06/WickedCode/

His solution was to not have a site map node for your leaf node but to create it in the 'HandleUnmappedNodes' he creates. The problem with his solution is that he appends the leaf node to the root node so that your sitemappath looses its ancestor hierarchy. I tried getting a reference to the parent node of the current node and adding the new node to it but, after a few naviagtion moves, the current node got destroyed somehow and my code went all goofy. So I just did the simple thing and it worked:

Copy code to clipboard in IE or select code for Firefox
public static SiteMapNode HandleUnmappedNodes(object sender, SiteMapResolveEventArgs e)
{
    HttpContext context = HttpContext.Current;
    SiteMapNode currentNode = SiteMap.CurrentNode.Clone(true);
    SiteMapNode tempNode = currentNode;

    // Change title for leaf nodes.
    if (context.Request.Path.ToLower().Contains("showarticle.aspx"))
    {
        string param = context.Request["QueryStringAppendage"];
        string title = String.IsNullOrEmpty(param) ?
            "Unmapped Page" : param;
        currentNode.Title = title;
        return currentNode;
    }

    return null; // Do nothing for other URLs.
}

posted on Tuesday, September 19, 2006  by sparrow @ 9:30 AM

In the GetReliance() method, be sure to change the line:

values.Add(s, context.Request.QueryString[var]);

to:

values.Add(var, context.Request.QueryString[var]);
posted on Tuesday, September 19, 2006  by bobby @ 9:38 PM

Done. Good catch.
posted on Friday, October 06, 2006  by vilo @ 8:41 AM

Hi guys, good job with the class. I have a case where i get to a page - Site.aspx?sid=123, then the user clicks on another page - Ref.aspx?rif=345, then another - Con.aspx?678. How can I take advantage of your class to make this work. I tried using it, but had no luck. Any help is deeply appreciated.
posted on Tuesday, January 02, 2007  by Janak Darji @ 2:19 AM

Hi All
I implemented this solution which is related to passing query string with sitemap. I copied the class SmartSiteMapProvider and do neccessary changes in web.config also
now at the time of loading the page where i implemented sitemap control it is going thru the SmartSiteMapProvide and that is showing me the node with the url+querystring but the effect of that querystring is not reflected in actuall sitemap control.

please give me the solution if anybody have
posted on Wednesday, February 21, 2007  by -Ken @ 11:00 PM

You might alter your Function NameValueCollectionToString as the following...I got an "out of bounds error" when I plugged the Class in and only used one querystring. I changed the parts(col.Count) and keys.Length functions to index length-1 and it no longer gives me an error (or aestheticallydoes not put an & on a part who's value is nothingthus leaving my url clean without a trailing &).

Thanks for your great work and help!

-Ken

Private Function NameValueCollectionToString(ByVal col As NameValueCollection) As String

Dim parts(col.Count - 1) As String
Dim keys() As String = col.AllKeys

Dim i As Integer

For i = 0 To keys.Length - 1
parts(i) = keys(i) + "=" + col(keys(i))
Next

Dim url As String = "?" + String.Join("&", parts)

Return url

End Function
posted on Thursday, April 12, 2007  by Steve @ 4:53 PM

This is great. However, I noticed that your SmartSiteMapProvider does not permit the sitemapFile parameter to be anything other than "web.sitemap". I originally has this set to "Application.sitemap" and it seemed to be ignored when the provider ran the "SiteMap.CurrentNode" request. Is there a simple fix to this?
posted on Wednesday, July 04, 2007  by Slava @ 12:15 AM

Don't understand how that code could ever work if in method GetReliance() this line of code seems to be incorrect:

if (node["ReliantOn"] == null)
return null;

node["ReliantOn"] - is a custom attribute from the Attributes collection of SiteMapNode. But the Attributes collection of SiteMapNode could be set either in constructor of SiteMapNode or explicitly assigned. You never do this in your code, thus node["ReliantOn"] will be always null.

Or may be I'm mistaken and if you add any attributes to SiteMapNode of web.sitemap they are automatically added to SiteMapNode custom attributes?
posted on Tuesday, July 10, 2007  by Marco @ 5:24 AM

Wonderful job! I love it! You solved me a lot of problems.

Congratulations, Marco from Italy
posted on Wednesday, September 05, 2007  by Joe @ 7:13 AM

I'd had this same problem last year and solved it by modifying the node url strings. I wasn't proud of it but it worked. This is much better and am determined to make it work. Right now however, my SiteMapPath is not passing the querystrings to my pages. Can anyone help please?

I have a simple web site that walks down a hierarchy of Orders > Order > SKU. Each of these pages load as content into a MasterPage. The Order page is reliant on an OrderNum querystring variable. So, in the load event of the Order page, I have:

Copy code to clipboard in IE or select code for Firefox
string ordNum = Request.QueryString["OrderNum"];


I have placed a SiteMapPath control in the "header" area of the master page and would like it to navigate the "breadcrumb" pages using the querystrings.

I have added the SmartSiteMapProvider.cs class in the App_Code folder of my project. I have also set the SiteMapProvider property of the SiteMapPath control to SmartSiteMapProvider.

In my Web.config, I have:

<?xml version="1.0"?>
<configuration>
...
<system.web>
<siteMap defaultProvider="SmartSiteMapProvider" enabled="true">
<providers>
<clear />
<add name="SmartSiteMapProvider" type="SmartSiteMapProvider" siteMapFile="Web.sitemap" securityTrimmingEnabled="true"/>
</providers>
</siteMap>
...
</system.web>
...
</configuration>

In my Web.sitemap, I have:

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
<siteMapNode url="http://www.csharper.net/Orders.aspx" title="Orders" description="">
<siteMapNode url="http://www.csharper.net/Order.aspx"
title="Order"
reliantOn="OrderNum"
description="">
<siteMapNode url="http://www.csharper.net/SKU.aspx"
title="SKU"
reliantOn="SKU"
description="">
</siteMapNode>
</siteMapNode>
</siteMapNode>
</siteMapNode>
</siteMap>
posted on Sunday, October 07, 2007  by Guillermo G. @ 10:02 PM

Thanks for sharing your code (and knowledge of course) , great job!.
Similar to Marco you bring to me a solution that solved a little problem with the traditional websitemap managing and preserving querystrings accross website navigation.

Guillermo G. from Medellin, Colombia
posted on Monday, November 05, 2007  by Martin J. @ 1:41 AM

HI!
nice piece of code, but could possibly behave really weird if you want use two sitemap's xmls, and switching between them... be sure you replace
SiteMap.CurrentNode.Clone(true);
to e.Provider.CurrentNode...

and

if( SiteMap.CurrentNode == null )
to
if( e.Provicer.CurrentNote == null) ....

hope this helps someone
posted on Wednesday, November 07, 2007  by Guillermo G. @ 5:14 PM

I'm using this code, but it's having a problem, when the root siteMapNode have an external URL (to another application or an Internet site) isn't appearing in the SiteMap Control. If I change the URL of the root SiteMapNode to an internal (example a page in my application) it works OK.

Can anybody help me to repair this issue?

Thanks in advance.
posted on Wednesday, November 07, 2007  by Guillermo G. @ 7:53 PM

AutoAnswer .... simply changing securityTrimmingEnabled="false" ... enable "external" SiteMapNodes.

:)
posted on Sunday, December 02, 2007  by Vai2000 @ 6:43 AM

<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0">
<siteMapNode url="default.aspx" title="Home" description="" >
<siteMapNode url="ProductListing.aspx" title="Side Panels" description="Side Panels" reliantOn="searchKey" >
<siteMapNode url="Customize.aspx" title="Customize" description="Customize your selection" reliantOn="id,img" />
</siteMapNode>
<siteMapNode url="ProductListing.aspx" title="Hoods" description="Hoods" reliantOn="searchKey" >
<siteMapNode url="Customize.aspx" title="Customize" description="Customize your selection" reliantOn="id,img" />
</siteMapNode>
</siteMapNode>
</siteMap>
I am getting error:
Mutliple nodes with the sameUrl were found! XmlSiteMapRovider requires that sitemap nodes have unique URLS!!!!!
posted on Sunday, January 20, 2008  by Richard H @ 1:53 PM

Vai2000:

You have redundance of the Customize.aspx reference.
Remove one of them and it should work.

Sincerely

Richard
posted on Friday, February 29, 2008  by Denis @ 10:12 AM

Thank you very much for this article.

I have a menu that shows selected node together with its siblings, and I noticed that the siblings disappeared after I started to use SmartSiteMapProvider. I figured, that happens because when we use the Clone method of SiteMapNode it doesn't clone the whole tree of nodes, but just make up reference to the parents of the clonned node.

I questioned why do we need to clone the node anyway, and after some playing around came up with a different version of SmartSiteMapProvider_SiteMapResolve procedure that worked better for me. Here it is:

Copy code to clipboard in IE or select code for Firefox
SiteMapNode SmartSiteMapProvider_SiteMapResolve(
    object sender, SiteMapResolveEventArgs e)
{
    if (SiteMap.CurrentNode == null) return null;

    RemoveQuery(SiteMap.RootNode);
    SiteMapNode tempNode = SiteMap.CurrentNode;
    while (tempNode != null)
    {
        string qs = GetReliance(tempNode, e.Context);
        if (qs != null)
        {
            tempNode.ReadOnly = false;
            tempNode.Url += qs;
            tempNode.ReadOnly = true;
        }
        tempNode = tempNode.ParentNode;
    }
    return SiteMap.CurrentNode;
}

private void RemoveQuery(SiteMapNode parent)
{
    int pos = parent.Url.IndexOf('?');
    if (pos != -1)
    {
        parent.ReadOnly = false;
        parent.Url = parent.Url.Remove(pos);
        parent.ReadOnly = true;
    }
    foreach (SiteMapNode child in parent.ChildNodes)
    {
        RemoveQuery(child);
    }
}


Thanks again,
Denis.
posted on Sunday, March 02, 2008  by Roman @ 6:32 PM

Hi!
I want PictureViewer.aspx?author=Aor&imgID=2 to show "Pictures Aor"
and PictureViewer.aspx?author=Roman&imgID=2 to show "Pictures Roman"
How can it be done?
I'm try to use following code, but it doesn't work:
<siteMapNode url="PictureGallery.aspx?author=Aor" title="Gallery of Aor">
<siteMapNode url="PictureViewer.aspx?author=Aor" title="Pictures Aor" reliantON="imgID"/>
</siteMapNode>
<siteMapNode url="PictureGallery.aspx?author=Roman" title="Gallery roman">
<siteMapNode url="PictureViewer.aspx?author=Roman" title="Pictures roman" reliantON="imgID"/>
</siteMapNode>
posted on Wednesday, March 19, 2008  by kanika @ 1:30 PM

Hi I dont' know if this post is relevant for my question, but you guys seems to work lot with sitemap.
My problem is For onw of my menu item, i need to ckeck if the user is coming for the first time then some page is displyed else soem other page is displayed.
e.g if have group as a menu item then same menu item will have two URLs and depending upon the some condition one will be binded.
I am able to do the binding and it workd fine. But i am stuck as i can have only one entry again that menu item in sitemap file. Now when other url comes it doen not dispy the page as i wanted. As if i add two nodes for that menu item then again erorr is thrown. Can you help
posted on Wednesday, March 26, 2008  by Carla @ 7:02 AM

Hi there, this is awesome.
Worked wonders for me, took me forever to find a solution, but its perfect.

Thanks so much Bobby and Danny Chen, you guys are the bomb
posted on Wednesday, May 07, 2008  by Tyrone @ 6:38 PM

Worked for me as well. Great Job
posted on Wednesday, June 04, 2008  by Jason N. Gaylord @ 8:50 PM

I've made the above modifications and converted it to VB.net for a project I was working on. Good suggestion and thanks for posting the original. Here is my posting:

http://tinyurl.com/5mfju9
posted on Sunday, November 16, 2008  by Jonas @ 2:42 AM

Hi,

This code is great, it's working perfect!!!!

Thanks for sharing.

Jonas
posted on Thursday, June 25, 2009  by Kerry @ 10:45 AM

Two questions if I may...

In the SiteMapResolve method, why do you create an instance of an Uri (u) and not use it? Is this call needed?

In the while loop in the same method, why do you check for "tempNode != null" in the middle of each loop? Isn't the check at the beginning of the loop sufficient?


New Post Notification

Search Posts

Recent Posts


About Meeself
People call me Bobby DeRosa
I live somewhere in San Diego, CA
MCSD, MCAD, MCP

This theme was adapted from fUnique by fahlstad        Icons by FamFamFam        XHTML 1.0 Strict; tuned for Mozilla-powered browsers

Admin Login Administrator Login
Invalid login attempts are logged.
  Username:
  Password: