Workspace 6.21.5
|
Many Workspace capabilities rely on event handling functionality behind the scenes. This functionality allows plugins, widgets, datatypes and operations to monitor workflows (and other objects) and respond to specific events, such as progress updates, completion notifications and user interactions. Developers can even define their own event types.
This tutorial describes how to respond to events inside a workflow or operation, as well as how to create and fire your own event types.
Various capabilities within Workspace are event-based, that is to say that they rely on the ability to create "events" in response to certain conditions, with the knowledge that one or more "observers" may respond to those events. Frequently, this kind of behaviour is used to separate user-interface concerns from underlying workflow or application logic: workflows can execute cleanly without having to consider which components will respond to any events that it raises. Event handling is also often used to manage synchronisation where multiple threads are used. Some examples of when you may wish to implement event handling in your Workspace plugin or application include:
There are three event handling mechanisms available to Workspace developers:
These three different solutions and their associated pros and cons are described in the sections below.
If you are implementing a widget or other user interface component that inherits (either directly or indirectly) from QWidget, a number of protected "event handling methods" will be available to you. For example, here are a number of the methods provided by QWidget itself:
The Qt Framework (the framework upon which Workspace is built) ensures that these events are invoked in response to the appropriate user actions. To take action in response to one of these events in your class, simply override the appropriate protected method and provide an appropriate implementation.
You cannot use this method of event handling if:
In this case, you will need to select one of the other two event handling approaches.
Signals and Slots are also mechanisms provided by the Qt Framework. Developers are able to define both Signals and Slots on any of their classes that a) inherit from QObject, and b) include the Q_OBJECT macro in their class definition. Signals defined in one class can be connected to one or more Slots (or indeed, other signals) defined in another. The result of this is that Slots are executed whenever the connected Signals are emitted in executing code.
Information on how to define Signals and Slots in your classes can be found on the Qt website.
The only piece of information not mentioned in the above article that is pertinant to Workspace developers is that any class that defines Signals or Slots must be placed in the MOC_HEADERS section in one of your project's CMakeLists.txt file:
This is critical, because it tells CMake to ensure these classes are preprocessed with Qt's Meta Object Compiler (MOC). The Meta Object Compiler generates the extra C++ code required to handle the connections defined in the code.
Signals and Slots provide the most straightforward approach to event handling, and as such are the recommended approach wherever standard event methods are not provided (see QWidget event handling methods). You cannot use Signals and Slots as your event handling technique if any of the following are true:
Observers and ObservableEvents are similar to Qt Signals and Slots, but instead of being implemented using the Qt Meta Object Compiler, they are implemented using templates. This means that while they are not as simple to use as Signals and Slots, they can be used anywhere in the code, with the only slight drawback being syntax.
Observers, Observables and ObservableEvents are relatively simple to understand:
With relatively few exceptions, most events in the core Workspace framework and plugins are implemented as ObservableEvents, which means you will need to use Observers to respond to them. Some examples of these event types are:
All of the core event types are defined in the following header files:
The easiest way to respond to ObservableEvents that are raised is to use an ObserverSet. ObserverSets provide a convenient interface for creating observers, while also ensuring that any observers share the lifetime of the ObserverSet. That is to say, the Observers will be deleted when the ObserverSet is deleted. Below is an example of an operation implementation that uses an ObserverSet to create an Observer that will respond when an element is added to an input array:
It is possible to create observers directly using the various template interfaces defined in the Observer class header, however, these should only be used where it is necessary to micro-manage the lifetime of the observer. For example, due to circular dependencies or thread lifetimes, it may be necessary to manually delete observers in certain circumstances.
Depending on how your application code is structured and what types of events you are dealing with, you may wish to specify how the observers are handled between different application threads. For example, Workspace workflows are generally executed using one or more execution threads, so when accessing workflow data during observer event handling methods, care needs to be taken to avoid deadlocks and race conditions.
Workspace provides three different observer types to support the developer in this regard:
The easiest way to specify the observer type is by using the ObserverType template parameter:
To create event notifications, the first step is to make sure that your class inherits Observable. In the below code, we are creating an Observable sub-class called "LongRunningProcess":
No virtual functions are required to be implemented on the derived class. It now has access to all the functions needed to notify any observers that are attached to it. To raise an event, we simply need to construct one and call the member function notifyEvent()
. In the below example, our LongRunningProcess class is going to notify observers of progress being made during its execute method call:
There are many reasons why you may want to create a custom event type, the most common of which is to provide the event with the means of storing data. This way, the event handling function does not need to access methods or data on the Observable itself, as all the data needed is stored on the event object. This essentially guarantees that the events can be raised in a thread safe way and monitored using the ThreadSafeObserver type, maximising performance. This is of course not possible for all event types, as the costs of copying the data into the event object may be prohibitive.
To create a custom event type, all that is needed is to extend the template class ObservableEventType. For example, here is some code to create a new ProcessTerminated event type:
Now in any class that derives from Observable, ProcessTerminatedEvents can be created using the below code. In this case we are assuming that there is a Process object available as a local variable "process" with a getId method:
We have now learned how to handle events within Workspace. We've learned about: