Caffeine-Powered Life

Getting ASP.NET MVC Routes Into Your JavaScript

Scenario

This is not legal is ASP.NET. But, oh, how I wish it were.

application.js
1
2
3
4
5
6
$("selector").click(function (e) {
  e.preventDefault();
  $.get('@Url.Action("Version", "Home")', function (result) {
    // SNIP! Do something with the result...
  });
});

You cannot do this, because your *.js files don’t get processed by the view engine. Instead of rendering out a valid path, you’ll really end up with a GET request being sent to http://myapplication.com/@Url.Action(%22Version%22,%20%22Home%22). I don’t know your application, but I’m willing to bet that isn’t quite right.

Ideally, you don’t want to hard-code the URL in the jQuery $.get call. You’ll lose refactor support, and your application won’t function the same way if it is installed to a virtual directory.

The Fix

My solution is to create a controller action that returns the necessary JavaScript. We’ll start with the model that we want to emit.

Models/JavaScriptExposedRoutes.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class JavaScriptExposedRoutes
{
  private readonly Dictionary<string, string> routeDictionary = new Dictionary<string, string>();

  public void Add(string name, string url)
  {
    routeDictionary.Add(name, url);
  }

  public string ToJavaScriptString()
  {
    var routeStrings = new List<string>();
    foreach (var item in routeDictionary)
    {
      var routeString = string.Format("{0}:\"{1}\"", item.Key, item.Value);
      routeStrings.Add(routeString);
    }
    return "routes={" + string.Join(",", routeStrings) + "};";
  }

  public override string ToString()
  {
    return ToJavaScriptString();
  }
}

We will also create a RoutesController that will return our routes as a JavaScript string.

Controllers/RoutesController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class RoutesController : Controller
{
  private static bool hasInitializedRoutes = false;
  private static JavaScriptExposedRoutes routes;

  public ActionResult Index()
  {
    // We will probably want to cache this output result. It isn't going to 
    // change during the application lifecycle. It will be much faster to
    // return 304's than 200's.
    InitializeRoutes();
    return JavaScript(routes.ToString());
  }

  private void InitializeRoutes()
  {
    // This won't change during an application's lifetime. We can make the routes
    // variable static, along with the initialization check. We cannot make the 
    // entire method static (nor can we use a static constructor), since the UrlHelper
    // instance (this.Url) is not static.
    if (!hasInitializedRoutes)
    {
      routes = new JavaScriptExposedRoutes();
      routes.Add("about_url", Url.Action("About", "Home"));
      routes.Add("home_url", Url.Action("Index", "Home"));
      routes.Add("version_url", Url.Action("Version", "Home"));
      hasInitializedRoutes = true;
    }
  }
}

When you navigate to http://site/MyAppPath/Routes/Index, this is the result. For this example, I have set up Visual Studio to run in a virtual directory name MyAppPath to demonstrate that the URL helper is doing it’s job. If you’re not using a virtual directory, then this will not be necessary for you.

1
routes={about_url:"/MyAppPath/Home/About",home_url:"/MyAppPath/",version_url:"/MyAppPath/Home/Version"};

Implementation

First, I’m going to add a route. By default, MVC puts these in global.asax. If you’ve moved this to a config folder (like you should), then put this with the rest of your routes.

1
2
// This route hides the fact that routes.js is really a controller action.
routes.MapRoute("JavaScript-Routes", "Scripts/routes.js", new { controller = "Routes", action = "Index" });

This is just a little bit of obfuscation. It’s one of those little things that will makes the user experience just a little bit nicer.

Here’s what it will look like in your view.

And here’s the rendered output in HTML.

The usage is now very straightforward. We have added a global object to your site called routes. This object contains only the routes you chose to expose through your controller action. Now, if we go back to our very first example, we can easily type the following.

application.js
1
2
3
4
5
6
$("#clickme").click(function (e) {
  e.preventDefault();
  $.get(routes.version_url, function (result) {
    alert("Version = " + result.version);
  });
});

Happy coding!

A copy of this source code is available on GitHub.

Comments