Shared Data across Coach Views

This article describes a design pattern for having Coach Views share commonly used data in a sensible way in the browser. This can drastically decrease redundant Ajax calls and thus have a very positive impact on the Coach’s performance.

Use Case: Dynamically loading localization

To illustrate the technique we will build a simple example where we can save a great number of unnecessary Ajax calls. Imagine the following use case: You have a list of objects (think Task List) where each item has its own status. This status is stored as a key. In the browser the user must be presented with a properly spelled out status name instead of the technical key. This is handled by a specific Coach View which retrieves the status names via an Ajax service.

Building the basic assets

To get the example running we create a few assets as shown below.

Demo – Share Data (Heritage Human Service)

This Human Service does nothing more than display our list of statuses. The data model is a simple list of Strings stored as a local variable taskList.

In the diagram a Server Script Init initializes the taskList with a couple of Strings, which will be the keys that need to be converted to proper status names.

The Coach UI contains a Table stock control that is bound to the taskList. It has the Buttons for Adding and Deleting entries enabled. Inside the table we use a stock Text control and our custom Status Coach View to display the values of the current entries of the list.

Common (Localization Resource)

The Localization Resource Common stores the status names in two languages – Default (English) and German. This is the basis for the status names and it is what should be used by the Coach View for displaying the statuses.

loadLocalization (Ajax Service)

This Ajax Service returns a Map with the status keys as, well, the key and the respective localized values as the – you guessed it – value.

Note how we can take advantage of the way the localization keys are structured in Common. Since the keys are identical we can e.g. get the localized value for the status approved by writing the following:

tw.resource.Common.status['approved']

For this example we chose to let the service return a subset of all possible statuses. It only returns the active states inprogress, open and sentback – the remaining statuses which represent terminated states (approved, completed and rejected) are not included.

Status (Coach View) – Initial Solution

Finally, this is the center-piece of this example. The Coach View shall take the status string and display it as a properly localized text. For this it has a binding to a string, which is the status key. To retrieve the localization values the loadLocalization Ajax service is set up as a Configuration Option.

The layout consists of a simple Custom HTML element which provides the markup for the status name to be inserted.

To get this going we make this first implementation, which is not very well-thought-out but all the more intuitive. We will point out the problems of this implementation and approach better solutions from there.

In the load event the Coach View fires the bound Ajax Service (line 10) which retrieves the localization map. If this is successful, the returned data is assigned to the statusMap variable (14) and the setStatus function is called (15). This will then get the localized status from the map and insert it into the markup (7), using “-” as a fallback in case the status key could not be found in the map.

Identifying Superfluous Ajax Calls

The problem with the above implementation becomes apparent if we open developer tools in the browser (e.g. Firebug on Firefox or the built-in tools) and take a look at the network traffic:

The taskList variable in the Human Service is initialized with seven items, resulting in seven requests sent to the server as the table control builds its rows each one with its own Status Coach View. What’s more, if we click the + button to add a new entry, another eight requests are made as the load event for the Status controls is fired, once for each row. Similarly, if we click one of the X buttons to remove an item another seven requests are made. By now we have sent a total of 22 requests to the server, which probably should be no more than a single one. Clicking on one of the Text inputs and changing a status (e.g. from “inprogress” to “open”) results in another request as the Coach View is loaded anew.
It is easy to imagine how this gets out of hand when dealing with large data sets. Also, there may be other controls that cause reloads – perhaps you want your table to automatically update every few minutes to always display the most current data? All these requests quickly become a huge strain on the Coach. So let’s see what we can do about it.

Shared Data after Ajax Callback

Unfortunately, the various instances of the Status control don’t know about each other. They don’t know whether the data they need has already been loaded before and even if they did, how would they access this data?

While the Coach Views can’t directly access each other they all share the same global context. This means they share the same window object or any other property of window. However, one must always be extremely careful when pushing anything to the global namespace. Deciding to do so should never be taken lightly. If you are not careful, then your global objects are removed or overwritten by the next best JavaScript library or tiny code snippet, resulting in unexpected and erroneous behavior.

In this example we will use the global com_ibm_bpm_coach object and its property currentprojectsnapshot as a hook for our own data. While this may make us feel less bad about polluting the global namespace, we are faced with the same question again: How can be store objects here and be sure they don’t get overwritten? The answer is simple: We cannot. So to mitigate the risk we must choose a unique identifier for our own namespace.

There are numerous sensible ways for defining a name that is most likely unique. You can use the acronym of your process app or toolkit or come up with creative names. If you want to use the technique described in this post as part of a assets which belongs to a certain project or toolkit you might want to create global object by that name. Or maybe you already use a global namespace derived from your company name or your client’s company name.

The code for creating a globally available object for all Coach Views might look something like this:

// Create global namespace for this coach view's shared content:
var snapshot = com_ibm_bpm_coach.currentprojectsnapshot;
var cvspace = "statusCoachView"; // namespace for this control
var sharedObject = "statusMap"; // name for the shared object
com_ibm_bpm_global = com_ibm_bpm_global||{};
com_ibm_bpm_global[snapshot] = com_ibm_bpm_global[snapshot] || {};
com_ibm_bpm_global[snapshot].shared = com_ibm_bpm_global[snapshot].shared || {};
com_ibm_bpm_global[snapshot].shared[cvspace] = com_ibm_bpm_global[snapshot].shared[cvspace]||{};
com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject] = com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject]|| false;

Note how no object is ever overwritten. If com_ibm_bpm_global already exists, it is assigned itself. Only otherwise it is created as an empty object. The same pattern is used for the subsequent properties, except for the shared object property. This is because the empty object {} is truthy in JavaScript. So if we want to check whether it has been filled already, it is the easiest to have it initialized with a falsy value.

With this in mind the full implementation of the Status control’s load event now looks like this:

var _this = this;

function setStatus(){
    var sStatus = _this.context.binding?_this.context.binding.get("value"):'';
    var oSpan = _this.context.element.querySelector('.statusText');
    oSpan.innerHTML = com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject][sStatus]||'-';
}

// Create global namespace for this coach view's shared content:
var snapshot = com_ibm_bpm_coach.currentprojectsnapshot;
var cvspace = "statusCoachView"; // namespace for this control
var sharedObject = "statusMap"; // name for the shared object
com_ibm_bpm_global = com_ibm_bpm_global||{};
com_ibm_bpm_global[snapshot] = com_ibm_bpm_global[snapshot] || {};
com_ibm_bpm_global[snapshot].shared = com_ibm_bpm_global[snapshot].shared || {};
com_ibm_bpm_global[snapshot].shared[cvspace] = com_ibm_bpm_global[snapshot].shared[cvspace]||{};
com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject] = com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject]||false;

if(com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject]){
    setStatus();
}
else {
    this.context.options.getLocalizations({
        params: {},
        load: function(data){
            if(data && data.statusMap){
                com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject] = 
                    data.statusMap;
                setStatus();
            }
            else console.warn("Coach View Status: No values retrieved.");
        }
    });
}

Now, each Status control instance will first check whether the [sharedObject] has been set to a truthy value. If this is the case, the status is set using the existing map. Only otherwise the Ajax call is made. In this the global object is filled with the retrieved data for later reuse.

If we now take another look at the browser, there is no difference during the initial load: There are still seven requests being made. However, if we now add a new item or delete an existing one, no further requests are made.

What is going on?

Shared Data from the Beginning

In the implementation presented previously the data returned from the Ajax Services is made available globally for all to use and reuse. However, these requests take time. When the page is built and all the Coach Views are created, their load events are fired quickly after each other. In fact, this happens so quickly that by the time the second, third and all other subsequent events are fired the response from the first Ajax call has not come back yet to store its data. Therefore, all the remaining Coach Views think they should retrieve the data themselves, resulting in the same number of request during page load.

Only by the time the user itself takes further actions, such as adding or deleting items, the Ajax calls have returned and stored the data successfully so that it can be reused then.

To fix this situation we must improve the communication between the Coach Views on what is going on regarding the loading process. We must ensure that only the first instance of the Coach View makes the Ajax call and no other instances do so. In addition, when this first call comes back, it must do what the other instances wanted to do, namely adjusting their individual status names.

Firstly, we introduce another level to the hierarchy. The shared object now is no longer simply the data which should be retrieved. Instead it is an object with three properties:

  • loading: If this is true, there is currently an Ajax service on the way, busy loading the data. Initially this is false. Once the Ajax returns it is set back to false again.
  • ready: If this is true, the data has been successfully loaded. Initially this is false.
  • content: This is the actual data to be stored and shared.
  • responsehandlers: This is an array of functions which are all executed in the Ajax service’s callback.

With these properties the Status instances can communicate: Once the first instance started the loading process, the others do not need to trigger an Ajax call themselves.

Next we must ensure that the response handlers do what the second and other Status control instances did not get to do the first time: set their statuses. We achieve this by having each instance add a function to the responsehandler object. The functions serve as response handlers when the Ajax Service comes back from the server. Each instance adds its own function to the array. Once the callback is made, the shared data is stored and all these functions are executed.

With all the above incorporated into the Coach View’s load event, the code looks like this:

var _this = this;

function setStatus(){
    var sStatus = _this.context.binding?_this.context.binding.get("value"):'';
    var oSpan = _this.context.element.querySelector('.statusText');
    oSpan.innerHTML = com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject].content[sStatus]||'-';
}

// Create global namespace for this coach view's shared content:
var snapshot = com_ibm_bpm_coach.currentprojectsnapshot;
var cvspace = "statusCoachView"; // namespace for this control
var sharedObject = "statusMap"; // name for the shared object
com_ibm_bpm_global = com_ibm_bpm_global||{};
com_ibm_bpm_global[snapshot] = com_ibm_bpm_global[snapshot] || {};
com_ibm_bpm_global[snapshot].shared = com_ibm_bpm_global[snapshot].shared || {};
com_ibm_bpm_global[snapshot].shared[cvspace] = com_ibm_bpm_global[snapshot].shared[cvspace]||{};
com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject] = com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject]||{ ready: false, loading: false};


if(com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject].ready){
    setStatus();
}
else {
    if(com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject].loading){
        com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject].onloadhandlers = com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject].onloadhandlers || [];
        com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject].onloadhandlers[com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject].onloadhandlers.length] = function(){
            setStatus();
        };
    }
    else {
        this.context.options.getLocalizations({
            params: {},
            load: function(data){
                if(data && data.statusMap){
                    com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject].content = data.statusMap;
                    com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject].ready = true;
                    setStatus();
                    
                    var handlers = com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject].onloadhandlers;
                    for(var i=0; i<handlers.length; i++){
                         if(typeof handlers[i] == "function")handlers[i]();
                    }
                }
                else console.warn("Coach View Status: No values retrieved.");
                com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject].loading = false;
            }
        });
        com_ibm_bpm_global[snapshot].shared[cvspace][sharedObject].loading = true;
    }
}

Now the Ajax Service is truly only ever called once – no matter how many instances of the Coach View are created and no matter how many times they get reloaded.

One thought on “Shared Data across Coach Views

  1. Pingback: Cached Data Library | C:Temp

Comments are closed.