The Practical Client

Simple Concurrent Client-Side Processing in ASP.NET MVC with Web Workers and TypeScript

Here's how to use web workers in ASP.NET MVC with TypeScript to enable concurrent processing in your client-side code. And, as a bonus, the correct way to think about web workers.

Recent versions of TypeScript provide support for the async and await keywords when working with methods that return a Promise object. However, because the JavaScript runtime is a single-threaded environment, the async/await keywords in TypeScript don't give you the ability to have multiple processes executing simultaneously (as opposed to C# and Visual Basic where they do).

If you want to have multiple processes running simultaneously, then you have to use web workers. Unfortunately, the way that web workers are normally described misses the point of this technology so you need to rethink that, also.

Starting a Web Worker
A web worker is a separate set of TypeScript from the code that calls it. Typically, this means that you'll put the code in a separate file. To mimic a long-running operation for testing purposes, you could put this code in a file called LongRunning.ts:

end = new Date().getTime() + 5000;
while (new Date().getTime() < end) { }

In TypeScript, running the code is relatively straightforward: You instantiate a Worker object, passing the path to your script file. If your web worker code is in a separate file, then formatting that path can be tricky. For example, if the file containing your calling code is in the same folder as your web worker file, you might be tempted to just pass the name of your web worker file to the Worker object as a "relative path." That won't work in ASP.NET MVC. You need, instead, a "relatively relative" path.

To process your path, the file name will be pasted onto the root of the URL used to retrieve the page. The issue is that the URL used to execute the page holding your calling code typically doesn't reference the site's root folder (a typical URL generated by ASP.NET MVC is something like "/Customer/Update"). To fix this, you must pass a "relatively relative" pathname that works its way up to the site's root folder through liberal use of the .. shortcut and then works its way back down to your web worker's file. You must also reference the JavaScript version of your TypeScript code.

Because both the file with my calling code and my LongRunning.ts file are in my application's Scripts folder, the TypeScript code to start my LongRunning web worker looks like this:

let w: Worker;
w = new Worker("../Scripts/LongRunning.js");

As soon as your Worker object is instantiated, the code in your worker that's not in a function or class will start to execute. If you want to organize your worker code into classes, then you'll need to leave some code outside of those classes to call your class's method. Code like this is typical of that approach:

class Customer {
   public processCustomers(): void {  ...method code...}
}

let c: Customer;
c = new Customer();
c.processCustomers();

This works fine as long as you have no parameters to pass to your concurrent code.

There are multiple limitations on what you can do in a worker, but the primary one is that you can't access the DOM because the worker is running on a different thread than your UI. This also means that you can't use jQuery in your worker code because it's heavily DOM-dependent. So, for example, if you want to call a Web Service from your worker code, then you're going to have to use the XMLHttpRequest object rather than jQuery's ajax method (or its wrappers).

Passing Parameters, Triggering Processing
However, if you want to pass parameters to your concurrent code, you should think about your web worker differently from what I've suggested. While you can't pass parameters when starting a web worker, you can send your web worker messages by calling the Worker object's postMessage method. Typically, any message you pass will be copied to the web worker's environment, which, if you're passing a large complex object, could hurt performance.

The one exception is for those objects that implement the Transferable interface (as does the ArrayBuffer object, for example). With Transferable objects, the object is passed to the web worker at virtually no cost at all. The only wrinkle here is that the calling code no longer has access to the object. You're most likely to notice this when you accidentally pick a Transferable object and discover (after posting it to the web worker) that you get errors when you try to use the object in your calling code.

Regardless of what you're passing to the worker, the code to send it to a web worker looks like this:

w.postMessage(custData);

To receive the data in a web worker, you need to catch the message event raised in the worker. You can do that either by tying a function to the event using AddEventListener or by storing a function that will process the message in onmessage. The function you use will be passed a MessageEvent object with the object from the calling code in its data parameter.

Either of these sets of code in the web worker file will work:

this.onmessage = function (e: MessageEvent): void {...process e.data...}
this.addEventListener("message", (e: MessageEvent) => ...process e.data...); 

The Right Way to Use Web Workers
Because JavaScript is a single-threaded environment, posting messages to the worker won't interrupt the worker's processing. It's much like having some long-running JavaScript code in your page: The user can whang away at a button on the page all they want, but, until that long running code finishes, the onclick events the button is raising won't be processed.

For example, in my initial dummy code for my web worker at the start of this column I have code that executes a loop for five seconds, tying up the thread that the worker is executing on. While my calling code can call postMessage to pass a message to the worker at any point within that five seconds, the worker is unlikely to recognize the event until after the loop finishes executing. To put it another way: I can't use postMessage to affect the processing in the loop. As a result, you'll typically use postMessage to trigger some onmessage code, which will then do all the work you want.

This is where I think most discussions of web workers miss the point: Typically, you'll want to pass a parameter to your web worker code. That being the case, you should put all the code you want to run concurrently in your onmessage method rather than directly into the web worker file. You cause that code to start executing (and pass it a parameter) by calling your Worker object's postMessage method. Think of instantiating your Worker object as creating a parallel thread waiting for you to offload processing to it. When you want to use that thread, just call your Worker object's postMessage method.

Returning Results, Terminating Processing
Just as you'll typically want to pass data to a web worker, you'll usually want the web worker to return data to your called code. To return data from a worker, you use the same mechanism as you did to pass data to the worker: postMessage. The wrinkle here is that the signature for the postMessage used within the worker is different from the signature used from the Worker class. Unfortunately, the TypeScript compiler always insists on the signature for the version used from from the Worker class. As a result, code within a worker that uses postMessage raises syntax errors at compile time and won't compile.

There are a number of solutions to this problem, but the one that I've found easiest to implement is to just turn off syntax checking on the worker's postMessage method by casting the method to the any type. This means giving up IntelliSense support and some compile-time checking because TypeScript won't check anything typed as any. However, since the postMessage used within the worker accepts only a single parameter (the data to be returned), you're not losing a lot.

A typical onmessage implementation that accepts some data, processes it and then posts the result to the calling code, therefore, looks like this:

this.onmessage = function (e: MessageEvent):void {
   ...process e.data...   
   (<any>postMessage)(...return results...);
}

To catch the result returned from the worker, you use the same mechanisms used to receive data inside the worker: AddEventListener or onmessage. Code to start a worker and set up to receive its message, therefore, looks like this:

w = new Worker("../Scripts/LongRunning.js"); 
w.onmessage = (e: MessageEvent):void => { ...process e.data sent from the worker... }; 

Finally, your calling program can stop the worker from processing by calling the worker's terminate method:

w.terminate();

Unfortunately, you'll find that debugging a web worker can be ... challenging (I couldn't get the client-side debugger to work with a web worker in either Chrome or Microsoft Edge, for example). I've got a solution to the debugging problem that I'll discuss in my next column because, after all, you never get it right the first time.

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