Use your routing system to create a multilingual web forms project

Name: *
My email: *
Recipient email: *
Message: *
Fields marked as bold are compulsory.
You haven't filled in compulsory values. The email is not correct

In a previous article we talked about multilingual sites. This time we are going to see how we can place that info in the page URL, using ASP.NET's routing system, and create this way an entirely different version of a website, depending on the language selected.
 
Why, you may ask, would I choose Web Forms to do this instead of using MVC's way better routing system? Well, no matter how deprecated Web Forms may become, websites based on that will still remain out there for a long time, so it may still be useful to some. And, after all, no matter what they look like right now, who doesn't like reading stuff concerning Web Forms?
 

Why messing with the URL?

 
To begin with, what we're talking about is not storing the info in the query string; instead we’ll try out the actual URL body. Actually, language info could as well be stored using cookies or other means. What do we get by using this
MyWebsite/en/contact.aspx
instead of 
MyWebsite/contact.aspx?lang=en
or simply store language on some cookie and use
MyWebsite/contact.aspx
 
The main reason for doing so, is SEO. Cookies have nothing to do with SEO, so using them is not a good choice. Storing parameters (language or anything else) in the query string is better than cookies but still not as good as storing info right inside the URL file path. 
 
You may of course use different hosts to achieve that (eg MyWebsite.en or en.MyWebsite) which is ok as well. However this is a whole new story which is totally up to you to choose. What we’re talking about here requires nothing more than a single host.
 
You may also read for yourself Google's point of view concerning multilingual sites here.
 
 
multi-language site

Creating routes

 
Before moving on to the main part of the article we are going to take a look at the basics of the web forms routing system. 
 
By default, Web Forms use file system request handling. The MyWebsite/contact.aspx request searches for the contact.aspx file located under the root menu. MyWebsite/superheroes/superheroes.aspx should look for the superheroes.aspx file located inside the superheroes file under the root menu.
 
Using the global.asax file we may add our own handling. All we need is to call a method, having a RouteCollection parameter, from within the Application_Start method. So let's create a method called RegisterRoutes and place it within a new file called MyRouteConfig.cs.
 
private void Application_Start(object sender, EventArgs e) 
{
    MyRouteConfig.RegisterRoutes(RouteTable.Routes);
}
 
public static class MyRouteConfig
{
   public static void RegisterRoutes(RouteCollection routes)
   {
   }
}
 
Things that change the way routing system works should be placed here. For example, in order to use friendly urls (getting the same result as MyWebsite/contact.aspx when you request MyWebsite/contact) your RegisterRoutes method should look like
 
private void RegisterRoutes(RouteCollection routes)
{
     routes.EnableFriendlyUrls();
 
To make it work, add references to Microsoft.AspNet.FriendlyUrls and System.Web.Routing. 
 
Now let's check out one more thing. Let's say, you had a page called superheroes.aspx where the superhero id was placed inside the query string superhoeroes.aspx?id=2 and then you got it's value from within your page using Request.QueryString["id"].
 
Instead we could use a friendlier method and replace superhoeroes.aspx?id=2 with superhero/ironman or superhero/hulk.
 
By default this would search for the ironman folder under the superhero folder
 
However we could add the following line in our RegisterRoutes method
 
routes.MapPageRoute("superheroes""superhero/{SuperheroName}", "~/superheroes.aspx");
 
The simplest instance of MapPageRoute requires three arguments.
1) A string as the route's name.
2) The URL the route will handle.
3) The physical file the route will point to.
 
Using the route we created, if we get a superhero/hulk request, this will redirect to the superheroes.aspx file.
Within the page code we may get the superhero name using Page.RouteData.Values["SuperheroName"]).
 
hulk-homer-avengers
 
In case of superhero/hulk, Page.RouteData.Values["SuperheroName"] would result in "hulk". Always pay attention to null values.
 
Now, let's move on and see how the concept described can be applied to more than one language. 
 
 

Adding the language

 
To add a language identifier we need an extra route that will point us to a new handler we will create.
 
routes.Add(new System.Web.Routing.Route("{language}/{*page}", new LanguageRouteHandler()));
 
This is the route we need. Now let's take care of the handler. We are going to create the LanguageRouteHandler class implementing the IRouteHandler interface. This requires creating a GetHttpHandler method that returns an IHttpHandler object. Here's what we have so far.
 
public class LanguageRouteHandler : IRouteHandler
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        string page = CheckForNullValue(requestContext.RouteData.Values["page"]);
        string virtualPath = "~/" + page;
 
        try
        {
            return BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(Page)) as IHttpHandler;
        }
        catch
        {
            return null;
        }
    }
}
 
Seems nice. So, how does it work?  Let's say we request the contact page. English is our default language. So requesting MyWebsite/contact would request the contact page (based on the previous part routing system) as if we had never created our handler.  That is because our handler searches for {language}/{*page} and  "contact" does not match this. We could have used MyWebsite/en/contact as well, and treat English as any other language; however default language usually needs no language identifier.
 
Don't forget to add references to System.Web, System.Web.UI, and System.Web.Compilation.
 
Now it's time to activate our handler. Request MyWebsite/el/contact.aspx. This request matches the route we created and will use the GetHttpHandler of our new class. Our page variable will be equal to "contact.aspx". However we should add a tilde (~) to it as CreateInstanceFromVirtualPath needs absolute paths to work properly. CreateInstanceFromVirtualPath will return an IHttpHandler object and things will work just fine.
 
CheckForNullValue is a simple custom method that returns a string, taking care of null values instances.
 
Whenever we need to get that language info, we may use Page.RouteData.Values["language"]), the same way we did earlier.
 
"el" refers to the Greek language. As a matter of fact I could have used any possible name eg MyWebsite/DotNetHintsRocks/contact.aspx as long as the "DotNetHintsRocks" string is a reference to a language, according to my personal language dictionary. Search engines however, would prefer spotting "en" or "el" on my URL instead of "DotNetHintsRocks".
 
That's the basic part. We have easily created a multilingual site. However there are still a few more things to do before it is completed.
 
For example MyWebsite/contact will route to MyWebsite/contact.aspx. Still, LanguageRouteHandler will not be able to redirect LanguageRouteHandler MyWebsite/el/contact. It will do its best using CreateInstanceFromVirtualPath but inevitably will get an HttpException stating "The file '/contact' does not exist."
 
To avoid this we could add some extra code to handle such requests, for example
if (!virtualPath.Contains(".aspx"))
      virtualPath += ".aspx";
 
Using that code, virtual path will be turned into "contact.aspx", a file name which does exists. This is a custom solution that may prove nice depending on your case.
 
Let's see a few more custom solutions.  Supposing MyWebsite/ shows home.aspx content. What would MyWebsite/el do? VirtualPath is going to become "~/.aspx" and since there is no such file we will get another exception.
 
There are two ways to avoid this. Either you redirect to your actual page, or you route to that page.
 
if (string.IsNullOrEmpty(page))
        {
            string language= CheckForNullValue(requestContext.RouteData.Values["language"]);
 
            string newPage = "/home";
            if (!string.IsNullOrEmpty(language))
                newPage = "/" + language + newPage;
            HttpContext.Current.Response.Redirect(newPage, false);
            HttpContext.Current.Response.StatusCode = 301;
            HttpContext.Current.Response.End();
 
           //Otherwise, we simply route to home
           //page = "home";
        }
 
Finally, remember that superhero/hulk route we have created? We used
routes.MapPageRoute("superheroes""superhero/{SuperheroName}", "~/superheroes.aspx");
to make it work. In order to add the language info we should put some extra route underneath. That would be
routes.MapPageRoute("languageSuperheroes", "{language}/superhero/{SuperheroName}", "~/superheroes.aspx");
 
languages-website
 
In case you'd like to see the whole picture, here's what the complete source code would look like. First the global.asax file:
 
<%@ Application Language="C#" %>
 
<script runat="server">
 
   private void Application_Start(object sender, EventArgs e) 
   {
       MyRouteConfig.RegisterRoutes(RouteTable.Routes);
   }
 
</script>
 
 
Then the MyRouteConfig.cs file
 
using System;
using Microsoft.AspNet.FriendlyUrls;
using System.Web.Routing;
using System.Web;
using System.Web.UI;
using System.Web.Compilation;
 
 
public static class MyRouteConfig
{
   public static void RegisterRoutes(RouteCollection routes)
   {
        routes.EnableFriendlyUrls();
 
        routes.MapPageRoute("superheroes""superhero/{SuperheroName}", "~/superheroes.aspx");
        routes.MapPageRoute("languageSuperheroes""{language}/superhero/{SuperheroName}""~/superheroes.aspx");
 
        routes.Add(new System.Web.Routing.Route("{language}/{*page}"new LanguageRouteHandler()));
  } 
 
 
 
   public class LanguageRouteHandler IRouteHandler
   {
       public IHttpHandler GetHttpHandler(RequestContext requestContext)
       {
           string page = CheckForNullValue(requestContext.RouteData.Values["page"]);
           string virtualPath = "~/" + page;
 
           if (string.IsNullOrEmpty(page))
           {
               string language= CheckForNullValue(requestContext.RouteData.Values["language"]);
               string newPage = "/home";
 
               if (!string.IsNullOrEmpty(language))
                   newPage = "/" + language + newPage;
               HttpContext.Current.Response.Redirect(newPage, false);
               HttpContext.Current.Response.StatusCode = 301;
               HttpContext.Current.Response.End();
 
              //Otherwise, route to home
              //page = "home";
           }
 
          if (!virtualPath.Contains(".aspx"))
                virtualPath += ".aspx";
 
           try
           {
               return BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(Page)) as IHttpHandler;
           }
           catch
           {
               return null;
           }
       }
   }
}
 

Pay attention

 
Keep in mind that there may be more things that could be irritating in your website's case. For example, our new handler might receive more requests than expected. A request for images/superheroes/hulk.jpg may enter our handler using "images" as our language and "superheroes/hulk.jpg" as page. In fact you should pay extra attention when turning a website from one to many languages support. There may be more routes that need to be specified or source code to be written in order to handle uncommon situations.
 
A few things that you should watch out are the literals or the URLs that will be created. Apart from that, ensure that you get no SEO issues which is quite common to show up.
 
Needless to say, that techniques described above can be applied to situations totally different than the language issue we've been talking about.
 
 

Summary

Placing the website's language info inside your URL is a good thing to do. In order to get to this, we said a few things concerning ASP.NET's routing system and how we can use it to create "better" URLs. Then we described how to create a custom handler that can use the language info and create a multilingual website.
 

 

Back to BlogPreviousNext

Comments



    Leave a comment
    Name: