Workspace 6.21.5
Event Handling in Workspace

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.




Tutorial contents




Overview

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:

  • Reporting progress of an operation or background thread
  • Notifying users when a workflow completes or aborts
  • Responding to user interactions such as an input or output being removed

There are three event handling mechanisms available to Workspace developers:

  1. QWidget event handling methods
  2. Qt signals and slots
  3. Observers, Observables and ObservableEvents

These three different solutions and their associated pros and cons are described in the sections below.

QWidget event handling methods

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:

virtual void closeEvent(QCloseEvent *event)
virtual void contextMenuEvent(QContextMenuEvent *event)
virtual void dragEnterEvent(QDragEnterEvent *event)
virtual void dragLeaveEvent(QDragLeaveEvent *event)
virtual void dragMoveEvent(QDragMoveEvent *event)
virtual void dropEvent(QDropEvent *event)
virtual void enterEvent(QEvent *event)
virtual void focusInEvent(QFocusEvent *event)

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:

  • You are not implementing a QWidget-based user interface component
  • The event you are attempting to respond to is not available as one of the protected methods on the base class from which your class extends

In this case, you will need to select one of the other two event handling approaches.

Qt signals and slots

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:

list(APPEND MOC_HEADERS
${MYPLUGIN_SOURCE_DIR}/classwithslot.h
${MYPLUGIN_SOURCE_DIR}/classwithsignal.h
)

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:

  • You are implementing a template class. The Q_OBJECT macro is not able to be defined in a template class, as the MOC compiler is unable to process templates appropriately. In this case, you will need to use Observers and ObservableEvents to manage your event handling. See section Observers, Observables and ObservableEvents.
  • You are attempting to respond to an event that is already implemented as an ObservableEvent rather than a Signal. See the section Observers, Observables and ObservableEvents for more information.

Observers, Observables and ObservableEvents

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:

  • Observers monitor an Observable, waiting to respond to events of a particular type (ObservableEvents)
  • Any class that derives from Observable is able to notify observers when events occur
  • A class that derives from ObservableEvent represents a unique event type in the system

Observing events

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:

  • InputAddedEvent
  • ProgressEvent
  • AddOperationEvent
  • UpdateEvent

All of the core event types are defined in the following header files:

#include "Workspace/DataExecution/InputsOutputs/iobaseevents.h"
#include "Workspace/DataExecution/InputsOutputs/inputscalarevents.h"
#include "Workspace/DataExecution/InputsOutputs/inputarrayevents.h"
#include "Workspace/DataExecution/InputsOutputs/progressevent.h"

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:

#include <iostream>
#include "Workspace/DataExecution/Events/inputarrayevents.h"
//
// Implementation class (printstrings.cpp)
//
class PrintStringsImpl
{
private:
ObserverSet observers_;
public:
TypedObject<QString> dataStrings_;
InputArray inputStrings_;
//
// Constructor
//
PrintStringsImpl(PrintStrings& op) :
dataStrings_(),
inputStrings_("Strings", dataStrings_, op)
{
observers_.add(inputStrings_, InputElementAddedEvent::eventID(),
*this, &PrintStringsImpl::inputElementAdded, ThreadedObserver);
}
//
// Event handling function. Invoked whenever InputElementAdded event notification occurs.
//
void inputElementAdded(const InputElementAddedEvent& e)
{
std::cout << tr("An input element was added!") << std::endl;
}
};

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.

Observer types

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:

  • UnthreadedObserver: These observers must be created and notified in the same thread. If created in a different thread from the Observable that they are monitoring, a run-time error will be raised indicating the problem.
  • ThreadedObserver: A ThreadedObserver will use a semaphore to block the thread in which the Observable's code is running in order to safely notify the Observer. This allows the event handling method to safely access data of the Observable even if it is running in a separate thread. While this resource safety is useful, it is worth considering the performance implications of blocking using Semaphores, as this may be a problem in performance critical situations.
  • ThreadSafeObserver: ThreadSafeObservers assume that they are running in a threadsafe environment. No protections against deadlocks or race conditions are provided. Generally speaking, these observers are useful where no data owned by the Observable needs to be accessed during the notification event (e.g. all of the relevant data is stored on the event object itself, or no data is needed).

The easiest way to specify the observer type is by using the ObserverType template parameter:

observers_.add(inputStrings_, InputElementAddedEvent::eventID(),
*this, &PrintStringsImpl::inputElementAdded, UnthreadedObserver);
observers_.add(inputStrings_, InputElementAddedEvent::eventID(),
*this, &PrintStringsImpl::inputElementAdded, ThreadedObserver);
observers_.add(inputStrings_, InputElementAddedEvent::eventID(),
*this, &PrintStringsImpl::inputElementAdded, ThreadSafeObserver);

Raising events

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":

#include "Workspae/DataExecution/Events/observable.h"
class LongRunningProcess : public DataExecution::Observable
{
public:
LongRunningProcess() :
Observable()
{
}
};

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:

void LongRunningProcess::execute()
{
for (int i = 0; i < numberOfTasks_; ++i)
{
// Do something complicated here
int progress = static_cast<float>(i) / numberOfTasks_ * 100.0;
notifyEvent(DataExecution::ProgressEvent(progress));
}
}
double i
Definition: opencljuliaset.cpp:45

Defining new event types

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:

class ProcessTerminatedEvent : public DataExecution::ObservableEventType<ProcessTerminatedEvent>
{
public:
ProcessTerminatedEvent(int processId) :
processId_(processId)
{
}
int getProcessId() const { return processId_; }
private:
int processId_;
};

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:

notifyEvent(ProcessTerminatedEvent(process.getId()));

Summary

We have now learned how to handle events within Workspace. We've learned about:

  • The different mechanisms for event handling availale in Workspace: overriding protected event methods, signals and slots, and observers.
  • The differences between each approach, and their limitations.
  • How to use Observers, define our own Observable classes, and create our own ObservableEvents.