The Practical Client

Creating a Client-Side Master-Detail Page in an AJAX App with TypeScript

Using Breeze and Knockout, you just need a few lines of TypeScript code to create a master/detail page that retrieves records from the server when it has to, but skips the trip to the server when it isn't necessary.

The two slowest things you can do in data processing are to make a call to another computer and read or write to your hard disk. And, in an AJAX application, when you're making a call from your client-side code to a service on your Web server, you're not only doing both of those things often, you're doing it using the world's slowest protocol -- HTTP. The secret to speeding up your application is to manage those server-side data-retrieval calls intelligently.

In this column I'll show how to manage your client-side objects and server-side calls in an AJAX application to provide the user with the up-to-date data while reducing trips to the server. To help me do all of this, I'm writing my client-side code in TypeScript, using Knockout to get my data onto the page, and leveraging Breeze to manage my client-side objects and make my server-side requests.

The Problem
In the application I'm building, I have a table of customers I'm displaying to my users in a Web page. Every customer can have sales orders. When the user initially retrieves the page, I retrieve all Customer objects from the server, but I don't retrieve the SalesOrder objects for those Customer objects. My reasoning for not retrieving the SalesOrders in the initial request is most of the time users only look at the sales orders for a few customers. Retrieving all the sales orders for all the customers would only delay the original display of my page.

Only when the user selects a customer do I retrieve and display the SalesOrder objects for that customer. But, as long as I'm going to make the trip to the server, I'll also re-fetch the Customer object in case someone else has made changes to the customer since my user retrieved it. My reasoning is if I have to go back to the server, I might as well make the most of the trip and get the latest version of the selected customer's data (I'm assuming here the cost of retrieving customer data along with the customer's related sales order data is negligible).

Retrieving the latest Customer object does create a problem at the client, however, because I'm letting the user make changes to the Customer object displayed in the page. If the user's made changes to the client-side Customer object, I don't want to update the client with the retrieved Customer object -- that would wipe out the user's changes. If the user's modified the client-side Customer object, I just want to update the Customer object with its newly-retrieved collection of SalesOrder objects.

With Breeze, Knockout and TypeScript, it all turns out to be remarkably easy to do.

Setting Up
If you've read my earlier columns on using TypeScript with Knockout and Breeze, feel free to skip to the next section. But if you want an introduction to what I've done without all the detail, this section will give you the client-side story (the "The Server-Side Code" sidebar tells the server-side story).

My first client-side step is to write the interfaces that define to TypeScript the objects I'll be retrieving from my service. These interfaces (shown in Listing 1) extend the Breeze breeze.Entity interface so that I pick up the properties Breeze adds to my objects as it downloads them from the server. My ICustomer interface includes a property called SalesOrders that holds an array of ISalesOrder objects.

Listing 1. Client-Side TypeScript Interfaces for Server-Side Objects
module PHVEntities 
{
  export interface ICustomer extends breeze.Entity 
  {
    Id: number;
    FirstName: string;
    LastName: string;
    SalesOrders: ISalesOrder[];
  }

  export interface ISalesOrder extends breeze.Entity 
  {
    Id: number;
    CustomerID: number;
    OrderDate: string;
    Customer: ICustomer;
  }

I use TypeScript to define a ViewModel that contains the code that handles interactions between my page and my server. That ViewModel has variables that hold a Breeze EntityManager along with several Knockout observable types. One of those Knockout variables holds the collection of my retrieved Customer objects, another holds the Customer the user has selected and the third variable holds the SalesOrders for the currently selected Customer. Listing 2 shows the constructor for my ViewModel that defines those variables. As the list also shows, at the top of the module, I use the TypeScript import keyword to set up shortcuts to the two other TypeScript modules whose objects I'll be using in my code: My PHVEntities module with my interface definitions, and the breeze module, which holds all of the Breeze definitions.

Listing 2. A Knockout/TypeScript ViewModel Constructor
module PhvVM
{
  import ent = PHVEntities;  
  import b = breeze;

  export class SalesOrderVM
  {
    constructor(
      private em: b.EntityManager = 
        new b.EntityManager("…url for a Breeze-enabled Web API Service…"),
      public customers: KnockoutObservableArray<ent.ICustomer> = 
        ko.observableArray<ent.ICustomer>(),
      public customer: KnockoutObservable<ent.ICustomer> = 
        ko.observable<ent.ICustomer>(),
      public salesOrders: KnockoutObservableArray<ent.ISalesOrder> = 
        ko.observableArray<ent.ISalesOrder>())
      { }

My ViewModel also holds the function that does the initial retrieval of all my Customer objects, but without their SalesOrder objects:

fetchAllCustomers() 
{
  b.EntityQuery   
   .from("Customers")
   .using(this.em)
   .execute()
   .then(dt => {this.customers(<ent.ICustomer[]> dt.results);})
   .fail(err => { "Problem:" + alert(err.message); });
}

The following code instantiates my ViewModel, binds my Knock-enabled page to it and calls the function that retrieves my Customer objects as soon as the page loads:

$(function () {
  var vm: PhvVM.SalesOrderVM;
  vm = new PhvVM.SalesOrderVM();
  ko.applyBindings(vm);
  vm.fetchAllCustomers;
});

And, for completeness sake, Listing 3 shows the HTML that displays the collection in a table. Each row in the table has a button that calls a function named fetchOrdersForCustomer.

Listing 3: HTML with Knockout Bindings To Display the Customers Collection
<table data-bind='foreach: customers'>
  <tr>
    <td><input type="button" 
               data-bind='click: function(data) { $root.fetchOrdersForCustomer(data, $root); }' 
               value="Get Orders" /></td>
    <td><input type="text"   data-bind='value: Id' /></td>
    <td><input type="text"   data-bind='value: FirstName' /></td>
    <td><input type="text"   data-bind='value: LastName' /></td>
  </tr>
</table>

Retrieving SalesOrders
With that all in place, I'm ready to write the fetchOrdersForCustomer function that's called from my Get Orders button. The Knockout binding on the button causes Knockout to pass the Customer object Knockout has associated with the row in the table to the function. In the function I first update the "current Customer" property on my ViewModel with the selected Customer. I also check to see if the Customer object already has its SalesOrders. If the selected Customer does already have its SalesOrders, I just update my ViewModel SalesOrders collection with the selected Customers SalesOrders:

fetchOrdersForCustomer(cust: ent.ICustomer) 
{
  this.customer(cust);                      
        
  if (cust.SalesOrders.length != 0)
  {
    this.salesOrders(cust.SalesOrders);
  }
  else

If the selected Customer doesn't have its SalesOrders yet, I use a Breeze query to retrieve those SalesOrders from my server-side Customers service. Unlike the previous query, this query uses Breeze's where function to specify which Customer object I want: it requires the server-side objects Id property to be equal to the Id property on the Customer object passed to the function. The query also uses the Breeze expand function to retrieve the SalesOrder objects for the requested Customer object.

My server-side Entity Framework objects have a navigation property that connects Customers to their SalesOrders. Using the Breeze expand function generates (via OData) a LINQ query at the server that will include the SalesOrders referenced from that navigation property in the results returned to my ViewModel:

{
  b.EntityQuery
   .from("Customers")
   .where("Id", b.FilterQueryOp.Equals, cust.Id)
   .expand("SalesOrders")
   .using(this.em)
   .execute()
   .then(dt => {this.UpdateCustomerSalesOrders(<ent.ICustomer> dt.results[0]);}, cust)
   .fail(err => {alert("Problem:" + err.message);});
}

If that query succeeds, I call another function in my ViewModel, passing the first object from the results retrieved from the server plus the selected Customer object passed to my function; if the call fails, I display an error message.

In the function called when the retrieval succeeds, I check to see if the selected Customer object has been modified by the user since I first got the object from the server. If the object hasn't been modified, I update it with the latest data from the server. If the selected Customer has been modified, I just update its SalesOrders property with the SalesOrder objects retrieved from the server:

private UpdateCustomerSalesOrders(custRetrieved: ent.ICustomer,
                                  custLocal: ent.ICustomer)
{
  if (!custLocal.entityAspect.entityState.isAddedModifiedOrDeleted()) 
  {
    custLocal = custRetrieved;
  }
  else 
  {
    custLocal.SalesOrders = custRetrieved.SalesOrders;
  }

Either way, at the end of the function, I update my ViewModel salesOrders property with the Customer SalesOrders:

  this.salesOrders(custLocal.SalesOrders);
}

Here's the HTML with the Knockout bindings that displays the SalesOrder objects as soon as I update the salesOrders collection on my ViewModel:

<table data-bind='foreach: salesOrders'>
  <tr>
    <td><input type="text" data-bind='value: Id' /></td>
    <td><input type="text" data-bind='value: ShipDate' /></td>
    <td><input type="text" data-bind='value: OrderDate' /></td>
  </tr>
</table>

For completeness' sake, here's the HTML that defines the button, that calls the function on my ViewModel, that saves the changes made by the user back to the server:

<input type="button" data-bind='click: saveChanges' value='Save Changes' /> <br />

Here's the function in my ViewModel that the button calls:

saveChanges()
{
  this.em.saveChanges()
         .then(sr => {alert("Saved " + sr.entities.length + " Customers")})
         .fail(err => {alert(err.message)});      
}

Still Not Quite Perfect
The TypeScript environment still has the odd issue (as I write this, TypeScript is still in beta). When I upgraded to TypeScript 0.9.5, I started getting errors from the Breeze definition file downloaded from DefinitelyTyped. The problem was the definitions for two classes in the file looked identical to the latest version of TypeScript:

class FetchStrategySymbol extends breeze.core.EnumSymbol {}
class MergeStrategySymbol extends breeze.core.EnumSymbol {}

I solved the issue by adding a dummy entry to one of the classes, to give me this:

class FetchStrategySymbol extends breeze.core.EnumSymbol {DummyStrategy}

Still, the more I work with TypeScript, the happier I am. In fact, it's becoming increasingly difficult to wait for version 1.0. I hope by the time you read this the wait will be over.

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