Saturday, June 18, 2011

Adding .Net Routing 3.5 to Asp.Net Web Forms

This article shows how to take a web forms application and enable it to use .Net Routing 3.5.

Why should I do this you might ask, well as you know web forms renders urls like: http://mydomain/Login.aspx.  Which is not very friendly to a user especially when you start adding parameters. 

Would it not be better to have the url like this: http://mydomain/login/. That’s nicer for the user to read and search engines will appreciate it more!

You can even do things like:
    http://mydomain/log-user-in/
    http://mydomain/product/1/  (which without routing would be http://mydomain/Product.aspx?id=1)

To enable .Net Routing in your web project you need to do the following:
1. Add these 2 references to your web project:
        System.Web.Routing and System.Web.Abstractions
Whilst System.Web.Abstractions is not needed for .Net Routing we will use it later in this post.

2. Add the following to your web.config inside the <httpModules> section

<!-- added to enable routing -->
<add name="RoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

e.g.

<httpModules>
    <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>

    <!-- added to enable routing –>
    <add name="RoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</httpModules>

3. Now we need to setup the routing which is done in the Global.asax.cs file (if you do not have one you can add it by ‘Right Click –> Add New Item’.

Add the following RegisterRoutes method to the Application_Start.

protected void Application_Start(object sender, EventArgs e)
{
    // .Net Routing Start up
    RegisterRoutes(RouteTable.Routes);
}

Now create the RegisterRoutes method and add your routes to it like this:

public static void RegisterRoutes(RouteCollection routes)
{
    // The routes:
    routes.Add("Default", new CustomRoute("", new URLRouteHandler("~/Login.aspx")));

    routes.Add(PageIdentifier.Login.ToString(), new CustomRoute("login", new URLRouteHandler("~/Login.aspx")));

    routes.Add(PageIdentifier.Logout.ToString(), new CustomRoute("logout", new URLRouteHandler("~/Logout.aspx")));

    routes.Add(PageIdentifier.ContactUs.ToString(), new CustomRoute("contact-us", new URLRouteHandler("~/ContactUs.aspx")));
}

To explain, the syntax of a route is essentially routes.Add(nameOfTheRoute, route);

nameOfTheRoute - is simply a string, but this is an important parameter as later on you can create a method to to find the route by using this string.  Which is why I like to use the PageIdentifier enum e.g.

public enum PageIdentifier : int
{
    Login = 1,
    Logout = 2,
    ContactUs = 21,
}

route – this at it’s simplest contains the name of the route (which is shown in the url), and where to route to e.g. the .aspx page.

Most of your routes will look something like the one below which means that ‘login’ is shown in the url (e.g. ‘http://mydomain/login’) when the login.aspx page is shown.

routes.Add(PageIdentifier.Login.ToString(), new CustomRoute("login", new URLRouteHandler("~/Login.aspx")));

But you can also setup a default route so if now route is specified in the url (e.g. ‘http://mydomain/’) then go to the login.aspx page.

routes.Add("Default", new CustomRoute("", new URLRouteHandler("~/Login.aspx")));


4. You may have noticed that we have missed something, what’s this CustomRoute and URLRouteHandler.

4.1. URLRouteHandler
In order to route our request to an .aspx page you need to create a route handler that will do this.  When you use the URLRouteHandler e.g.

new URLRouteHandler("~/Login.aspx")

the Login.aspx is the page you want displayed, the URLRouteHandler essentially creates an instance of this page and returns it.

/// <summary>
/// Maps a .Net Route to an .aspx page
/// This can be invoked from the global.asax.cs like this:
/// routes.Add("ContactUs", new CustomRoute("contact-us", new URLRouteHandler("~/ContactUs.aspx")));
/// </summary>
public class URLRouteHandler : IRouteHandler
{
    public URLRouteHandler(string virtualPath)
    {
        this.VirtualPath = virtualPath;
    }

    public string VirtualPath { get; private set; }

    public System.Web.IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        foreach (var paramUrl in requestContext.RouteData.Values)
            requestContext.HttpContext.Items[paramUrl.Key] = paramUrl.Value;
        try
        {
            var page = BuildManager.CreateInstanceFromVirtualPath(VirtualPath, typeof(Page)) as IHttpHandler;
            return page;
        }
        catch (Exception)
        {
            throw;
        }
    }
}


4.2. CustomRoute
While you can just use the ‘Route’ class provided by .Net Routing, I found it useful to create a new CustomRoute class.  This class below will always render a url returned to the browser in lowercase and will append a trailing slash e.g. ‘http://mydomain/login/’.  This is useful for improving your Google search engine ranking.

/// <summary>
/// Override of the .Net Routing 'Route' class, to ensure the url returned is always lowercase and has a trailing slash.
/// </summary>
public class CustomRoute : Route
{
    #region Constructors
    public CustomRoute(string url, IRouteHandler routeHandler)
        : base(url, routeHandler)
    {
    }

    public CustomRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
        : base(url, defaults, routeHandler)
    {
    }

    public CustomRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler)
        : base(url, defaults, constraints, routeHandler)
    {
    }

    public CustomRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler)
        : base(url, defaults, constraints, dataTokens, routeHandler)
    {
    }
    #endregion

    public override RouteData GetRouteData(System.Web.HttpContextBase httpContext)
    {
        var routeDate = base.GetRouteData(httpContext);
        return routeDate;
    }
                
    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        VirtualPathData path = base.GetVirtualPath(requestContext, values);
        // Make sure the path returned is lowercase and has a trailing slash (SEO friendly)
        if (path != null)
            path.VirtualPath = path.VirtualPath.ToLowerInvariant() + "/";
        return path;
    }
}


5. Up to now the routing will work if you type the route into the url, but we also need to be able to show the routes on our website as links e.g. <a href=”somelinkhere”>click me</a>

Suppose you had this on a page:

<asp:HyperLink ID="LogoutHyperLink" runat="server">Log Out</asp:HyperLink>

and instead of the NavigateUrl being ‘Logout.aspx’ which in the browser would show up as ‘http://mydomain/Logout.aspx’ you wanted to show the route e.g. ‘http://mydomain/logout/’.

You can do this simply by using that PageIdentifer enum I mentioned at the start of this post.

LogoutHyperLink.NavigateUrl = LinkFactory.GetRoute(PageIdentifier.Logout.ToString());

And the code for the LinkFactory class is shown below (creating this class just makes it easier to reuse later as you would want to do this for every hyperlink on your site).

public class LinkFactory
{
   /// <summary>
   /// Determines the url path (route) to return based on the routeName passed in.
   /// A routeName must exist in the global.asax.cs
   /// </summary>
   /// <param name="context"></param>
   /// <param name="routeName"></param>
   /// <returns></returns>
   public static string GetRoute(String routeName)
   {
       var pathData = System.Web.Routing.RouteTable.Routes.GetVirtualPath(
             null, routeName, new System.Web.Routing.RouteValueDictionary { });
       return pathData.VirtualPath;
   }
}

One final point, is all your existing .aspx pages will continue to work as they do today because .Net routing only kicks in when a page cannot be found on disc.

Here is the source code

That’s it, happy routing!

1 comment:

  1. Hi Jon,

    Thank you so much for a great post. I am new to routing, and your methodology is so far the only one that works for my project. I also really admire your structural approach and your clear writing.
    I have only one problem. Right now when I open the project and my homepage comes up, the URL I see in the browser is “mydomain.com/Default.aspx”. I want to suppress the “Default.aspx” and only show “mydomain.com/”. How can I do that?
    Moreover, when I return to the homepage Default.aspx from another page on the site, I am at the moment seeing “mydomain.com/home”, but I would like to suppress “home” here again and only show “mydomain.com/”.
    Do you have any advice on that?
    I would really appreciate your help! Thank so much in advance!

    Sample code:

    class CustomRoute.cs - as you defined it

    Global.asax :
    void RegisterRoutes(RouteCollection routes)
    {
    routes.Ignore(
    "{resource}.axd/{*pathInfo}");
    routes.Add(
    PageIdentifier.Default.ToString(), new CustomRoute("home", new URLRouteHandler("~/Default.aspx")));
    routes.Add(
    PageIdentifier.About.ToString(), new CustomRoute("about", new URLRouteHandler("~/About.aspx")));
    routes.Add(
    PageIdentifier.Login.ToString(), new CustomRoute("login", new URLRouteHandler("~/Account/Login.aspx")));
    }
    void Application_Start(object sender, EventArgs e)
    {
    // Code that runs on application startup
    RegisterRoutes(
    RouteTable.Routes);
    }
    An example of call for pages:
    namespace
    TestRouting
    {
    public partial class SiteMaster : System.Web.UI.MasterPage
    {
    protected void Page_Load(object sender, EventArgs e)
    {
    Menu mn = (Menu)this.FindControl("NavigationMenu");
    mn.Items[0].NavigateUrl =
    LinkFactory.GetRoute(PageIdentifier.Default.ToString());
    mn.Items[1].NavigateUrl =
    LinkFactory.GetRoute(PageIdentifier.About.ToString());
    HtmlAnchor HeadLoginStatus = ((HtmlAnchor)HeadLoginView.FindControl("HeadLoginStatus"));
    HeadLoginStatus.HRef=
    LinkFactory.GetRoute(PageIdentifier.Login.ToString());
    }
    }
    }

    ReplyDelete