The Practical Client

Building and Testing a View Model in TypeScript

Peter Vogel continues to build out a TypeScript project by defining a view model. Along the way he looks at defining interfaces, setting up constructors, creating optional parameters and initializing arrays in TypeScript.

So far in this TypeScript project, I've defined my business entities and set up my development environment to support test-driven development (TDD). Now it's time to start building a Web page.

I prefer to implement my client-side code using the Model-View-ViewModel (MVVM) pattern. With that pattern, the entity objects I defined earlier form my model and the HTML I'll eventually write will form my view. My view model will be a class holding all the TypeScript code that gets and updates my entities and handles interacting with the HTML in my view. What I like about having a view model is that I can test it independently of my Web page using the test runner I set up in my June column; I don't have to open a Web page to execute my TypeScript code.

Integrating Interfaces for a View Model
When I define my view model, I first define an interface and then write the ViewModel class and implement it. To be honest, I don't know that I'll ever take advantage of having an interface for my view model that's defined separately from my ViewModel class -- but I've gone back often enough to have my classes implement interfaces that I now do it automatically. (I went back and defined interfaces for the entity objects I created in my earlier column, for instance.)

The interface for my view model defines two methods: one to retrieve a specific customer and one to retrieve all the customers. The interface also includes methods to delete, update and insert customers. Finally, the view model has two properties: one that exposes a single Customer object and one that exposes a collection of Customers (which is read-only). Because I want to use this interface outside of the module, I declare it with the export keyword:

export interface ICustomerVM 
{
  fetchAllCustomers();
  fetchCustomer(id: number);
  deleteCustomer(id: number);
  updateCustomer(cust: AdventureWorksEntities.ICustomer);
  insertCustomer(cust: AdventureWorksEntities.ICustomer);
  getCustomer(): AdventureWorksEntities.ICustomer;
  setCustomer(cust: AdventureWorksEntities.ICustomer);
  getCustomers(): AdventureWorksEntities.ICustomer[];
}

To tie a class to an interface in TypeScript, I just use the implements keyword with the interface's name. At the top of this class, I also define two fields that hold the data for my view model's two properties. The cust field holds a single Customer object, while the custs field holds the collection of Customers:

export class CustomerVM implements ICustomerVM {
  private cust: AdventureWorksEntities.ICustomer;
  private custs: AdventureWorksEntities.ICustomer[];

The two properties in the view model (Customers and Customer) just update and return their respecting backing fields:

getCustomer(): AdventureWorksEntities.ICustomer
{
  return this.cust;
};
setCustomer(cust: AdventureWorksEntities.ICustomer)
{
  this.cust = cust;
};
getCustomers(): AdventureWorksEntities.ICustomer[]
{
  return this.custs;
};

To support testing these properties, I have my view model's constructor accept a Customer class or a collection of Customers and move them to the appropriate backing field. I can use these parameters to make sure the properties on my view model work without having to retrieve objects from the server. Because I probably won't be using these parameters outside of testing, I'll declare them as optional by decorating the parameter names with a question mark. If I follow the parameter name with an equals sign, I can also provide a value for the parameter when it's omitted. To initialize my collection parameter without specifying the number of rows it should have, I just set the parameter to an open/close set of square brackets ([]).

When I'm done, my view model's constructor (which in TypeScript must be called "constructor") looks like this:

constructor(cust?: AdventureWorksEntities.ICustomer = null,
  custs?: AdventureWorksEntities.ICustomer[] = [])
{
  this.cust = cust;
  this.custs = custs;
}

The rest of the methods I'll leave empty for now. This code also demonstrates how easy it is to define a class in TypeScript if you've defined the interface first -- just copy the interface members into the class and put curly braces before each semicolon:

fetchAllCustomers() { };
fetchCustomer(id: number) { };
deleteCustomer(id: number) { };
updateCustomer(cust: AdventureWorksEntities.ICustomer) { };
insertCustomer(cust: AdventureWorksEntities.ICustomer) { };

Does It Work?
Now I want to test what I've done. I switch to my Test project, add a TypeScript file called CustomerOrderMVVMTests.ts, and delete all the sample code in the file. From the Scripts folder in my application, I drag the files holding my entities and my view model code over to the test class editor window and drop them into my TypeScript file. From the script folder of my Test project, I drag and drop my qunit.js file (the tool I use for writing tests) and its TypeScript definition file (qunit.d.ts). Those actions add references to my TypeScript file that will let me use the objects those files contain (and, more important, that will let the TypeScript compiler make sure I'm using them correctly).

After I add the skeleton for my first test, my TypeScript test file looks like this:

/// <reference path="Scripts/qunit.js" />
/// <reference path="Scripts/typings/qunit/qunit.d.ts" />
/// <reference path=
      "../PJSalesOrder/Scripts/AdventureWorksEntities.ts" />
/// <reference path=
      "../PJSalesOrder/Scripts/SalesOrderMvvm.ts" />

test("cust property on creating CustomerVM", function () 
{
    
});

For my first test, I create a single Customer object, pass it to my view model's constructor, and then try to retrieve that object from my view model's Customer property. In TypeScript, that test looks like this:

var cust: AdventureWorksEntities.ICustomer;
var custVM: SalesOrderMvvm.ICustomerVM;

cust = new AdventureWorksEntities.Customer(
  123, "Peter", "Vogel");
custVM = new SalesOrderMvvm.CustomerVM(cust);
ok(custVM.getCustomer().getFirstName() == "Peter", 
  "customer property not set from constructor");

After rebuilding my project, the test appears in the Visual Studio 2012 test runner. I right-click on the test, select Run Selected Tests, and I'm rewarded (eventually) with a green checkmark indicating the test ran successfully.

My biggest problem is that I have to remember to save my code after making a change to it and before rerunning any of my tests. That's easy to remember the first time I run the test because the tests don't appear in the Visual Studio 2012 test runner until I save my code (or build it, thanks to installing Web extensions). But once the test appears in the test runner, I find I often forget to save my work after making changes to the code in my test or my view model. If I omit that step, I end up running the previously saved version of my code -- and then wonder why I'm not seeing any changes in my test results.

The test for the Customers collection property is only slightly more complicated than my first test. To initialize my collection to a specific number of entries, I use an Array object, passing the number of rows I want. I then load the collection with Customer objects, instantiate my CustomerVM, pass the collection to the view model and check the Customers property:

test("custs collection property on creating CustomerVM", function () 
{
  var cust: AdventureWorksEntities.ICustomer;
  var custVM: SalesOrderMvvm.ICustomerVM;

  var custs: AdventureWorksEntities.ICustomer[];
  custs = new Array(2);
  custs[0] = new AdventureWorksEntities.Customer(123, "Peter", "Vogel");
  custs[1] = new AdventureWorksEntities.Customer(456, "Jan", "Vogel");
  custVM = new SalesOrderMvvm.CustomerVM(null, custs);
  ok(custVM.getCustomers()[1].getFirstName() == "Jan", 
    "customers collection not set from constructor"); });

For the sake of completeness, here's the test that sets the Customer property and then retrieves the value from it to make sure that works, also:

test("cust property CustomerVM set and get", function () {
  var cust: AdventureWorksEntities.ICustomer;
  var custVM: SalesOrderMvvm.ICustomerVM;

  cust = new AdventureWorksEntities.Customer(123, "Peter", "Vogel");
  custVM = new SalesOrderMvvm.CustomerVM();
  custVM.setCustomer(cust);
  ok(custVM.getCustomer().getFirstName() == "Peter", 
    "customer property not set ");
});

After rerunning all of my tests (including the ones I set up in my last column for my entity objects), I'm ready to move on. I'll do so next month.

I use Knockout.js to build my page MVVM structure, so I also need to integrate that library into this project. Additionally, I should figure out how to use jQuery from TypeScript -- it won't do me much good to have a working view model if I can't make it function in a page. In the meantime, I now have to figure out why my qunit.js file is generating hundreds of errors but still seems to be working. Ah, well: It's still beta software. But it's possible that by the time you read this, version 0.9 of TypeScript will be available (I'm using 0.8.3.1).

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube