-
Notifications
You must be signed in to change notification settings - Fork 42
App to App Communication
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.
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.
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.
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
}
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"
}
Actions can either be created dynamically at runtime, or saved and loaded by the system at login.
Actions can be created by calling this Dispatcher method: makeAction(id: string, defaultName: string, targetMode: ActionTargetMode, type: ActionType, targetPluginID: string, primaryArgument: any):Action
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/actions
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"
]
}
}
}
]
}
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.
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
}
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.
You can call the Dispatcher method, addRecognizer(predicate:RecognitionClause, actionID:string):void
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.
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.
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.
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.
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.