Workspace 6.21.5
Writing a Workspace Operation - looking at the code

In this section, we're going to understand the code that was automatically generated by Workspace so that we know how to customise our operation, and define its execute() function: the function that all operations use to do their work.


Customising the generated code (continue on with tutorial Writing a Workspace Operation )


calculaterectanglearea.h

Again, the header file should contain a header guard followed by any other header files this one requires. The Workspace/DataExecution/Operations/operation.h header provides the DataExecution::Operation base class that we have to inherit from. The Workspace/DataExecution/Operations/operationfactorytraits.h header provides the DECLARE_WORKSPACE_OPERATION_FACTORY macro which is the Workspace operation analogy of the DECLARE_WORKSPACE_DATA_FACTORY macro for Workspace data types.

#ifndef CSIRO_CALCULATERECTANGLEAREA_H
#define CSIRO_CALCULATERECTANGLEAREA_H
#include <memory>
#include "simpleplugin.h"

The next thing to do is to define our implementation class CalculateRectangleAreaImpl.

namespace CSIRO
{
class CalculateRectangleAreaImpl;
Top level namespace for all Workspace code.
Definition: applicationsupportplugin.cpp:32

For the purpose of this tutorial, we will create an operation that takes two double precision values as inputs and provides the product of the two values as an output. The definition of an operation class is usually simple, but we use an even more simplified class here so as not to complicate things. We will beef it up a bit in another tutorial. The class is defined like this:

class SIMPLEPLUGIN_API CalculateRectangleArea : public CSIRO::DataExecution::Operation
{
// Allow string translation to work properly
Q_DECLARE_TR_FUNCTIONS(CSIRO::CalculateRectangleArea)
std::unique_ptr<CalculateRectangleAreaImpl> pImpl_;
protected:
bool execute() override;
public:
CalculateRectangleArea();
~CalculateRectangleArea() override;
// Prevent copy and assignment - these should not be implemented
CalculateRectangleArea(const CalculateRectangleArea&) = delete;
CalculateRectangleArea& operator=(const CalculateRectangleArea&) = delete;
};
All workspace operations must derive from this class.
Definition: operation.h:118

There are a few things to note in this class definition. Firstly, there is no sign of any inputs or outputs anywhere. This is deliberate, because we don't want other code to know about how the operation does what it does, nor what data it stores internally. These are implementation details that should be hidden, which is why we have the CalculateRectangleAreaImpl implementation class. We will define CalculateRectangleAreaImpl in the calculaterectanglearea.cpp file shortly, but we don't need to know anything about it for now because we only store a pointer to a CalculateRectangleAreaImpl object, which we call pImpl_. This is a common name given to this sort of "pointer to implementation" object within Workspace.

The second thing to note is that, apart from the constructors and destructor, the only other thing in the class is the execute() function. This function is the heart of the operation, defining exactly what it does and how it does it. It is called at the appropriate times by the base class during Workspace execution. We will talk some more about this shortly.

At the end of the file is the DECLARE_WORKSPACE_OPERATION_FACTORY macro. Just like its analogous macro for Workspace data types, this macro defines the things needed by Workspace when it uses the factory. The first parameter is the fully scoped name of the operation and the second parameter should be the export symbol used between the class keyword and the class name in the class definition.

DECLARE_WORKSPACE_OPERATION_FACTORY(CSIRO::CalculateRectangleArea, SIMPLEPLUGIN_API)
#endif
#define DECLARE_WORKSPACE_OPERATION_FACTORY(T, WORKSPACE_EXPORT_SYMBOL)
Definition: operationfactorytraits.h:166

Customising the generated code (continue on with tutorial Writing a Workspace Operation )


calculaterectanglearea.cpp

We will show this implementation file in stages and talk about each part as we go. As usual, we start by including the required headers. This time, we have a few more that we need. For now, we'll just list them and hold off talking about them until we meet the things they define for us. We also include the usual namespace scopes and make things from the CSIRO::DataExecution namespace visible to make our code clearer:

#include "calculaterectanglearea.h"

The next thing we need to do is define our private implementation class:

class CalculateRectangleAreaImpl : public CSIRO::Application::TextLogger
{
// Allow string translation to work properly
Q_DECLARE_TR_FUNCTIONS(CSIRO::CalculateRectangleAreaImpl)
public:
CalculateRectangleArea& op_;
// Inputs and outputs
CalculateRectangleAreaImpl(CalculateRectangleArea& op);
bool execute();
};
Convenience base class for anything associated with an operation that needs to write text to the log ...
Definition: textlogger.h:66
Definition: simpleoperationio.h:43
Definition: simpleoperationio.h:230

The SimpleInput class is provided by the Workspace/DataExecution/InputOutput/simpleoperationio.h header. SimpleInput acts as a wrapper around a data object and presents it to an operation as a single named input. The SimpleOutput class is analogous, provided by the same Workspace/DataExecution/InputOutput/simpleoperationio.h header and with area_ acting as a wrapper around the area data object.

So: Inputs and Outputs wrap the data, giving it a name and exposing it to Workspace.

The definition of the CalculateRectangleAreaImpl constructor makes this clearer:

CalculateRectangleAreaImpl::CalculateRectangleAreaImpl(CalculateRectangleArea& op) :
CSIRO::Application::TextLogger(op),
op_(op),
width_("Width", op_),
height_("Height", op_),
area_("Area", op_)
{
// Make sure all of our inputs have data by default. If your operation accepts a
// large data structure as input, you may wish to remove this call and replace it
// with constructors for each input in the initialisation list above.
op_.ensureHasData();
// Recommend setting a description of the operation and each input / output here:
// op_.setDescription(tr("My operation does this, that and this other thing."));
// input1_.input_.setDescription(tr("Used for such and such."));
// output1_.output_.setDescription(tr("Results of the blah-di-blah."));
}

The first parameter to width_, height_ and area_ provides the name the input/output will be known by and the second parameter is the operation to add the input/output to.

bool CalculateRectangleAreaImpl::execute()
{
const auto& width = *width_;
const auto& height = *height_;
auto& area = *area_;
// =======================================================
// Put the implementation of your task or algorithm here
// =======================================================
area = width * height;
// If your operation always succeeds, leave the following line
// returning true. Otherwise, add your own logic to determine
// whether or not the operation succeeded and return true only
// if no error was encountered.
return true;
}

As you can see, all we have needed to do was add to our auto-generated execute function the following single line of code:

area = width * height;
Note
Note that for long-running operations, you will want to provide feedback to users during execution. To do that, see Providing User Feedback During Execution

If your operation involves InputArrays then both a TypedObject and a DataObject wll be generated. The following excerpt shows an alternative CalculateRectangleArea mechanism to illustrate how the two-object mechanism works in the CalculateRectangleAreaImpl declaration:

public:
CalculateRectangleArea& op_;
// Data objects
// Inputs and outputs
Scalar input class (only one output can connect to it at a time).
Definition: inputscalar.h:50
Class for operation outputs.
Definition: output.h:53

and then in the CalculateRectangleAreaImpl constructor:

CalculateRectangleAreaImpl::CalculateRectangleAreaImpl(CalculateRectangleArea& op) :
op_(op),
dataWidth_(),
dataHeight_(),
dataArea_(),
inputWidth_("Width", dataWidth_, op_),
inputHeight_("Height", dataHeight_, op_),
outputArea_("Area", dataArea_, op_)

You can think of TypedObject<X> as the same as a pointer to X. It adds some template magic to handle things like creating, destroying, cloning, assigning and serializing X objects, mostly without you having to do anything at all. In the above example, you can use dataWidth_, dataHeight_ and dataArea_ as though they were pointers. We will see this shortly when we define the execute() function. The TypedObject class is provided by the Workspace/DataExecution/DataObjects/typedobject.h header. Whenever we talk about "data objects", we are almost always referring to a TypedObject.

The InputScalar class is provided by the Workspace/DataExecution/InputOutput/inputscalar.h header. InputScalar acts as a wrapper around a data object and presents it to an operation as a single named input. In the above example, inputWidth_ will act as a wrapper around width_ and inputHeight_ will act as a wrapper around height_. The Output class is analogous, provided by the Workspace/DataExecution/InputOutput/output.h header and with output_ acting as a wrapper around the area_ data object.

So: DataObjects point to our data, and Inputs and Outputs wrap the data, giving it a name and exposing it to Workspace.

The CalculateRectangleArea constructor and destructor come next:

CalculateRectangleArea::CalculateRectangleArea() :
CSIRO::DataExecution::Operation(
CSIRO::DataExecution::OperationFactoryTraits<CalculateRectangleArea>::getInstance(),
tr("Calculate Rectangle Area")),
pImpl_(std::make_unique<CalculateRectangleAreaImpl>(*this))
{
}
CalculateRectangleArea::~CalculateRectangleArea() = default;

The CalculateRectangleArea constructor passes two important pieces of information to its base class. First, it passes the factory associated with the operation in the OperationFactoryTraits<CalculateRectangleArea>::getInstance() parameter. The other parameter specifies the default label to give to all new operations. This will be shown to the user and can contain more or less any text, although shorter is better. Spaces are allowed in the label, as the "Calculate rectangle area" example shows.

The constructor creates the private implementation object pImpl_ and that's all. The destructor simply deletes what the constructor created.

The second last thing to do is to define the execute() function:

bool CalculateRectangleArea::execute()
{
return pImpl_->execute();
}

Here we can see how the data objects are being treated exactly like pointers. A logical question to ask, however, is what actually created the data these pointers are pointing to? The answer is that this is all handled by the base class before execute() is called. It is always guaranteed (with one small caveat) that at the start of your execute() function, all your inputs and outputs have valid data in them. The small caveat is that, if you really want to, you can take control of the updating of inputs and outputs for yourself, but this is well beyond the scope of this simple tutorial and generally only needed in very special cases (see bringInputsUpToDate for more information).

The CalculateRectangleAreaImpl::execute() function must return a value to indicate whether it was successful or not. In this tutorial, the operation carries out a very trivial task that essentially cannot fail, so it always returns true to indicate success. If the operation were able to fail, then it could return false in those situations and Workspace will ensure that execution then comes to a halt.

The last thing we do is close off our namespaces and use a macro to define the factory for our operation:

using namespace CSIRO;
DEFINE_WORKSPACE_OPERATION_FACTORY(CalculateRectangleArea,
CSIRO::SimplePlugin::getInstance(),
CSIRO::DataExecution::Operation::tr("Simple Plugin"))
#define DEFINE_WORKSPACE_OPERATION_FACTORY(T, P, C)
Definition: typedoperationfactory.h:321

The DEFINE_WORKSPACE_OPERATION_FACTORY macro is just like the one we used for the data factory. It takes care of defining the operation factory for us. We have to supply three things to the macro: the operation it is for (CalculateRectangleArea), the plugin it belongs to (SimplePlugin::getInstance() ) and a category to file it under ("Simple plugin" in this example). The category is a sort of filing system for all the operations a Workspace knows about. If you've used the Workspace editor, you will have seen the operation catalogue which presents all the operations in a hierarchical tree organized by the category each operation factory specifies. Subcategories are easily made by using a forward slash. For example, "Tutorial/CoolStuff" would refer to a top level called "Tutorial" with a subcategory called "CoolStuff".


Customising the generated code (continue on with tutorial Writing a Workspace Operation )


simpleplugin.cpp

The operation creator wizard has automatically updated the simpleplugin.cpp file so that the plugin exposes the CalculateRectangleArea operation to Workspace. Two changes have been made to this file, the first of which is to include the new operation header at the top.

#include "simpleplugin.h"
#include <memory>
#include <QString>
#include <QStringList>
#include "calculaterectanglearea.h"

Secondly, a line of code has been added to the SimplePlugin::Setup function. This line will register our new OperationFactory (an auto-generated class which knows how to create our new operation) with Workspace, so that it can create CalculateRectangleArea operations as the user requests them.

bool SimplePlugin::setup()
{
// Add your data factories like this:
//addFactory( CSIRO::DataExecution::DataFactoryTraits<MyDataType>::getInstance() );
// Add your operation factories like this:
//addFactory( CSIRO::DataExecution::OperationFactoryTraits<MyOperation>::getInstance() );
// Add your widget factories like this:
//addFactory( MyNamespace::MyWidgetFactory::getInstance() );
// Add your adaptor factories like this:
//addFactory( CSIRO::DataExecution::SimpleAdaptorFactory<MyDataType, OtherDataType>::getInstance());
// Add your collection workflows here like this:
//addWorkspaceCollection(":/appdata/<MyPlugin>/myworkflow.wsx");
return true;
}
Traits class for operations of type T.
Definition: operationfactorytraits.h:64

Customising the generated code (continue on with tutorial Writing a Workspace Operation )


CMakeLists.txt

Lastly, the operation creator wizard has automatically updated the plugin CMakeLists.txt file to add the operation's header and source files in the three set functions:

set(HEADERS
${SIMPLEPLUGIN_SOURCE_DIR}/calculaterectanglearea.h
${SIMPLEPLUGIN_SOURCE_DIR}/simpleplugin_api.h
${SIMPLEPLUGIN_SOURCE_DIR}/simpleplugin.h
)
set(INSTALL_HEADERS
${SIMPLEPLUGIN_SOURCE_DIR}/calculaterectanglearea.h
${SIMPLEPLUGIN_SOURCE_DIR}/simpleplugin_api.h
${SIMPLEPLUGIN_SOURCE_DIR}/simpleplugin.h
)
set(SOURCES
${SIMPLEPLUGIN_SOURCE_DIR}/calculaterectanglearea.cpp
${SIMPLEPLUGIN_SOURCE_DIR}/simpleplugin.cpp
)

Customising the generated code (continue on with tutorial Writing a Workspace Operation )