Named routing with Knockout, ASP.NET MVC, and AttributeRouting, from JavaScript

It’s hard to describe exactly what I’ve built here, but I’m just throwing these pieces out on the Internet in case someone: a) finds them useful, or b) has a better solution.

Goals:

  1. Named routes, with a wee bit of a Rails feel
  2. A Knockout friendly syntax for binding to the values of a model to build an href
  3. Automatic replacement of values in the url with values from the model
  4. JavaScript generation (OK, not really a goal, but a necessary evil I suspected)

Solution:

I started by using AttributeRouting (from AttributeRouting.net). It offers a simple syntax for creating routes (and naming them):

[GET("research/{id}", RouteName = "research_details")]
public ActionResult Details(string id)
{
    var model = new ResearchDetailsViewModel();

In RouteConfig.cs, code gathers the list of named routes, and builds a snippet of JavaScript which will be inserted on every page (yes, it would be nice if this were cached, see refactoring below):


var named = new List();
foreach (var route in RouteTable.Routes)
{
AttributeRoute r = route as AttributeRoute;
    if (r != null)
    {
       if (!string.IsNullOrWhiteSpace(r.RouteName))
       {
          named.Add(r);
       }
    }
}
if (named.Count > 0)
{
    StringBuilder js = new StringBuilder();
    foreach (var namedRoute in named)
    {
       js.AppendFormat(@"function {0}_url(model){{ return buildUrl(""{1}"", model); }}",
       namedRoute.RouteName, namedRoute.Url);
       js.AppendLine();
    }
    RouteScript = js.ToString();
}

The name of the function is based on the RouteName property of the attribute.

I added a function called buildUrl to a common JavaScript file:


function buildUrl(url, model) {
    // unfixed if there's not a thing
    if (typeof model === 'undefined' || model === null) { return url; }

    var propValue;
    for (var propName in model) {
        if (model.hasOwnProperty(propName)) {
            propValue = model[propName];
            if (ko) { propValue = ko.utils.unwrapObservable(propValue); }

            if (typeof propValue === 'undefined' || propValue === null) {
                propValue = "";
            } else {
                propValue = propValue.toString();
            }
            url = url.replace('{' + propName.toLowerCase() + '}', propValue);
        }
    }
    return url;
}

The code above validates there is data and then proceeds to loop through every property of the model to see if there might be a replacement needed within the passed URL. If Knockout is available (if ko check), then it unwraps the value as the code needs the raw value from this point onward.

It lowercases each property name and tries a replacement.

So, if the string was /research/{id}/{category}, and the model has properties named, Id and Category, the values of each will be substituted and returned as a full string. For example, it might be /research/123/Coding.

Next, a simple example of using some JSON data:

$(function () {
    var vm = {
        url: function ($data) {
            return app_url(research_details_url($data));
        },
        data : ko.observable()
    };

    $.getJSON(app_url("api/research/")).success(function (data) {
        vm.data = ko.mapping.fromJS(data);
        ko.applyBindings(vm);
    });
});

Then, finally, the Knockout template:

<div data-bind="foreach: data.research">
    <h3 class="title" data-bind="text: Title"></h3>
    <div>
        <a data-bind="attr: { href: $parent.url($data) }">Details</a>
    </div>
</div>

By using $parent in the template, Knockout points at the view model in this case (vm). On the view model, I added a function called url. Right now, I’ve hard coded it to point to a specific dynamically generated function called research_details_url. It’s passing the array item (which simply has an Id & Title property). So, the data-bind for the anchor (A) element assigns the value of the computation to the href attribute.

app_url:


function app_url(url) {
return "@Request.ApplicationPath" + url;
}

As I said, it’s not super elegant, but it achieved my basic goals.

Elegance rating: 59%

Function rating: 100%

Need of some refactoring rating: 99%

One Comment

Comments are closed.