The Practical Client

Implementing CRUD Activities in a TypeScript ViewModel

Peter adds client-side update, delete and insert support to an ASP.NET application using TypeScript. Along the way he shows how TypeScript interface support allows you to integrate your objects with JavaScript libraries in TypeScript 0.9.5.

In this column I'm going to show how to integrate create, read, update and delete (CRUD) functionality into your client-side application using TypeScript. But first, I need to address how to integrate your entity objects with the requirements that might be imposed by a JavaScript library.

Integrating Entities by Extending Interfaces
It isn't unusual for a JavaScript library to add new functionality to objects or classes you create. For example, I use the IdeaBlade's free Breeze library to manage the client-side objects retrieved from methods on my server-side services. To support managing client-side objects, Breeze adds an entityAspect property to my objects as it retrieves objects from the server. That entityAspect property contains state information about the object. To notify Breeze that an object's to be deleted, I call the setDeleted method on the object's entityAspect property.

In TypeScript, when working with server-side objects after they're downloaded to the browser as JSON objects, I don't have access to those objects' C# or Visual Basic definitions. To provide a definition of those objects that my client-side code can use, I create a TypeScript interface that duplicates my Visual Basic or C# definition. With Breeze, however, I need that TypeScript interface to also include the methods and properties that Breeze adds to my objects as part of downloading them. Unlike the server-side objects I created, however, I don't know what the Breeze properties and methods look like. Fortunately, TypeScript provides extended interfaces to handle this issue.

To have my client-side interface include the methods and properties that Breeze adds to my client-side objects, I define the interfaces for my objects as extensions to interfaces exposed by Breeze. The Breeze interfaces I need, for example, are defined in a class called breeze.Entity. This means the interface that describes my Customer object, with its three properties and including the Breeze extensions, looks like this:

module PHVEntities 
{
  export interface ICustomer extends breeze.Entity 
  {
    Id: number;
    FirstName: string;
    LastName: string;
  }

Now, my Customer interface includes the Breeze entityAspect property defined in breeze.Entity. I can mark a Customer as deleted with code like this:

cust: PHVEntities.ICustomer;
...download customer object...
cust.entityAspect.setDeleted();

Implementing CRUD
With that interface in place, I can use Breeze to retrieve Customer objects from my server-side services. Listing 1 shows the start of my ViewModel (written with Knockout) with the code that does the retrieval and puts the objects in an array called customers defined in the ViewModel constructor.

Listing 1: A Knockout ViewModel with Breeze
import b = breeze;
import ent = PHVEntities;  

export class SalesOrderVM
{
  constructor(
    private em: b.EntityManager = new
      b.EntityManager("...url for services..."),
    public customers: KnockoutObservableArray<ent.ICustomer> = 
      ko.observableArray<ent.ICustomer>()) { }

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

I have a button on the page that runs this method through a Knockout binding:

<input type="button" value="Get Customers" data-bind="event: { click: fetchAllCustomers }" />

After retrieving a collection of Customer objects, I display those Customers in a table. The HTML that binds to my customers collection generates a table that includes a button that binds to another method in my ViewModel:

<table>
  <tbody data-bind='foreach: customers'>
    <tr>
      <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>
      <td><input type="button" data-bind='click: $root.removeCustomer' 
                 value="Delete" /></td>
    </tr>
  </tbody>
</table>

Each row in the table represents a single customer object. If I were to just bind my button to "removeMethod" then Knockout would look for the method on the customer object. Using $root tells Knockout that I want it to look for the method on the ultimate parent: my Knockout ViewModel.

Deletes, Inserts and Updates
In the removeCustomer method tied to the button in each row, I must do two things. First, I need to mark the Customer in the row as deleted to cause Breeze to trigger deletes back at the server. Second, I need to remove the Customer from my client-side customers collection so that it will no longer appear in the table.

Knockout will automatically pass the Customer in the row to a method called from that row, so the removeCustomer method looks like this:

removeCustomer(cust: ent.ICustomer)
{
  cust.entityAspect.setDeleted();
  this.customers.remove(cust);
}

Following my table, I have another button that allows the user to add a new row. That button also binds to a method on my ViewModel but, because the button is outside the context of the repeating rows in the table, I don't need to qualify the method name:

<input type="button" data-bind='click: insertCustomer' value='Add Customer' />

My insertCustomer method also does two things. First, it uses Breeze to create a new customer entity and places it in a variable I've called newCust. The method then adds that newCust object to my customers collection:

insertCustomer()
{
  var newCust: ent.ICustomer;
  newCust = <ent.ICustomer> this.em.createEntity("Customer");
  this.customers.push(newCust);
}

Adding newCust to my Knockout-enabled collection causes Knockout to refresh the table and display the new blank row for the user to enter data.

I don't need to do anything to support updates: The user can update the textboxes in the table that displays the customer's properties. Thanks to Knockout, those changes are saved back to my collection of Breeze-enabled customers. All that's necessary to have those changes (including inserts and deletes) sent back to my service is to call the Breeze SaveChanges method. I put that code in a method in my ViewModel called saveChanges:

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

Therefore, it's just a matter of binding this method to a button on my page to let the user save all of their deletes, inserts and updates:

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

All that's missing on the client is the code to run at startup to cause Knockout to bind my HTML to my ViewModel with its methods and collections. That code looks like this:

$(function () {
  ko.applyBindings(new PhvVM.SalesOrderVM());
});

That brings this column up-to-date with the current version of my ongoing TypeScript project. In my next column, I'm going to look at retrieving the child records associated with each Customer.

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