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:
- Named routes, with a wee bit of a Rails feel
- A Knockout friendly syntax for binding to the values of a model to build an href
- Automatic replacement of values in the url with values from the model
- 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):
[sourcecode language=”csharp”]
[GET("research/{id}", RouteName = "research_details")]
public ActionResult Details(string id)
{
var model = new ResearchDetailsViewModel();
[/sourcecode]
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):
[sourcecode language=”csharp”]
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();
}
[/sourcecode]
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:
[sourcecode language=”javascript”]
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;
}
[/sourcecode]
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:
[sourcecode language=”javascript”]
$(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);
});
});
[/sourcecode]
Then, finally, the Knockout template:
[sourcecode language=”html”]
<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>
[/sourcecode]
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:
[sourcecode language=”javascript”]
function app_url(url) {
return "@Request.ApplicationPath" + url;
}
[/sourcecode]
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%