Make programmatic navigation easier in ASP.NET 2.0

by zack.moore November 26, 2007 21:52

All code published in this article is published under the Microsoft Public License. See source code download for a copy of the license.

Copyright 2007 Zack Moore

This is a small project I built earlier this year that I thought I would share.
ASP.NET web pages usually consist of several pages and users transition from page to page through a few different mechanisms. The simplest method is through clicking a link. Links can appear on menus or could be placed anywhere on a page. This is an example of a self guided transition.
Users can also be transitioned to different pages programmatically. Usually, this happens when a users performs an action like clicking a command button. When this happens, the page posts back to the server and the command button event handler runs. The event handler runs some code, perhaps making a decision, and then calls Response.Redirect() or perhaps Server.Transfer().
Most ASP.NET applications probably contain this same sequence many times. The code to perform the redirect should look something like this:

string url = string.Format("~/Output.aspx?ID={0}&StartDT={1}", 
    Server.UrlEncode(id), Server.UrlEncode(StartDate.ToShortDateString())); 
Response.Redirect(this.ResolveUrl(url));

There are several problems with this code. While this is short at only 2 lines it is overly complex and repetitive. Second, the developer has to remember to URL Encode each query string parameter. The developer must also correctly type in the URL. Lastly, if a resource moves then every page that does a redirect to that resource must be edited and retested.
That's a lot for just a couple of lines of code. Wouldn't it be nicer if we could wrap some of the repetitive parts of this together and only have one place to change the URL if the page or resource moved?
As it turns out, ASP.NET 2.0 has already given us the base on which to build something to do just that. The ASP.NET Site Navigation system is very nice and it is now standard on most of the new web projects that I start. This system allows you to define the navigation of your web site and bind that information to either a standard databinding control or a specialized navigation control. You can define hierarchy and even limit which user roles can see which navigation nodes. This is a powerful framework, and we can extend it a little further.
What we want, is a system that will automatically perform URL Encoding for us, and which will allow us to redirect or transfer users to different pages without having to put the URL in each page.


The ASP.NET Site Navigation by default uses an XML file, usually with the file extension ".sitemap", to define the page locations and hierarchy. The Site Navigation system uses the provider model, so there are other providers that retrieve navigation information from other sources such as a database, however in this article we are focusing on the XmlSiteMapProvider.
Here is an example sitemap file:

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
    <siteMapNode url="~/Default.aspx" title="Home">
        <siteMapNode url="~/Details.aspx" title="Details" />
        <siteMapNode url="~/ContactUs.aspx" title="Contact Us"/>
    </siteMapNode>
</siteMap>

This sitemap defines a Default page as the root of a hierarchy containing a ‘Details’ and ‘Contact Us’ page below it. The sitemap defines a title for each page and the URL for each page. This is a minimum set of information, but each node can have more information associated with it depending on your needs.

When a menu or other control binds to this sitemap, the nodes for the correct level will be displayed. That means that you typically wouldn’t put nodes in the sitemap that you wouldn’t want displayed on a menu. Consequently, there could be many pages not represented in the sitemap, but that you may wish to be able to navigate to through code.

In order to create a solution for this, I am going to add two new attributes to the siteMapNode. The first new attribute I am going to add is a ‘visible’ attribute which can be either true or false. This will be used to tell the site map provider whether or not this node should be shown on a menu.

In order to be able to direct our navigation to a particular node, we need to be able to uniquely identify a particular node. The site map provider assigns a key for each node already, but the XmlSiteMapProvider uses the URL as the key. This doesn’t help us, so I am going to define a new attribute which I will call ‘navigatorId’. This can be any string which will be easy for you and your developers to use as an identifier for a page or resource. I could have used the already existing ‘title’, but there may be a scenario where you need more than one node to have the same title.

If you are observant, you may have realized that siteMap defines a namespace and adding new attributes to siteMapNode would violate that schema. Luckily for use, the XmlSiteMapProvider doesn’t use a validating XML Reader. However if it did, we could attempt to define a different schema which extended the default schema.

Our new site sitemap file looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
    <siteMapNode url="~/Default.aspx" title="Home" navigatorId="Default">
        <siteMapNode url="~/Details.aspx" title="Details" navigatorId="Details"/>
        <siteMapNode url="~/ContactUs.aspx" title="Contact Us" navigatorId="ContactUs"/>
        <siteMapNode url="~/ContactUsEmailSent.aspx" title="Contact Us" navigatorId="ContactUsEmailSent" visible="false"/>
    </siteMapNode>
</siteMap>

The first change is adding a ‘navigatorId’ attribute to each node. Notice that I also added a new node which has its ‘visible’ attribute set to false.

Just adding these attributes isn’t enough. In order to take advantage of this new data, we need to extend the XmlSiteMapProvider and configure our new provider in the web.config file.

In order to accomplish what we want, I could write our own site map provider and add all the features we want. But with only a few new features, we can get by simply extending the existing provider and this saves us the trouble of having to re-implement all the parts that we want to keep.

Site map providers implement a method named IsAccessibleToUser() to determine if a node should be shown. This is typically used with security trimming, but we can extend this method along with our ‘visible’ attribute to hide nodes that we don’t want to show up on a menu. Take a look at the code below.

using System;
using System.Collections.Generic;
using System.Text;
using System.Web;

namespace ZacksFiasco.Web.Navigation
{
    public class NavigatorSiteMapProvider : XmlSiteMapProvider
    {
        public override bool IsAccessibleToUser(HttpContext context, SiteMapNode node)
        {
            bool isVisible = true;
            bool rc = false;

            return rc;
        }        
    }
}

IsAccessibleToUser() takes two parameters: the current HttpContext and the SiteMapNode to test. SiteMapNode has an indexer that enumerates all of the attributes  of the node, including the custom attributes that we added. If you check the 'visible' attribute, you can determine if the node should be visible to the user or not.  You should be sure to test if the node that was passed to you is not null, that the attribute exists, and that the attribute value can be parsed into a boolean. For this implementation because this is a new attribute, if the attribute does not exists, then we are going to assume that the node is visible.

if (node != null && node["visible"] != null && bool.TryParse(node["visible"], out isVisible))
{
}

If the visible attribute exists and is true, you should not return true. For a correct implementation you should call down to the base class and return the base class's result. This allows the base class to have the final say and apply any logic that the base class implements. If the attribute does not exist, you should call down to the base class.  The only time you should return a value different than the base class is if the attribute exists, but is false. In that case, we definitely do not want to show the node and so we want to return false.

The provider code ends up looking like this

using System;
using System.Collections.Generic;
using System.Text;
using System.Web;

namespace ZacksFiasco.Web.Navigation
{
    public class NavigatorSiteMapProvider : XmlSiteMapProvider
    {
        public override bool IsAccessibleToUser(HttpContext context, SiteMapNode node)
        {
            bool isVisible = true;
            bool rc = false;

            if (node != null && node["visible"] != null && bool.TryParse(node["visible"], out isVisible))
            {
                if (isVisible)
                {
                    rc = base.IsAccessibleToUser(context, node);
                }
                else
                {
                    rc = false;
                }
            }
            else
            {
                rc = base.IsAccessibleToUser(context, node);
            }

            return rc;
        }        
    }
}

This is all we need to complete our site map provider. Just add this to your web application by inserting the following into your web.config. Just keep in mind that the value of type is "<namespace>.<type name>, <assembly>". In this example, I also turned on security trimming. This is a good example of why we call down to the base class in our custom site map provider. If we returned true without calling the base class, then the base class would not have a chance to apply security trimming and possibly limit visibility to this node.

<siteMap defaultProvider="NavSiteMapProvider" enabled="true">
    <providers>
        <add name="NavSiteMapProvider"
             description="Custom SiteMap provider."
             type="MyNavigation.MySiteMapProvider, MyNavigation"
             siteMapFile="Web.sitemap"
             securityTrimmingEnabled="true"/>
    </providers>
</siteMap>

Now that our site map provider is in place, we want to take advantage of this new functionality. We can now navigate and perform Redirects by navigatorId by searching the site map provider for the correct node and building our URL. This is very useful, but still repetitive. We can improve this by removing manual string manipulation and wrapping the code that performs the URL Encoding.

To begin, create a new class. I named my class Navigator. First, I gave Navigator a single private static method.

static string GetUrlFromKey(string navigatorId)

This method takes as a parameter a navigatorId string. Using this id, we can search all nodes in the site map provider until we find a node that matches and return that node's URL. We can search the nodes using a foreach loop like the following:

foreach (SiteMapNode node in SiteMap.Providers["AspNetXmlSiteMapProvider"].RootNode.GetAllNodes())

This loop statement uses the AspNetXmlSiteMapProvider. In the XML I provider for the web.conig I did not clear the provider list before adding our custom provider. There are reasons why we are not searching our own custom site map provider. First,since the name of the site map provider is specified in the web.config, I can't know at compile time what that name is. But I do know what the default name of the XmlSiteMapProvider is. Secondly, I want to be able to call IsAccessibleToUser() to check whether we should return this URL. If I call our custom provider, it will check the 'visible' flag which we don't want, since we want to be able to navigate to nodes that aren't visible on a menu. The solution is to use a provider that doesn't check the 'visible' flag when it evaluates accessibility. So in your loop, if the navigatorId matches and the node is accessible, then retrieve the URL from the node and return it.

We can improve this by adding to our configuration file a list of the site map providers that we wish to search. This give us the flexibility to search any and as many providers as we wish and only the providers that we wish.

static string GetUrlFromKey(string navigatorId)
{
    SiteMapNode rcNode = null;
    string rc = string.Empty;

    NavigatorSection ns = ConfigurationManager.GetSection("ZacksFiasco.Web.Navigation") as NavigatorSection;

    if (ns != null)
    {
        foreach (ProviderElement pe in ns.SiteMapProvidersToSearch)
        {
            foreach (SiteMapNode node in SiteMap.Providers[pe.SiteMapProviderName].RootNode.GetAllNodes())
            {
                if (node.IsAccessibleToUser(HttpContext.Current) && node["navigatorId"] == navigatorId)
                {
                    rcNode = node;
                    break;
                }
            }

            if (rcNode != null)
            {
                break;
            }
        }
    }
    else
    {
        foreach (SiteMapNode node in SiteMap.Providers["AspNetXmlSiteMapProvider"].RootNode.GetAllNodes())
        {
            if (node.IsAccessibleToUser(HttpContext.Current) && node["navigatorId"] == navigatorId)
            {
                rcNode = node;
                break;
            }
        }
    }

    if (rcNode != null)
    {
        rc = rcNode.Url;
    }

    return rc;
}

Implementing this configuration is outside the scope of this article, but the resulting web.config ends up looking like this.

<configuration>
    <configSections>
        <section name="ZacksFiasco.Web.Navigation"
                 type="ZacksFiasco.Web.Navigation.Configuration.NavigatorSection, ZacksFiasco.Web.Navigation"/>
    </configSections>
    <ZacksFiasco.Web.Navigation>
        <siteMapProvidersToSearch> 
            <add siteMapProviderName="AspNetXmlSiteMapProvider" />          
        </siteMapProvidersToSearch>       
    </ZacksFiasco.Web.Navigation>
    <system.web>
        <siteMap defaultProvider="NavSiteMapProvider" enabled="true">
            <providers>
                <add name="NavSiteMapProvider"
                     description="Custom SiteMap provider."
                     type="ZacksFiasco.Web.Navigation.NavigatorSiteMapProvider, ZacksFiasco.Web.Navigation"
                     siteMapFile="Web.sitemap"
                     securityTrimmingEnabled="true"/>
            </providers>
        </siteMap>
    </system.web>
</configuration>

Next, add to your class a member dictionary that takes a string as the key and a string as the value. Add a property get that wraps this dictionary. I called my property Parameters.

Now add a string member named navigatorId and add a property with a get and set that wraps this member.

string navigatorId;

public string NavigatorId
{
    get { return navigatorId; }
    set { navigatorId = value; }
}

Dictionary<string, string> parameters;

public Dictionary<string, string> Parameters
{
    get { return parameters; }
}

Override the ToString() member. In ToString(), perform the string concatenation necessary to build the URL. To retrieve the base URL, call the static method GetUrlFromKey() and pass the navigatorId member. To add the querystring parameters, perform a loop on the parameters dictionary and URL Encode each parameter.

Now add two final methods. Redirect() and Transfer(). Redirect() performs a Response Redirect() and Transfer performs a server-side Transfer. To implement each of these, just get the current HttpContext and call to the appropriate context object. Use the ToString() method to build your URL and that is all you need.

public void Transfer()
{
    HttpContext.Current.Server.Transfer(ToString());
}

public void Redirect()
{
    HttpContext.Current.Response.Redirect(ToString());
}

To use what you have written, the code to perform a programmatic navigation looks like the following:

protected void btHidden1_Click(object sender, EventArgs e)
{
    Navigator n = new Navigator("HiddenOne");
    n.Parameters["text"] = txParam.Text;
    n.Parameters["time"] = DateTime.Now.ToLongTimeString();

    n.Redirect();
}

Visit the ZacksFiasco.Web.Navigation Codeplex site to download the source code or download the assembly and begin using it in your projects right away.

kick it on DotNetKicks.com

Currently rated 4.0 by 3 people

  • Currently 4/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags:

Web Navigation | ASP.NET | C# | SiteMapProvider

Powered by BlogEngine.NET 1.4.5.0
Theme by Mads Kristensen | Modified by Mooglegiant


I've been doing software development since I was little and my dad first brough home an ATARI 800. I picked up PILOT and BASIC. Now I mostly write C# and ASP.NET, and about a dozen other languages and platforms. I also enjoy a bunch of outdoor sports including running and mountain biking. I am very happily married. I am currently working for a great Software and IT consulting company named SPINEN where I am a Senior Developer.

RecentPosts


Copyright Zack Moore

TextBox