Sunday, June 19, 2011

Add a Permanent 301 Redirect Route Using .Net Routing 3.5

As pages on our sites get old and replaced sometimes we need to redirect users to the newer pages.  You can do this in .Net Routing by setting up a new route which does a 301 (Permanent) redirect.

You will need a new route handler method to do the redirect.  The code for this is shown below:

/// <summary>
/// This route handler allows redirecting old urls to a new page/route name and 
/// sets a permanent 301 status code (Moved Permanently)
/// This can be invoked from the global.asax.cs like this:
///     The route below will redirect requests from somedirectory/A.aspx to somedirectory/login
///     routes.Add("OLDRoute1", new CustomRoute("somedirectory/A.aspx", new PermanentRedirectRouteHandler("login")));
/// </summary>
public class PermanentRedirectRouteHandler : IRouteHandler, IHttpHandler
{
    public PermanentRedirectRouteHandler(string virtualPath)
    {
        this.VirtualPath = virtualPath;
    }

    public string VirtualPath { get; private set; }
    private RequestContext requestContext = null;

    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        this.requestContext = requestContext;
        return this;
    }

    public bool IsReusable
    {
        get { return false; }
    }

    public void ProcessRequest(HttpContext context)
    {
        context.Response.StatusCode = 301;
        context.Response.AppendHeader("Location", VirtualPath);
    }
}

While testing you might want to change the StatusCode to 302 (temporary redirect).

To create a route using the PermanentRedirectRouteHandler method you would add a route like this:

Example 1:

routes.Add("OLDRoute1", new CustomRoute("A.aspx", new PermanentRedirectRouteHandler("login")));

The route above will redirect a request to ‘http://mydomain/A.aspx’ to ‘http://mydomain/login’.

Example 2:

routes.Add("OLDRoute2", new CustomRoute("site1/A.aspx", new PermanentRedirectRouteHandler("login")));

The route above will redirect a request to ‘http://mydomain/site1/A.aspx’ to ‘http://mydomain/site1/login’.

Example 3:

routes.Add("OLDRoute3", new CustomRoute("site1/B.aspx", new PermanentRedirectRouteHandler("/login")));

The route above will redirect a request to ‘http://mydomain/site1/B.aspx’ to ‘http://mydomain/login’.

Example 4:

routes.Add("OLDRoute4", new CustomRoute("contact-us", new PermanentRedirectRouteHandler("/login")));

The route above will redirect a request to ‘http://mydomain/contact-us/’ to ‘http://mydomain/login’.

As routes are evaluated in order you should add the redirect routes to the top of your routes in the Global.asax.

If you missed the first post in this series have a look at Adding .Net Routing 3.5 to Asp.Net Web Forms.

Happy Routing!

Route to Directories That Do Not Exist On The Disc with .Net Routing 3.5

In my last post we added .Net Routing to web forms, you can find that post here: Adding .Net Routing 3.5 to Asp.Net Web Forms.

This post shows how to create a route that in the url has directories that do not exist on the disc.  Typically .Net will through a 404 file cannot be found error if a directory does not exist but .Net Routing allows you to say actually I’m going to accept that route because I know it maps to this location.

e.g. http://mydomain/site1/login/   or http://mydomain/site2/login

In that example site1 & site2 do not exist as directories on the disc, and login is the page we want to show. 
What this lets me do is have the same code running under mydomain but have different sites (which may be using different style sheets, etc).  The url will always look like it does above for each site, so users can easily bookmark it.

This lets both sites use the same pages, without deploying the same code again or creating multiple virtual directories.

How will the routes look in the Global.asax? Like this:

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

    routes.Add(“Site1” + PageIdentifier.Login.ToString(), new CustomRoute("site1/login", new URLRouteHandler("~/Login.aspx")));

    routes.Add(“Site2” + PageIdentifier.Login.ToString(), new CustomRoute("site2/login", new URLRouteHandler ("~/Login.aspx")));

    routes.Add(“Site1” + PageIdentifier.Logout.ToString(), new CustomRoute("site1/logout", new URLRouteHandler("~/Logout.aspx")));

    routes.Add(“Site2” + PageIdentifier.Logout.ToString(), new CustomRoute("site2/logout", new URLRouteHandler("~/Logout.aspx")));
}

The first 2 routes ‘Site1Default’ & ‘Site2Default’ say if a url like http://mydomain/site1/’ is entered then by default go to the login.aspx page.

 

How can I get Normal .aspx pages to work under the missing directories?

So far by doing the above will mean that normal aspx pages will not work with the missing site1 & site2 directorues e.g. http://mydomain/site1/Login.aspx’. This will throw a 404 file not found exception. But if you want them to work you can add a couple of extra routes at the end of your existing routes like this.

routes.Add("Site1ASPX", new CustomRoute("site1/{page}", new DefaultURLRouteHandler()));

routes.Add("Site2ASPX", new CustomRoute("site2/{page}", new DefaultURLRouteHandler()));

This says everything under site1/* should be handled by the DefaultURLRouteHandler, and this route handler is expecting the {page} parameter e.g. ‘http://mydomain/site1/Login.aspx’ in this example the {page} parameter is ‘Login.aspx’.

The code for the DefaultURLRouteHandler is shown below:

/// <summary>
/// This route handler allows standard .aspx pages to work where there are directories that do not exist on disk
/// e.g. somedirectory/login.aspx will reroute to login.aspx even though on disk somedirectory does not exist
/// This can be invoked from the global.asax.cs like this:
///     var route = new CustomRoute("somedirectory/{page}", new DefaultURLRouteHandler());
/// </summary>
public class DefaultURLRouteHandler : IRouteHandler
{
    public DefaultURLRouteHandler()
    {
    }

    public System.Web.IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        string filename = requestContext.RouteData.Values["page"] as string;

        foreach (var paramUrl in requestContext.RouteData.Values)
            requestContext.HttpContext.Items[paramUrl.Key] = paramUrl.Value;

        try
        {
           var page = BuildManager.CreateInstanceFromVirtualPath("~/" + filename, typeof(Page)) as IHttpHandler;
            return page;
        }
        catch (Exception)
        {
            throw;
        }
    }
}

The line in red is where the magic happens.

Happy routing!

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!

Sunday, June 12, 2011

MVC3 RadioButtonList Helper

An MVC 3 example which uses the Radio Button helper twice on a page to create 3 radio buttons with the first radio button helper and 2 radio buttons with the 2nd helper.  Both have validation to ensure 1 option is selected. And if the form fails validation the chosen radio option is preselected when the form is reshown.

The radio buttons can be shown on the page horizontally or vertically.

The Html Helper Method

Essentially all this helper does is loop round a list that you provide it and for each item in the list creates a html radio button and a label for it.

I created a new class with the following code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Linq.Expressions;
using System.Text;

namespace MVC3_RadioButtonList_Helper_Sample
{
    public static class HtmlExtensions
    {
        public static MvcHtmlString RadioButtonForSelectList<TModel, TProperty>(
            this HtmlHelper<TModel> htmlHelper,
            Expression<Func<TModel, TProperty>> expression,
            IEnumerable<SelectListItem> listOfValues)
        {
            var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
            var sb = new StringBuilder();

            if (listOfValues != null)
            {
                // Create a radio button for each item in the list
                foreach (SelectListItem item in listOfValues)
                {
                    // Generate an id to be given to the radio button field
                    var id = string.Format("{0}_{1}", metaData.PropertyName, item.Value);

                    // Create and populate a radio button using the existing html helpers
                    var label = htmlHelper.Label(id, HttpUtility.HtmlEncode(item.Text));
                    var radio = htmlHelper.RadioButtonFor(expression, item.Value, new { id = id }).ToHtmlString();

                    // Create the html string that will be returned to the client
                    // e.g. <input data-val="true" data-val-required="You must select an option" id="TestRadio_1" name="TestRadio" type="radio" value="1" /><label for="TestRadio_1">Line1</label>
                    sb.AppendFormat("<div class=\"RadioButton\">{0}{1}</div>", radio, label);
                }
            }

            return MvcHtmlString.Create(sb.ToString());
        }
    }
}

I’ve added a div around the radio button and it’s label with a class of ‘RadioButton’, this will let you in CSS position the radio button either vertically or horizontally.  The default is vertically:

InitialPage

But you can switch this to horizontal with a bit of CSS

.RadioButton { float:left; }

InitialPageAsHorizontal

The rest of this article shows how to use the RadioButtonForSelectList helper, you reference it in a view like this: @Html.RadioButtonForSelectList(m => m.TestRadio, Model.TestRadioList)

The Model

For this example I just created an empty MVC3 application and added this model.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace MVC3_RadioButtonList_Helper_Sample.Models
{
    public class IndexViewModel
    {
        public IEnumerable<SelectListItem> TestRadioList { get; set; }
        public IEnumerable<SelectListItem> TestRadioList2 { get; set; }

        [Required(ErrorMessage = "You must select an option for TestRadio")]
        public String TestRadio { get; set; }

        [Required(ErrorMessage = "You must select an option for TestRadio2")]
        public String TestRadio2 { get; set; }
    }

    public class aTest
    {
        public Int32 ID { get; set; }
        public String Name { get; set; }
    }
}

The Controller Action

I added the following to my controller to populate the model with 2 different lists one for each of the radio button helpers.  The first radio button help also has a default value set so the middle option (‘Line2’) will be pre-selected.

I also changed the Index HttpPost method so that if validation fails the same list of radio buttons is added to the model (obviously I have duplicated the list creation code, in a live example you should create a common function).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MVC3_RadioButtonList_Helper_Sample.Models;

namespace MVC3_RadioButtonList_Helper_Sample.Controllers
{
    public class TestController : Controller
    {
        //
        // GET: /Test/
        public ActionResult Index()
        {
            List<aTest> list = new List<aTest>();
            list.Add(new aTest() { ID = 1, Name = "Line1" });
            list.Add(new aTest() { ID = 2, Name = "Line2" });
            list.Add(new aTest() { ID = 3, Name = "Line3" });
            SelectList sl = new SelectList(list, "ID", "Name");

            List<aTest> list2 = new List<aTest>();
            list2.Add(new aTest() { ID = 1, Name = "test1" });
            list2.Add(new aTest() { ID = 2, Name = "test2" });
            SelectList sl2 = new SelectList(list2, "ID", "Name");

            var model = new IndexViewModel();
            model.TestRadioList = sl;
            model.TestRadioList2 = sl2;
           
            model.TestRadio = "2";  // Set a default value for the first radio button helper

            return View(model);
        }

        [HttpPost]
        public ActionResult Index(IndexViewModel model, string returnUrl)
        {
            if (ModelState.IsValid)
            {
                ModelState.AddModelError("", "Always force an error to be raised so we can test the postback sets the radio buttons to their last values.");
            }

            // If we got this far, something failed, redisplay form
            List<aTest> list = new List<aTest>();
            list.Add(new aTest() { ID = 1, Name = "Line1" });
            list.Add(new aTest() { ID = 2, Name = "Line2" });
            list.Add(new aTest() { ID = 3, Name = "Line3" });
            SelectList sl = new SelectList(list, "ID", "Name");

            List<aTest> list2 = new List<aTest>();
            list2.Add(new aTest() { ID = 1, Name = "test1" });
            list2.Add(new aTest() { ID = 2, Name = "test2" });
            SelectList sl2 = new SelectList(list2, "ID", "Name");

            model.TestRadioList = sl;
            model.TestRadioList2 = sl2;

            return View(model);
        }
    }
}

The View

To test the html helper I added a view that is bound to the IndexViewModel.

I added a using statement so the helper method would be found by intelisence @using MVC3_RadioButtonList_Helper_Sample

I added the new helper RadioButtonForSelectList and passed in the TestRadio property from the IndexViewModel which is where the selected result will be put (the radio button ID value e.g. 2).

And the TestRadioList which is a list of SelectListItems for which a radio button will be created for each item in the list.

@Html.RadioButtonForSelectList(m => m.TestRadio, Model.TestRadioList)
@Html.ValidationMessageFor(m => m.TestRadio)

So the error message (as specified in the model ‘TestRadio’ property is shown when no radio button is selected I added a validation message helper.

I then added a 2nd radio button helper to the form like this, which is bound to a different list and it’s return value will be in TestRadio2.

@Html.RadioButtonForSelectList(m => m.TestRadio2, Model.TestRadioList2)
@Html.ValidationMessageFor(m => m.TestRadio2)

The form below shows the validation in action:

ValidationAtWork

 

Here is the source code.