Intercepting Boundary Events in Coach View

Boundary Event is core concept of UI technology in IBM BPM: any visual element (coach view) of screen (coach) can fire such event moving control from current screen to some other place, crossing “screen boundary“. In fact Boundary Event is the only way to make such transition on diagram of coaches. Problem is that IBM BPM event model is trivial and does not allow virtually any interference with event in progress, one cannot change event bubbling or handle it in try-catch manner. Find how I bypassed this limitation and implemented events interceptor.

Common processing

Common processing

Separate processing

Separate processing

Imagine screen that has multiple buttons. They are typical source of boundary events and if wired on diagram they may lead to appropriate processing – see diagram aside for 3 buttons with 3 distinct scripts to run on each button click.
What if some buttons are serve similar purpose, like different variants of action to take. In such condition their boundary event could lead to the same script. Now the problem is to know which button was clicked. In stock coaches one could bind each button to boolean variable and check them one by one: shortcoming is human service is polluted with unnecessary variables (which is a problem of IBM BPM coaches anyway). With SPARK UI toolkit one could add easily on-click event handler storing, let say, button view ID in shared variable.

I imagined wrapper view that intercepts button boundary events and triggers own one instead. Wrapper would expose binding variable with source of event (view ID) to make solution complete. I created sample scenario (see below) that has coach view container hosting other controls and intercepting their boundary events.

Test scenario - diagram

Looking closer at coach framework JavaScript API, it is easy to spot that there are only two functions dealing with boundary events: context.trigger(callback) and context.cancelBoundaryEvents(). Note however the latter works only from inside callback of trigger function. Nevertheless both functions work at the level of event source (widget triggering event) there is no much room for play with event in progress.

Browser JavaScript however gives a way for some trick to bypass source limitations: in my case I realized that I could… replace context.trigger() on all views I want to take control over! First step was to make children traversal. It was structured into recursive walk and delegation of remaining work to visitor function (all code in Inline JavaScript section).

this._visitChildren = function(view) {
  var rootView = this;
  if (view && view.context && view.context.subview) {
    if (rootView !== view) {
      rootView._visitor(view);
    }
    var viewIds = Object.keys(view.context.subview);
    viewIds.forEach( function(id) {
      var subviews = view.context.getSubview(id);
      subviews.forEach( function(sv) {
        rootView._visitChildren(sv);
      });
    });
  }
};

Visitor takes each subview and replaces trigger function with proxy. Proxy saves event source (view ID) in variable bound on root interceptor level and then calls interceptor’s trigger():

this._visitor = function(subview) {
  console.log("Proxying " + subview.context.viewid);
  var rootView = this;
  var trigger = subview.context.trigger;
  subview.context.trigger = function(callback, options) {
    console.log("trigger() proxy called for " + subview.context.viewid);
    //trigger.call(subview, callback, options); //original trigger call
    rootView.context.binding.set("value", subview.context.viewid);
    rootView.context.trigger(callback, options);
  };
}

That was pretty easy until I realised that it will not work for views that mutate in runtime. This is what happens when editable table is used e.g. adding row means creating views after initial load. Or when panel is lazily loaded like in custom control that initialise panels after they become visible e.g. on tabbed section.

Fortunately proxying is quite powerful idea. Because creating views in runtime is done by calling to context.createView() run on existing view, it means that any extra view inside tree of children of interceptor must be called from already-proxied view. That means extra responsibility of createView() was to re-visiting nodes after creation:

this._visitor = function(subview) {
  ...
  var createView = subview.context.createView;
  subview.context.createView = function(domNode, index, parentView) {
    console.log("createView() proxy called for " + subview.context.viewid);
    createView.call(subview.context, domNode, index, parentView);
    // createView on subview could recursively create multiple views
    rootView._visitChildren(subview);
  };
}

Since views can be created on any level of children it means same sub-trees can be potentially visited again and again. Above code without checks simply leads to layering excessive proxies (proxy on proxy etc). Adding flag on each subview and guard condition on visitor is simple and effective counter-measure. Tuned visitor function looks like this:

this._visitor = function(subview) {
  if (subview._bec_proxified) {
    return;
  }
  subview._bec_proxified = true;
  ...
}

To prove everything works as expected I created screen with few buttons on different levels of nesting. To simulate dynamic views creation I made up coach view that manages content-box itself and loads child view after some time delay. Boundary Event Collector and Delayed load coach views has visual guides: colourful border frames, green and red respectively.

Console in browser’s inspector (screen below) shows how children are visited during on-load phase. B1 and B2 as top level, then section with B3. After 5 seconds delay, Delayed load coach view is loaded and note it happens from createView() proxy, so that when original context.createView() finishes loading CV and button B4, visiting continues again. Finally after clicking on B4 proxified trigger is called and second screen displays source of registered boundary event.

Dynamic content

Delayed load coach view created for testing interceptor touches one of interesting advanced subjects in coach technology: dynamic manipulation on child views lifecycle i.e. creating them or deleting in runtime. This feature is rarely used in real life scenarios on application level development, yet it is good to know fundamentals.

By default content-box is managed by framework: views placed in content-box are initialised automatically before parent’s load event handler is called; it means that during on load phase children are full blown coachviews already.
be-cboxWhen content-box is configured to self-manage its content, it means that parent (our) coach view must do the job using context.createView() on items placed during design phase (and for runtime views as well). Because child views can be nested forming a tree structure, it is our CV responsibility to walk over it. In my case delayed creation looked like this:

this._loadChildren = function(view) {
  var cb = view.context.element.querySelector(".ContentBox");
  for(var i = 0; i < cb.children.length; i++) {
    var child = cb.children[i];
    if(domClass.contains(child, "ContentBox")) {
      this._loadChildren(child);       
    } else if(domAttr.has(child, "data-viewid")) {
      try {
        this.context.createView(child, null, this);
      } catch(e) {
        console.error(e);
      }
    }
  } 
}

where domClass and domAttr are AMD dependencies on dojo/dom-class and dojo/dom-attr modules respectively.

For any further learning, look into source code of existing coach-based toolkits.

Create interceptor coach view

If you want to play yourself you can create the “Boundary Event Collector CV” yourself as shown below. You can also download whole project with test suite in TWX form from section below.

  1. Create “Boundary Event Collector CV” coach view.
  2. In “Overview” section of this CV mark it “Can fire boundary event”.
  3. In “Behavior” section open “Inline JavaScript” section and paste
    content of this JavaScript file.
  4. In “load” event handler of this CV call on-load function like this:
     this.onLoad();
  5. In “Variables” section create String variable named controlID.
  6. In “Layout” section place “Content-box” widget, no special setting is required.
  7. Done!

Download project

Boundary_Event_Collector.twx (6.5MB) export file for BPM 8.5.7 version.

This entry was posted in Software and tagged . Bookmark the permalink.

Leave a Reply

Your email address will not be published.