Razor Pages, TypeScript and Knockout

In this article, I look at the steps required to get TypeScript up and running in the context of a Razor Pages application. I discuss why you might want to do this, and I use Knockout for the purposes of demonstration. By the end of the article, I will have configured and used TypeScript to replicate part of the final code for the Single Page Application tutorial from learn.knockout.com.

But first, why Knockout? Is it still even a thing?

So here it is: I was at the DDD Conference at Microsoft's UK office in Reading, Berks a few weeks ago and attended a session on Blazor delivered by Jospeh Woodward, which was pretty good. During the session, Joseph got interactive and asked for a show of hands: "Who here still uses jQuery?" The question was asked in the same tone that you might use when asking "who here still smokes in the car with children in the back?" No one (including me, even though I do - still use jQuery extensively, that is) put their hand up, perhaps sensing the possible reaction if they did:

Are you still using jQuery?

It just so happened that this question built on a comment from Steve Sanderson to me when he showed how to use databinding in Blazor to avoid having to deal with the DOM directly. And it all got me to thinking that perhaps its about time that I should explore alternatives to using jQuery solely to update parts of pages.

As it happens, I have a large ASP.NET Web Pages LOB application (written in VB, would you believe?) that needs to be rewritten from the ground up. I have no interest in converting thie whole thing into a Single Page Application, so full SPA frameworks (which I would have to learn) are not required, but I could use an SPA approach to specific features, similar to the one demonstrated in the Knockout tuorial. I am happy using Razor Pages routing to navigate between features and to use server-side code to render as much of the initial state as possible. All I really need is a client-side framework that will help keep views in sync with the current state of their underlying model. There are plenty of choices for this, but Knockout is mature, stable, and pretty much all the questions about its use have been asked and answered somewhere, just a Google search away. I've written about using Knockout a couple of times in the past, so I have a reasonable idea how it works.

TypeScript

TypeScript has been around for quite a while now. It's a programming language based on JavaScript. Source code files end in .ts. The TypeScript compiler generates JavaScript files as output, translated from the TypeScript code. This process has been named Transpiling. One of the main benefits of using TypeScript is that it supports static typing, which enables IDEs to perform static analysis on the code, reporting compile-time errors. This is much better than (mis)typing Javascript, and then only discovering mistakes when the page doesn't behave as expected and the browser console is clogged with error messages. So I have decided to write all the scripts in TypeScript. Again, this is a new option for me.

TypeScript is (should be) enabled in Visual Studio by ensuring that the JavaScript and TypeScript languages support option is checked under Node.js development when you choose features:

TypeScript Support

However, this may not accurately reflect whether TypeScript is actually available to you. At least, that was my experience. You should double check by going to Tools » Options » Text Editors and make sure that JavaScript/TypeScript appears in the list:

TypeScript Support

This was missing in my case and a full repair to my Visual Studio installation was needed. 

Configuring TypeScript

This section assumes that you have already created a Razor Pages application. Once you have TypeScript support sorted in your IDE, you need to configure it for the application. You can accomplish this by adding a tsconfig.json file to the root of the project:

tsconfig.json

This is the default content generated by Visual Studio which will need a bit of modification:

The contents of the file provide instructions to the TypeScript compiler. I add "compileOnSave": true, before the compilerOptions section to ensure that the compiler runs whenever i make a change to a .ts file, and I remove wwwroot from the exclude section because I will place my TypeScript file there. I also change the target to ES6 (which means that I won't be able to support IE11, but that is my pleasure), and add an entry for module with a value of amd. This covers the module loader which I discuss later. The resulting tsconfig.json file should look like this:

TypeScript should compile every .ts file in the project except those in the node_modules folder and output the JavaScript in the same location. The compiler will run every time you save a TypeScript file ("compileOnSave": true) and when you build the project in Visual Studio. At the moment, that process will result in an error message:

Error TS18003 No inputs were found in config file 'PathTo/tsconfig.json'. Specified 'include' paths were '["**/*"]' and 'exclude' paths were '["node_modules"]'.

If there are no TypeScript files in the locations specified in the tsconfig.json file (which is everywhere in the project when no include paths are specified), the compiler returns the error above. To prevent this error, you just need to add an empty .ts file to the project somewhere. If you want to follow this article along, simply add a file named webmail.ts to wwwroot/js.

Obtaining Knockout

Because you have nodejs installed, you can use its package manager to install Knockout. There are countless other options for obtaining external libraries, some of which I may explore at a later date - if they haven't already gone out of fashion. The command for installing Knockout using the node package manager is npm install knockout. If you are using Visual Studio, you can execute this command from the Package Manager Console. Otherwise you can use the Terminal in VS Code. In both cases, a node_modules folder will be created. Visual Studio will create the folder in the solution folder, i.e. one level up from the project. VS Code will create in the node_modules folder in the location that the command is executed - usually the root of the project. Either way, you will want to copy the knockout-latest.js file from the download (node_modules/knockout/build/output) into your wwwroot folder. I create a folder structure that follows those for existing third party libraries: wwwroot/lib/knockout/dist/ and place the knockout file there, renaming it to knockout.js.

Declaration Files

JavaScript is not statically typed. IntelliSense, code completion tools and static code anaysers have little to work with when you are typing JavaScript code. You might get some code completion suggestions based on previous usage in the same file if you are lucky. Because you can opt in to static typing with TypeScript, you can define the data types of variables etc. And if you do that. IntelliSense is a lot more helpful and the compiler can catch errors as you type. That's OK for the basic types supported by TypeScript such as string, number, date and so on, but what about types defined in third party libraries?

Declaration files have a .d.ts extension and "describe the shape of an existing JavaScript codebase to TypeScript". Essentially, they enable you to work with third party libraries such as Knockout in a strongly typed manner in TypeScript by enabling various advanced IDE features that you usually rely on when working with statically typed languages such as IntelliSense. Declaration files are a design-time tool. They are not included as part of the published output.

Declaration files are available from https://github.com/DefinitelyTyped/DefinitelyTyped where there are thousands of them for all sorts of libraries. That makes it kind of difficult to locate the one that you are interested in to get its installation command, but Microsoft have developed a search engine for declaration files here: https://microsoft.github.io/TypeSearch/.

The command for installing Knockout is npm install --save @types/knockout. This will create a new folder in node_modules called @types. Inside that, you will find a folder for Knockout containing an index.d.ts file. It doesn't matter whether this is located in the project folder or the soution folder. The TypeScript compiler will find it. You should avoid any temptation to create a copy of this file anywhere else in the project. If you do, the compiler will generate a number of "duplicate identifier" errors, not knowing which version of the declarations to use.

Module Loading and Dependency Resolution

When you create your Knockout ViewModel code in TypeScript, you will create a module. It will have a dependency on Knockout. At runtime, something needs to be able to resolve and load the dependency - a Module Loader.  The recommended module loader for use in web applications is RequireJS. You can install this using the npm command: npm install requirejs. Once installed, you may need to move a copy of the require.js file to a location that's accessible to browsing. To keep things consistent in relation to third party script libraries, I copy it to wwwroot/lib/requirejs/dist/.

Some Code, Finally

Now that the configuration and setup is complete, it's time to code. The end result of code will take you to the third stage in the Single Page Application tutorial on the Knockout site. At this point, you will have a web mail application that displays a lsit of mail by folder, and enables you to view the details of individual emails.

Mail Data Service, Entities and an API

The mail will come from a service. So to begin with, you need some data. I borrowed the data from the Knockout tutorial site. You can get it here. Create a folder named Data in the application and store the content of the Gist as webmail.json in it.

The code for the model follows:

These classes represent the entities that the application will work with. You may need to change the namespace to suit your application. I place this file in a folder named Models in the root of the application. Then I create a folder named Services and add a new C# class file with the following content:

And then register the service with the Dependendency Injection system (line 11):

Finally, I add a Web API controller named MailController.cs with the the following content:

User Interface

The UI is a Razor Page named WebMail. It includes the following markup in the content page:

The page references a CSS style sheet named webmail.css (also acquired from the Knockout tutorial):

The style sheet is included in a RenderSection, so a complementary method call is required in the Pages/Shared/_Layout.cshtml file

@RenderSection("Styles", required: false)

This line is placed just before the closing </head> tag. Another line of code is placed just before the RenderSection call for scripts section just before the closing </body> tag at the end of the file to bring Requirejs into play:

<script src="~/lib/requirejs/dist/require.js"></script>

The Scripts

Finally, Here's where the action happens. First, I shall provide the JavaScript version from the tutorial:

I won't go into too much detail about how Knockout works. That isn't the focus of this article. But the code declares a ViewModel with some properties representing the mailbox folders, the currently selected folder and the currently selected email message. The goToFolder function enables the user to move from one folder to another, and the contents of the folder are obtained using the jQuery AJAX get method from a server-side service. The goToMail function uses the same technology to obtain the selected message from the mail service. The firing of these methods is controlled by declarative bindings to click events in the UI. Data is bound to the UI using similar bindings.

Here is the typescript version which is added to the wwwroot/js folder as webmail.ts. When the TypeScript compiler runs, a file named webmail.js is generated in the same location:

The functionality of the code is identical. The import statement at the top of the file resolves Knockout as a dependency. The TypeScript code is based on EcmaScript 2015, which introduced classes, modules, arrow functions, string interpolation and much more to JavaScript.

The main differences between this and the original JavaScript is that the ViewModel is defined as a class, and its members are declared along with their data types. The KnockoutObservable<T> type is made available to the code via the declaration file. The code also makes use of the Fetch API rather than the standard XmlHttpRequest.

I have also declared a couple of entity classes at the end of the file, adding the bare minimum members to support IntelliSense:

TypeScript Intellisense

And here is the default view when the application is run: 

webmail

The full code for the application is available at this github repo.

Summary

In truth, the UI used for this demonstration could have been a plain HTML file. There is no real use of Razor in it. However, I could have made the arry of mail folders part of a Razor PageModel, along with the content of the inbox which forms the default view and rendered that on the server, which would speed the initial page load time.

Moving away from using jQuery for updating areas of the DOM is something that I have been meaning to do for some time. Knockout is an established library with a relatively gentle learning curve, which is why I chose to use it. TypeScript is new to me. Getting it up an running in VS was not without its pain points (as far as my experience goes) which is why I chose to feature the steps required to accomplish as a blog article. I am sold on the idea of using it, however, although I am sure there will be other bumps along the road as I progress. I'll stick with it. It's a nice way to write JavaScript.

Ultimately however, I am hoping that the Blazor experiment becomes an official thing from Microsoft. Then I will be able to just use C# for client-side development.