Generate Knockout Viewmodels using T4 templates






4.98/5 (19 votes)
Use a T4 template to generate Knockout-viewmodels based on .NET classes.
Introduction
When developing JSON-only web apps (or mixed web apps) and you're using Knockout to bind your JavaScript models to the UI, you will have noticed how tedious the translation from .NET classes to Knockout models is. This article describes a solution to this problem by using T4 templates to generate the JavaScript Knockout models automatically based on the .NET classes. The resulting Knockout models are extendable so as to be able to add additional functions and (computed) properties client-side. Finally, I also added an IsDirty feature that can indicate if the model has been modified since it's data was set.
Background
For a new project that I've started working on, I went for a mixed approach of JSON-based webapp combined with some ASP.NET MVC. The server-side consists of a REST WCF-service and a NHibernate datalayer. I use AutoMapper to fill the properties of my viewmodels based on my business objects. But I had the problem that when I sent the viewmodels client-side (serialized in JSON) I had to create similar Knockout-viewmodels in JavaScript, which is a pretty tedious task that could easily be automated.
I had read the article T4 transformation toolkit on Scott Hanselman's blog and I had already used T4 templates in other scenario's. I was pretty sure those templates could also be used to generate JavaScript viewmodel-classes.
I started to write a T4 template that would pick up my .NET classes and convert them to JavaScript Knockout-viewmodels. Oleg Sych's blog and especially his post How to generate multiple outputs from single T4 template were extremely valuable. Once I had the Knockout-viewmodels defined, I noticed that I'd need the ability to add functions and properties to them client-side. But I couldn't modify the generated files because all modifications would be lost once I ran the T4 generator again (e.g. when the .NET viewmodel was modified). This post on StackOverflow (cfr. answer by Eric Barnard) solved my problem. In my application, the Save and Cancel buttons are only visible once the model has been modified by the end user. To determine if the model is modified ('dirty') I've added the code from this article by Ryan Niemeyer.
Using the template(s)
You can find the template files in the included Demo solution, in the TRIS.ViewModel project.
There are actually 2 template files: the generator and the actual template.
The generator will process all .NET viewmodels and use the template to generate the JavaScript files. The generator is the
ViewModelGenerator.tt file while the actual template
is the ViewModel.tt file. Because it is the generator is being executed, its 'Custom Tool' property is set to
TextTemplatingFileGenerator
(in the Visual Studio properties). While the
'Custom Tool' property of the ViewModel.tt file is set to None (to avoid compilation errors because the
ViewModel.tt file can't be generated on it's own).
Once the templates are in place, you can use the 'Run Custom Tool' menu option on the ViewModelGenerator.tt file to (re)generate the JavaScript viewmodels.
Note that every time that you modify a .NET viewmodel, you'll have to recompile your assembly containing the viewmodel and run the custom tool on the generator to keep your JavaScript viewmodels up-to-date.
The project requires that you use the (very good) Json.NET library for serializing and deserializing your objects. You can fetch it via NuGet or download it from
CodePlex. For successfully passing the objects from and to the client, is is required to set at least the TypeNameHandling
property to Objects
in the JsonSerializerSettings: this will add a $type property to all serialized objects. More important: it expects this property to
exist (and be the first property) when deserializing JSON objects back into their corresponding .NET type.
var json = Newtonsoft.Json.JsonConvert.SerializeObject(o, new Newtonsoft.Json.JsonSerializerSettings()
{
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,
ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore,
DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Local
});
The following chapters describe how the templates work in this demo. To really understand the workings of both templates, you can refer to this article: Oleg Sych - How to generate multiple outputs from single T4 template.
The ViewModelGenerator.tt template
var list = new List<Type>();
foreach (Type type in System.Reflection.Assembly.GetAssembly(typeof(TRIS.ViewModels.BaseViewModel)).GetTypes())
{
if (type.IsAbstract) continue; // only generate JS viewmodels for non-abstract classes
list.Add(type);
}
foreach (var type in list)
{
var vmtemplate = new ViewModelTemplate(type, list);
vmtemplate.Output.Project = @"..\TRIS.Web\TRIS.Web.csproj";
vmtemplate.Output.File = @"Scripts\viewmodels\" + type.Name.ToLower() + ".js";
vmtemplate.Render();
}
This template will iterate over all .NET types in the viewmodels-assembly and will add each type that is found to a list. That is the reason that I place my viewmodels in a separate assembly (other strategies are possible, though). An exception is made for abstract types: I use this trick to avoid that a viewmodel is generated for my base-viewmodel class (BaseViewModel.cs). Again: other strategies are possible. Once the list is ready, I run the generator on each of these types. The project to which the file must be added is specified along with the location where the file must be placed.
The ViewModel.tt template
This template will generate the Knockout-viewmodel in JavaScript. The template is run by the ViewModelGenerator template which passes the Type
to generate and also the
list of the other mapped types. This list is required to allow the generator to be able to detect that a property's type is actually another viewmodel that must be mapped.
The template will generate a viewmodel according the following rules:
- Each viewmodels' first property must be $type. This is required to allow Json.NET to be able to deserialize the incoming JSON back into the original .NET ViewModel. Although the order of serialization isn't guaranteed in JSON, I found that if I explicitely declared this property (as a non-Knockout observable) it was always sent first. If I declared this property as a Knockout-observable, I had no guarantee that the property would be serialized as first.
-
Then, the template iterates over each property of the .NET viewmodel and determines if it should be mapped to a Knockout-observable or a Knockout-observableArray. Enumerables are mapped to an
observableArray
unless they're strings or arrays of bytes (although these are enumerables, they shouldn't be mapped to anobservableArray
).
All the other properties are mapped asobservable
s. -
Once the properties are added, the
init
-function of the prototypeextendable
-object will be invoked. The init-function invokes all extenders of the viewmodel. Theextendable
object allows you to register extender-functions which add properties and functions to the viewmodel. As those properties and functions might depend on the presence of the properties of the viewmodel, the init-function is invoked after those properties are added to the viewmodel.
It is theextendable
prototype that will allow us to register additional properties and functions outside of the auto-generated JavaScript file. -
Then, a
setModel
-function is added. The purpose of the SetModel-function is to pass it a plain JavaScript object coming from the server and it will set all the viewmodel observables to the values of the passed object.
If the value is an enumerable of objects, it'll create a viewmodel for those objects, invokesetModel
in turn on those viewmodels and add them to theobservableArray
.
If the value is a serialized date, it'll be converted to a JavaScript Date object.
Finally, if dirty-tracking is enabled, the flag will be (re)set to false. -
After the viewmodel is defined, it's prototype is set to a new instance of
extendable
(in JavaScript, inheritance is prototype-based). This allows extending the viewmodel in the page scripts.
Exploring the demo solution
Note: the demo application requires T4 Toobox to be installed. Refer to the 'Used tools and libraries' section for a link.
The demo application that can be downloaded with this article, demonstrates working with the templates. It consists of an ASP MVC4 solution with an assembly containing business objects, an assembly containing the viewmodels and the MVC project. The MVC project contains a WCF Rest service and a webpage that interacts with it (the /Home/Index page). The demo solution doesn't contain data-access, authentication, validation, etc... : it only is there to illustrate the technology to generate the viewmodels and use these on the client-side.
The projects in the solution are:
- TRIS.BusinessObjects: the assembly containing the business objects. Note that the business objects inherit from the
BaseBO
class. - TRIS.ViewModels: the assembly containing the viewmodels. Note that the viewmodels inherit from the
BaseViewModel
which mirrors theBaseBO
class and is abstract. - TRIS.Web: the project containing the MVC website, the scripts and the WCF REST service.
The WCF service exposes 3 methods: one for requesting a list of cars (simple viewmodels), one for requesting one specific car (extended viewmodel) and one for inserting/updating a car
object.
The page that communicates with the service extends the generated car viewmodel with some properties and a method. It also adds the dirty-detection feature to the viewmodel.
Important: in the Scripts-directory you'll find a framework.js script. This script contains the definition of the extendable
object and the trackDirty
function.
The templates count on the inclusion of the definition of the extendable
object so you'll have to add this code to one of your files that are always referenced when the
viewmodels are referenced (and the definition of the extendable
object must come before the references to the viewmodel files).
Unless you do not want to use the dirty-tracking feature, you'll also have to add the trackDirty
function to your pages, before the reference to the viewmodel files.
The /Home/Index JavaScript code
First of all, a reference to the Knockout and the framework-script is added (note that the jQuery script is added in the master page). After those scripts, references to the viewmodel scripts are added.
I start my script by extending the generated viewmodel(s) to add page-specific functionality:
carviewmodel.prototype.extend(function () {
var self = this;
// demonstration: computed property
self.euronorm = ko.computed(function () {
if (self.co2() < 100)
return "euro1";
else if (self.co2() < 200)
return "euro2";
else
return "euro3";
});
self.save = function() {
var json = ko.toJSON(self);
$.post("/CarService.svc/car", json, function (result) {
var car = JSON.parse(json);
self.setModel(car);
});
}
});
Note the first line: var self=this
. Refer to this article for more information about this
pattern (chapter "Managing 'this'").
Invoking the service from JavaScript is just an AJAX call. You could use the XMLHttpRequest
object or one of the jQuery's AJAX wrappers:
$.ajax("/CarService.svc/car/" + item.id()).done(function (result) {
var car = JSON.parse(result);
var carvm = new carviewmodel();
carvm.isDirty = trackDirty(carvm); // enable 'dirty' tracking
carvm.setModel(car);
self.current(carvm);
});
Once the result is in, parse it back to a JavaScript object (unless you're specifying dataType
='json' when executing your AJAX call) and instantiate the
appropriate viewmodel. Eventually, add dirty-tracking to the viewmodel. Then, invoke the
setModel
function on the viewmodel, passing it your JavaScript object.
Used tools and libraries
If you're considering to edit or develop your own T4 templates, check out the following libraries and tools:
- Oleg Sych's T4 toobox (actually, installation of this library is required to be able to generate the viewmodels)
- T4 editor by Tangible Engineering (there's a free version available). This tool will also provide syntax coloring for .tt-files.
When working with dates in JavaScript, I'm using the Moment.js library.
Points of Interest
Although in my demo project I'm setting the Json.NET DateTimeZoneHandling
setting to 'Local', in real-life projects I'm storing all my dates in UTC (and using the
DateTimeZoneHandling
'RoundTrip'-setting). They're sent in UTC over the wire and are converted to and from
UTC on the client-side. It's the kind of thing
that is best foreseen from the start.
Update: I recently discovered that on Safari, the ISO-datetime's aren't parsed. I've updated the viewmodel-generator to parse datetimes with the moment.js library meaning the generated viewmodels now have a dependency on that library.
History
- 2013-04-16: Submitted to CodeProject.
- 2013-04-24: Discovered a problem on Safari with the date/time conversion from an ISO-string. Adapted example and article. Also added a one-to-many relationship in the viewmodel.
- 2013-05-18: Added that the T4 Toolbox is required to be able to generate the viewmodels.
- 2013-05-26: Added support for lists of ints, strings,... in the viewmodels