Click here to Skip to main content
15,882,113 members
Articles / Programming Languages / Javascript

Generate Knockout Viewmodels using T4 templates

Rate me:
Please Sign up or sign in to vote.
4.98/5 (19 votes)
26 May 2013CPOL9 min read 79.6K   1.9K   70   23
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.

Image 1

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.

Context menu opened on the Run Custom Tool option

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.

C#
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

C#
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 an observableArray).
    All the other properties are mapped as observables.
  • Once the properties are added, the init-function of the prototype extendable-object will be invoked. The init-function invokes all extenders of the viewmodel. The extendable 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 the extendable 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, invoke setModel in turn on those viewmodels and add them to the observableArray.
    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 the BaseBO 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:

Java
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:

Java
$.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:

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  

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
CEO TRI-S bvba, Cogenius bvba
Belgium Belgium
I'm working since 1999 in an IT environment: started developing in PROGRESS 4GL, then VB6 and am working since 2003 with C#. I'm currently transitioning to HTML5, CSS3 and JavaScript for the front-end development.
I started my own company (TRI-S) in 2007 and co-founded another one (Cogenius) in 2012.
Besides being a Microsoft Certified Professional Developer (MCPD) I'm also a Microsoft Certified Trainer (MCT) and am teaching .NET and JavaScript courses.

Comments and Discussions

 
QuestionHow to generate for Dynamic Json model Pin
Member 1118590519-Jan-15 0:48
Member 1118590519-Jan-15 0:48 
QuestionMy vote is 5 and I have an improvement Pin
Christian Schiffer23-Oct-14 13:59
Christian Schiffer23-Oct-14 13:59 
GeneralMy vote of 5 Pin
Martin Lottering13-Nov-13 2:06
Martin Lottering13-Nov-13 2:06 
Very nice. Thanks.
GeneralMy vote of 5 Pin
zolitamasi11-Jul-13 10:13
zolitamasi11-Jul-13 10:13 
QuestionProblem if attribute is list<string> Pin
Obiwan00725-May-13 22:14
Obiwan00725-May-13 22:14 
AnswerRe: Problem if attribute is list<string> Pin
Xavier Spileers25-May-13 23:55
Xavier Spileers25-May-13 23:55 
GeneralMy vote of 5 Pin
Rui Jarimba20-May-13 2:46
professionalRui Jarimba20-May-13 2:46 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA10-May-13 18:28
professionalȘtefan-Mihai MOGA10-May-13 18:28 
Questionko mapping plugin Pin
Trojaan24-Apr-13 9:19
Trojaan24-Apr-13 9:19 
AnswerRe: ko mapping plugin Pin
Xavier Spileers24-Apr-13 21:36
Xavier Spileers24-Apr-13 21:36 
GeneralRe: ko mapping plugin Pin
D Rhys Jones10-May-13 0:19
D Rhys Jones10-May-13 0:19 
GeneralRe: ko mapping plugin Pin
sanjozko13-Jan-14 20:27
sanjozko13-Jan-14 20:27 
GeneralRe: ko mapping plugin Pin
BiggerNoise6-Aug-13 4:56
BiggerNoise6-Aug-13 4:56 
QuestionTagetPath not found Pin
doc-zoidberg19-Apr-13 4:43
doc-zoidberg19-Apr-13 4:43 
AnswerRe: TagetPath not found Pin
Xavier Spileers19-Apr-13 5:04
Xavier Spileers19-Apr-13 5:04 
GeneralRe: TagetPath not found Pin
doc-zoidberg19-Apr-13 5:26
doc-zoidberg19-Apr-13 5:26 
GeneralRe: TagetPath not found Pin
hardik vyas115-Sep-13 22:07
hardik vyas115-Sep-13 22:07 
GeneralRe: TagetPath not found Pin
Xavier Spileers5-Sep-13 22:50
Xavier Spileers5-Sep-13 22:50 
GeneralRe: TagetPath not found Pin
hardik vyas115-Sep-13 23:37
hardik vyas115-Sep-13 23:37 
GeneralRe: TagetPath not found Pin
Xavier Spileers6-Sep-13 0:13
Xavier Spileers6-Sep-13 0:13 
GeneralRe: TagetPath not found Pin
hardik vyas116-Sep-13 0:38
hardik vyas116-Sep-13 0:38 
GeneralRe: TagetPath not found Pin
Xavier Spileers6-Sep-13 0:48
Xavier Spileers6-Sep-13 0:48 
GeneralMy vote of 5 Pin
Espen Harlinn18-Apr-13 1:19
professionalEspen Harlinn18-Apr-13 1:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.