Skip to content

App to App Communication

1000TurquoisePogs edited this page Aug 3, 2020 · 9 revisions

zLUX Apps can opt-in to various App Framework abilities, such as to have a Logger, to make use of a URI builder utility, and more. One such ability that is unique to a zLUX environment with multiple Apps is the ability for one App to communicate with another. The framework provides a few constructs which facilitate this ability, all of which are explained here. They are the Dispatcher, Actions, Recognizers, Registry, and features that utilize them such as the framework's Context menu.

  1. Reason for App-to-App Communication
  2. Actions
  3. Recognizers
  4. Dispatcher

Reason for App-to-App Communication

When working with a computer, users tend to use multiple applications to accomplish some task, for example: checking a dashboard before digging into a detailed program or checking email before opening a bank statement in a browser. In many environments, the relationship between one program and another is loose and not well defined (you might open one program to learn of a situation, which you solve by opening another and typing or pasting in content). Or perhaps a hyperlink is provided or an attachment, which opens up a program by using a lookup table of which the program is the default for handling a certain file extension. The App framework attempts to solve this problem by creating a notion of structured messages that can be sent from one App to another. An App has a context of what information is currently contained within it, which could be used to invoke an action on another App which might be better suited to deal with some information discovered in the first App. Well-structured messages facilitate knowing what App is "right" to deal with a situation, and explains in detail what that App should do. This way, rather than finding out that the attachment with the extension ".dat" was not meant for a text editor, but instead for an email client, one App may instead be able to invoke an action on an App which can handle opening of an email for the purpose of forwarding to others - a more specific task than can be explained with filename extensions.

Actions

In order to manage communication from one App to another, a specific structure was needed. In the App framework, the unit of App-to-App communication is an Action. The typescript definition of an Action is as follows:

export class Action implements ZLUX.Action {
    id: string;           // id of action itself.
    i18nNameKey: string;  // future proofing for I18N
    defaultName: string;  // default name for display purposes, w/o I18N
    description: string;
    targetMode: ActionTargetMode;
    type: ActionType;   // "launch", "message"
    targetPluginID: string;
    primaryArgument: any;

    constructor(id: string, 
                defaultName: string,
                targetMode: ActionTargetMode, 
                type: ActionType,
                targetPluginID: string,
                primaryArgument:any) {
       this.id = id;
       this.defaultName = defaultName;
       // proper name for ID/type
       this.targetPluginID = targetPluginID; 
       this.targetMode = targetMode;
       this.type = type;
       this.primaryArgument = primaryArgument;
    }

    getDefaultName():string {
      return this.defaultName;
    }
}

What we see here is that an Action always has a specific structure of data that is passed, to be filled in with the context at runtime, and a specific target that should receive the data. In addition, the Action is dispatched to the target in one of various modes: such as to target a specific existing instance of an App, any instance, or to create a new one. The Action may also be something less detailed than a message: It could be a request to minimize, maximize, close, launch, and more. Finally, all of this information is related to a unique ID and localization string such that it can be managed by the framework.

Action Target Modes

When you request an Action on an App, the behavior is dependent upon which instance of an App you are targeting. You can tell the framework how to target the App with a target mode from the ActionTargetMode enum:

export enum ActionTargetMode {
  PluginCreate,                // require pluginType
  PluginFindUniqueOrCreate,    // required AppInstance/ID
  PluginFindAnyOrCreate,       // plugin type
  //TODO PluginFindAnyOrFail
  System,                      // something that is always present
}

Action Types

The App framework will perform different operations on Apps depending on what the type of an Action was. The behavior can be quite different, from simple messaging to requesting that an App be minimized. The types are defined by an enum:

export enum ActionType {       // not all actions are meaningful for all target modes
  Launch,                      // essentially do nothing after target mode
  Focus,                       // bring to fore, but nothing else
  Route,                       // sub-navigate or "route" in target
  Message,                     // "onMessage" style event to plugin
  Method,                      // Method call on instance, more strongly typed
  Minimize,
  Maximize,
  Close,                       // may need to call a "close handler"
} 

Loading Actions

Actions can either be created dynamically at runtime, or saved and loaded by the system at login.

Dynamically

Actions can be created by calling this Dispatcher method: makeAction(id: string, defaultName: string, targetMode: ActionTargetMode, type: ActionType, targetPluginID: string, primaryArgument: any):Action

Saved on system

Actions can be stored in JSON files to be loaded at login.
These must be stored as a single file within the plugin package's config/action folder, and the filename must be identical to the plugin's identifier.
The JSON structure is as follows:

{
  "actions": [
    {
      "id":"org.zowe.explorer.openmember",
      "defaultName":"Edit PDS in MVS Explorer",
      "type":"Launch",
      "targetMode":"PluginCreate",
      "targetId":"org.zowe.explorer",
      "arg": {
        "type": "edit_pds",
        "pds": {
          "op": "deref",
          "source": "event",
          "path": [
            "full_path"
          ]
        }
      }    
    }
  ]
}

Recognizers

Actions are meant to be invoked when certain conditions are met. For example, there's no need to open a messaging window if you have nobody you need to message. Recognizers are objects within the App framework that utilize the context that Apps provide to determine if there is a condition that would make sense to execute an Action for. Each recognizer has statements about what condition they wish to recognize, and upon that statement being met, which Action could be executed at that time. The invocation of the Action is not handled by the Recognizer; it simply detects that an Action could be taken.

Recognition Clauses

Recognizers associate a clause of recognition with an action, as you can see from the class:

export class RecognitionRule {
  predicate:RecognitionClause;
  actionID:string;

  constructor(predicate:RecognitionClause, actionID:string){
    this.predicate = predicate;
    this.actionID = actionID;
  }
}

A clause in turn is associated with an operation, and subclauses that the operation acts upon. The list of supported operations may grow over time, but at the moment is the following:

export enum RecognitionOp {
  AND,
  OR,
  NOT,
  PROPERTY_EQ,        
  SOURCE_PLUGIN_TYPE,      // syntactic sugar
  MIME_TYPE,        // ditto
}

Loading Recognizers at runtime

You can add a Recognizer to the App environment in one of two ways: loading from Recognizers saved on the system, or adding them dynamically.

Dynamically

You can call the Dispatcher method, addRecognizer(predicate:RecognitionClause, actionID:string):void

Saved on system

Recognizers can be stored in JSON files to be loaded at login.
You can have multiple recognizer files, one for each plugin they are intended for.
These must be stored within the plugin package's config/recognizers folder, and each file within must be identical some plugin's identifier.
The JSON structure is as follows:

{
  "recognizers": [
    {
      "id":"<actionID>",
      "clause": {
        <clause>
      }
    }
  ]
}

clause can take on one of two shapes:

"prop": ["<keyString>", <"valueString">]

Or,

"op": "<op enum as string>",
"args": [
  {<clause>} 
]

Where this one can again, have subclauses.

Recognizer Example

Recognizers can be as simple or complex as you write them to be, but here is an example to illustrate the mechanism

{
  "recognizers":[
    {
      "id":"org.zowe.explorer.openmember",
      "clause": {
        "op": "AND",
        "args": [
         {"prop":["sourcePluginID","org.zowe.terminal.tn3270"]},{"prop":["screenID","ISRUDSM"]} 
        ]
      }
    }
  ]
}

In this case, we have a Recognizer which detects if it is possible to execute the org.zowe.explorer.openmember Action when the TN3270 Terminal App is on the screen ISRUDSM, which is an ISPF panel for browsing PDS members.

Dispatcher

The dispatcher is a core component of the App framework, and is accessible via the [Global ZLUX Object] at runtime. This Dispatcher interprets Recognizers and Actions that are added to it at runtime. You can register Actions and Recognizers on it, and later, invoke an Action through it. It handles how the Action's effects should be carried out, acting in combination with the [Window Manager] and Apps themselves to provide a channel of communication.

Registry

The Registry is a core component of the App framework, accessible via the [Global ZLUX Object] at runtime. It holds information about which Apps are present in the environment, and what abilities each App has. This is important to App-to-App communication, as a target may not be a specific App, but rather any App of a category, or with a specific featureset, or capable of responding to the type of Action requested.

Pulling it all together in an example

The standard way to make use of App-to-App communication is by having Actions and Recognizers that are saved on the system. These get loaded at login, and then later either via a form of automation or by a user action, Recognizers can be polled to determine if there is an Action that can be executed. All of this is handled by the Dispatcher, but the description of the behavior lies in the Action and Recognizer used. In the Action and Recognizer sections above, you will see two JSON definitions: One is a recognizer of when the Terminal App is in a state, and another is an Action that will tell the MVS Explorer to load a PDS member for editing. Putting the two together, a practical application is that you can launch the MVS Explorer to edit a PDS member that you have selected within the Terminal App.