Skip to main content
Jump to: navigation, search

BPMN2-Modeler/DeveloperTutorials/CustomPropertyTabs

Versions

This tutorial was developed with Eclipse 4.4 (Luna) and BPMN2-Plugin version 1.1.1.

Creating custom property tabs

In this tutorial we will go into more detail about how to define Property sheets and create handlers that control the rendering of object features. You may wish to also read the CustomTask Tutorial for additional information.

A Property sheet tab starts with the definition of a <propertyTab> extension element. You can either use the form-based (visual) plugin.xml editor, or go directly to the bare metal with XML:

<propertyTab
    id="org.imixs.bpmn2.mytask.tab"
    class="org.imixs.bpmn2.MyTaskPropertySection"
    label="My Task"
    type="org.eclipse.bpmn2.Task"
    runtimeId="org.imixs.bpmn2.runtime">
</propertyTab>

Please refer to the Extension Point Schema document for a more detailed description of the <propertyTab> element.

The Property Section

The important thing in <propertyTab> is the class attribute: this should point to your property section implementation which subclasses the AbstractBpmn2PropertySection abstract class. A property section corresponds to one of the tabs in the Eclipse Property View. When the Property View is rendered, the editor collects all property sections (tabs) defined for your extension plugin and applies a filter to select only those that apply to the currently selected graphical element in the editor.

The property sections are rendered when the user clicks one of the tabs in the Property View. The property sections constructs and initializes an SWT Composite which contains the editing widgets for the selected graphical element. The property section class method then calls the createBindings() method, in which all the "magic" happens.

Here is the Java code for a sample property section implementation class:

public class MyTaskPropertySection extends DefaultPropertySection {

    public MyTaskPropertySection() {
        super();
    }

    @Override
    protected AbstractDetailComposite createSectionRoot() {
        // This constructor is used to create the detail composite for use in the Property Viewer.
        return new MyTaskDetailComposite(this);
    }

    @Override
    public AbstractDetailComposite createSectionRoot(Composite parent, int style) {
        // This constructor is used to create the detail composite for use in the popup Property Dialog.
        return new MyTaskDetailComposite(parent, style);
    }
}

Typically, the only thing your implementation class needs to do is provide factory methods for an AbstractDetailComposite that knows how to render editing widgets for the selected object's features.

AbstractDetailComposite and the createBindings() method

The model extension tutorial explains how to use the custom property section class with a very simple implementation of the createBindings() method:

....
@Override
public void createBindings(EObject be) {
   Task myTask = (Task)be;
   TaskConfig taskConfig = null;
   // Fetch all TaskConfig extension objects from the Task
   List<TaskConfig> allTaskConfigs = ModelDecorator.getAllExtensionAttributeValues(myTask, TaskConfig.class);
   if (allTaskConfigs.size()==0) {
      taskConfig = ModelFactory.eINSTANCE.createTaskConfig();
      TargetRuntime rt = getTargetRuntime();
      CustomTaskDescriptor ctd = rt.getCustomTask(ImixsTaskFeatureContainer.PROCESSENTITY_TASK_ID);
      EStructuralFeature feature = ctd.getModelDecorator().getEStructuralFeature(be, "taskConfig");
      ModelDecorator.addExtensionAttributeValue(myTask, feature, taskConfig, true);
   }
}

In this example a new empty TaskConfig element is created if it does not yet exists and is added to the Task object. But it is also possible to add some default values to the taskConfig object. See the following example:

taskConfig = ModelFactory.eINSTANCE.createTaskConfig();
// add some default values...           
Parameter paramAddress=ModelFactory.eINSTANCE.createParameter();
paramAddress.setName("address");
paramAddress.setValue("London");
taskConfig.getParameters().add(paramAddress);

NB: Notice that the call to ModelDecorator.addExtensionAttributeValue() in the above example can modify the model. Normally this would not be allowed because the editor wraps all model changes in an EMF transaction. Setting up and executing a change transaction should normally only be done in response to some user action, i.e. dropping an element from the tool palette on the canvas, changing the name of an element, etc. The Graphiti framework decodes and dispatches these user actions, and calls specific handlers in the BPMN2 Modeler to act on them. This should be the only time that your code can make model changes!

However, the Property sheet editing widgets require a concrete object as a target to persist their editing changes. So what we have is a classic chicken and egg dilemma. To get around this we create a "disconnected" object (one not contained in an EMF Resource) for the editing widgets, in the line

taskConfig = ModelFactory.eINSTANCE.createTaskConfig();

The object is then initialized as needed and an InsertionAdapter is used to manage the object. Whenever some value of this disconnected object is changed by an editing widget, an EMF change notification is fired and caught by the InsertionAdapter. The adapter then creates a transaction, inserts the disconnected object into its parent, and commits the transaction.

See the discussion of Special Adapters for a more detailed explanation of this mechanism.


Bind Attributes

Now that we have defined concrete parameters for our custom task, these objects can be bound to the property section. The AbstractDetailComposite provides a set of methods to bind an EObject to a text input widget:

bindAttribute(parent, paramAddress,"value","Please enter the address here");

The third parameter ‘value’ is the name of the attribute of our Parameter class to be bound.

Create custom controls

If the bind methods provided by the AbstractDetailComposite do not fit your needs, you can create input widgets manually. The package org.eclipse.bpmn2.modeler.core.merrimac.dialogs contains several specialized editing widgets which may be of interest to the developer. See the following example which creates the same result as before:

TargetRuntime rt = getTargetRuntime();
CustomTaskDescriptor ctd = rt.getCustomTask(ImixsTaskFeatureContainer.PROCESSENTITY_TASK_ID);
EStructuralFeature feature= ctd.getModelDecorator().getEStructuralFeature(paramAddress, "value");
ObjectEditor editor = new TextObjectEditor(this,paramAddress,feature);
editor.createControl(parent,"Please enter the Address here");

Custom Layout

You can also customize the layout of your property section in createBindings().

....
@Override
public void createBindings(EObject be) {
   Composite mailAttributesSection = createSectionComposite(parent, "Mail Konfiguration");
   // fill in the contents of the mail section here by
   // creating some ObjectEditor widgets.
   createLabel(mailAttributesSection, "Some text");
   ...

   super.createBindings(be);
}

Note that the order of appearance of your additions depends on when the super.createBindings() is called: in this case the mailAttributesSection will appear at the top of the Property sheet, then all of the other widgets are rendered below that.

Hiding widgets

The AbstractDetailComposite createBindings() method, by default, will render all features that are enabled for a given object. Object and feature enablement is controlled by a User Preference in the BPMN2 Editor Tool Profiles section, so you can either disable rendering for certain features through the User Preferences dialog, or you can do it programmatically.

To hide selected object features in your Java code, you will need to override the following three methods from AbstractDetailComposite:

protected void bindAttribute(Composite parent, EObject object, EAttribute attribute, String label)
protected void bindReference(Composite parent, EObject object, EReference reference)
protected AbstractListComposite bindList(EObject object, EStructuralFeature feature, EClass listItemClass)

In each of these methods, you can check for the feature by name or feature ID, for example:

@Override
protected void bindList(EObject object, EStructuralFeature feature, EClass listItemClass) {
   if ("parameters".equals(feature.getName()))
     return null; // ignore the "parameters" list
   else
      return super.bindAttribute(parent, object, attribute, label);
}

The full example

Here is the full example of a custom property section where a param list is initialized with default values and bound to the AttributesParent composite:

package org.imixs.bpmn.model;

import java.util.List;

import org.eclipse.bpmn2.Task;
import org.eclipse.bpmn2.di.BPMNDiagram;
import org.eclipse.bpmn2.modeler.core.merrimac.clad.AbstractBpmn2PropertySection;
import org.eclipse.bpmn2.modeler.core.merrimac.clad.AbstractDetailComposite;
import org.eclipse.bpmn2.modeler.core.model.ModelDecorator;
import org.eclipse.bpmn2.modeler.core.runtime.CustomTaskDescriptor;
import org.eclipse.bpmn2.modeler.core.runtime.TargetRuntime;
import org.eclipse.bpmn2.modeler.core.utils.BusinessObjectUtil;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.swt.widgets.Composite;

/**
 * This PorpertySection provides the attributes for Mail config.
 * 
 * @author rsoika
 *
 */
public class MailPropertySection extends AbstractBpmn2PropertySection {

    public MailPropertySection() {
        super();
    }

    @Override
    protected AbstractDetailComposite createSectionRoot() {
        // This constructor is used to create the detail composite for use in
        // the Property Viewer.
        return new MyTaskDetailComposite(this);
    }

    @Override
    public AbstractDetailComposite createSectionRoot(Composite parent, int style) {
        // This constructor is used to create the detail composite for use in
        // the popup Property Dialog.
        return new MyTaskDetailComposite(parent, style);
    }

    /**
     * Here we extract the bpmn task element from the current ISelection
     */
    @Override
    public EObject getBusinessObjectForSelection(ISelection selection) {

        EObject bo = BusinessObjectUtil.getBusinessObjectForSelection(selection);
        if (bo instanceof BPMNDiagram) {
            if (((BPMNDiagram) bo).getPlane() != null && ((BPMNDiagram) bo).getPlane().getBpmnElement() != null)
                return ((BPMNDiagram) bo).getPlane().getBpmnElement();
        }
        return bo;
    }

    public class MyTaskDetailComposite extends AbstractDetailComposite {

        public MyTaskDetailComposite(AbstractBpmn2PropertySection section) {
            super(section);
        }

        public MyTaskDetailComposite(Composite parent, int style) {
            super(parent, style);
        }

        @Override
        public void createBindings(EObject be) {
            TaskConfig taskConfig = null;

            if (!(be instanceof Task)) {
                System.out.println("WARNING: this proeprty tab in only working with Tasks Please check por plugin.xml!");
            }

            TargetRuntime rt = getTargetRuntime();
            // Get the CustomTaskDescriptor for this Task.
            CustomTaskDescriptor ctd = rt.getCustomTask(ImixsTaskFeatureContainer.PROCESSENTITY_TASK_ID);

            Task myTask = (Task) be;

            // Fetch all TaskConfig extension objects from the Task
            List<TaskConfig> allTaskConfigs = ModelDecorator.getAllExtensionAttributeValues(myTask, TaskConfig.class);
            if (allTaskConfigs.size() == 0) {
                // There are none, so we need to construct a new TaskConfig
                // which is required by the Property Sheet UI.
                taskConfig = ModelFactory.eINSTANCE.createTaskConfig();

                // initalize values
                initializeProperty(taskConfig, "txtMailSubject", "");
                initializeProperty(taskConfig, "namMailReceiver", "");
                initializeProperty(taskConfig, "keyMailReceiverFields", "");
                initializeProperty(taskConfig, "namMailReceiverCC", "");
                initializeProperty(taskConfig, "keyMailReceiverFieldsCC", "");
                initializeProperty(taskConfig, "rtfMailBody", "");

                // Get the model feature for the "taskConfig" element name.
                // Again, this must match the <property> element in <customTask>
                EStructuralFeature feature = ctd.getModelDecorator().getEStructuralFeature(be, "taskConfig");

                // Add the newly constructed TaskConfig object to the Task's
                // Extension Values list.
                // Note that we will delay the actual insertion of the new
                // object until some feature
                // of the object changes (e.g. the Parameter.name)
                ModelDecorator.addExtensionAttributeValue(myTask, feature, taskConfig, true);

            } else {
                // Else reuse the existing TaskConfig object.
                taskConfig = allTaskConfigs.get(0);
            }

            setTitle("Mail Configuration");
            bindAttribute(this.getAttributesParent(), getProperty(taskConfig, "namMailReceiver"), "value", "To");
            bindAttribute(this.getAttributesParent(), getProperty(taskConfig, "txtMailSubject"), "value", "Subject");
            bindAttribute(this.getAttributesParent(), getProperty(taskConfig, "rtfMailBody"), "value", "Body");
        }

    }

    /**
     * This method verifies if a specific property still exists. If not the
     * method initializes the value
     * 
     * @param taskConfig
     * @param propertyName
     */
    protected Parameter initializeProperty(TaskConfig taskConfig, String propertyName, String defaultVaue) {

        // test all parameters if we have the propertyName
        EList<Parameter> parameters = taskConfig.getParameters();
        for (Parameter param : parameters) {
            if (param.getName().equals(propertyName)) {
                // param allready exists
                return param;
            }
        }

        // the property was not found so we initialize it...
        Parameter param = ModelFactory.eINSTANCE.createParameter();
        param.setName(propertyName);
        param.setValue(defaultVaue);
        taskConfig.getParameters().add(param);
        return param;
    }

    /**
     * THis method returns the Parameter object for a specific object. If the
     * object did not exist the method creates an empty new parameter
     * 
     * @param taskConfig
     * @param propertyName
     * @return
     */
    protected Parameter getProperty(TaskConfig taskConfig, String propertyName) {

        // test all parameters if we have the propertyName
        EList<Parameter> parameters = taskConfig.getParameters();
        for (Parameter param : parameters) {
            if (param.getName().equals(propertyName)) {
                // param allready exists
                return param;
            }
        }

        // we have not found this param - so we add a new one....
        return initializeProperty(taskConfig, propertyName, "");
    }
}

The property tab will look like this:

screen07

The XML for the Task element with these extensions will look like this in the bpmn2 file:

 <bpmn2:task id="Task_2" imixs:type="MyTask" imixs:Imixs="Hello World" imixs:benefit="0" name="Task 2">
  <bpmn2:extensionElements>
   <imixs:taskConfig>
    <imixs:parameter xsi:type="imixs:Parameter" name="txtMailSubject" value="Hello"/>
    <imixs:parameter xsi:type="imixs:Parameter" name="namMailReceiver" value="ralph.soika@imixs.com"/>
    <imixs:parameter xsi:type="imixs:Parameter" name="keyMailReceiverFields" value=""/>
    <imixs:parameter xsi:type="imixs:Parameter" name="namMailReceiverCC" value=""/>
    <imixs:parameter xsi:type="imixs:Parameter" name="keyMailReceiverFieldsCC" value=""/>
    <imixs:parameter xsi:type="imixs:Parameter" name="rtfMailBody" value="Some message...."/>
   </imixs:taskConfig>
  </bpmn2:extensionElements>
  ...
 </bpmn2:task>

The mysterious IllegalStateException

Sometimes it is necessary to modify an object's feature during setup of the Property sheet in createBindings(). For example, let's assume we need to insert a new Parameter object in a previously created TaskConfig attached to an existing Task. We could do something like this:

@Override
public void createBindings(EObject be) {
   super.createBindings(be);
   ...
   Parameter param = ModelFactory.eINSTANCE.createParameter();
   param.setName(propertyName);
   param.setValue(defaultVaue);
   taskConfig.getParameters().add(param);
}

But if you try this you will get a java.lang.IllegalStateException with the error message: “Cannot modify resource set without a write transaction.” To get around this problem you should use the InsertionAdapter. This adapter will insert a new value into its container feature only when the owning object's content changes. This allows the UI to construct new objects in the correct way:

@Override
public void createBindings(EObject be) {
   super.createBindings(be);
   ....
    Parameter param = ModelFactory.eINSTANCE.createParameter();
    param.setName(propertyName);
    param.setValue(defaultVaue);
    // use the InsertionAdapter to add the new parameter....
    InsertionAdapter.add(taskConfig, MyModelPackage.eINSTANCE.getTaskConfig_Parameters(), param);
   ....
 }

For more information about this adapter, please see the Adapters tutorial.

Adding & Removing extension elements

Sometimes we need to be able to control whether or not an extension object is serialized to the bpmn2 file. What we don't want, for example, is something that looks like this:

  <bpmn2:process id="process_4" name="Default Process" isExecutable="false">
    <bpmn2:extensionElements>
      <mm:metaData></mm:metaData>
    </bpmn2:extensionElements>

In this example, our metaData object has no features with non-default values, and so it appears as just an empty element. Whether or not the appearance of an empty metaData element has any significance to our BPM engine is another story, but the assumption is: if it's empty, it should not be there in the first place!

Let's assume our plugin design calls for the addition of an extension element whose type is MetaData. It should have two string attributes, name and value where the name is required, but value may be empty.

With the addition of this new object, our Property sheet should look like this:

BPMN2-Modeler-CustomPropertyTabs-addremove.png

The question is: how can we control the existence (and therefore serialization) of the metaData object? As always, we have some options here:

  1. We could control serialization of this object with our own implementation of the Bpmn2ModelerResourceImpl class
  2. We could add some code that recognizes when the user erases the name attribute, and delete the object
  3. We could use push buttons to add and remove the object

Option 1 requires yet another specialization of an API class, and is quite complicated - we would rather not go there.

Option 2 would work, since that would violate the "name may not be empty" rule and we wouldn't want to serialize an invalid object. However, what should we do with the contents of the value field? We can't simply erase it because the user's intention may have only been to change the name of the object, not erase it completely.

In this scenario, 3 is clearly our best option. Initially, our Property sheet would look like this before a MetaData object is created:

BPMN2-Modeler-CustomPropertyTabs-add.png

If a MetaData object already exists, it is used to populate the text editing fields and a "Remove" button is included.

BPMN2-Modeler-CustomPropertyTabs-remove.png

Not only does this simplify lifecycle management of our object, but it also becomes very clear to the user what is going on here: if I see an "Add" button, I guess I need to push it to create one of these MetaData things; if I see the text fields and a "Remove" button I can either edit what's there, or delete it.

Here is the Java code behind this trick:

@Override
public void createBindings(final EObject be) {
    super.createBindings(be);
    // in our example, all BaseElements may have a MetaData extension element
    if (be instanceof BaseElement)
        bindMetaData((BaseElement)be);
}

protected void bindMetaData(final BaseElement be) {
    if (isModelObjectEnabled(METADATA_CLASS)) {
        // create a new Property Tab section with a twistie
        Composite section = createSectionComposite(this, "Metadata");
        // get the MetaData object from this BaseElement's extension elements
        MetaData metaData = (MetaData) findExtensionAttributeValue(be, METADATA_FEATURE);
        if (metaData==null) {
            // the BaseElement does not have one
            // create a button to add a new one
            Button button = toolkit.createButton(section, "Add Metadata", SWT.PUSH);
            // since the Property Tab composite has 3 columns, we need to add some fillers
            toolkit.createLabel(section, ""); //$NON-NLS-1$
            toolkit.createLabel(section, ""); //$NON-NLS-1$
            button.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    // create the new MetaData and insert it into the
                    // BaseElement's extension elements container
                    // Note that this has to be done inside a transaction
                    TransactionalEditingDomain domain = getDiagramEditor().getEditingDomain();
                    domain.getCommandStack().execute(new RecordingCommand(domain) {
                        @Override
                        protected void doExecute() {
                            MetaData metaData = MyModelFactory.eINSTANCE.createMetaData();
                            metaData.setValue("");
                            addExtensionAttributeValue(be, METADATA_FEATURE, metaData);
                            setBusinessObject(be);
                        }
                    });
                }
            });
        }
        else {
            // create text editors for the MetaData name and value
            TextObjectEditor nameEditor = new TextObjectEditor(this, metaData, METADATA_NAME);
            TextObjectEditor valueEditor = new TextObjectEditor(this, metaData, METADATA_VALUE);
            valueEditor.setMultiLine(true);
            nameEditor.createControl(section, "Name");
            valueEditor.createControl(section, "Value");

            // create a button (with fillers) to remove this MetaData
            toolkit.createLabel(section, ""); //$NON-NLS-1$
            Button button = toolkit.createButton(section, "Remove Metadata", SWT.PUSH);
            toolkit.createLabel(section, ""); //$NON-NLS-1$
            button.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    // remove the MetaData object from this BaseElement
                    TransactionalEditingDomain domain = getDiagramEditor().getEditingDomain();
                    domain.getCommandStack().execute(new RecordingCommand(domain) {
                        @Override
                        protected void doExecute() {
                            removeExtensionAttributeValue(be, METADATA_FEATURE);
                            setBusinessObject(be);
                        }
                    });
                }
            });
        }
    }
}

/**
 * Find the first entry in this BaseElement's extension elements container
 * that matches the given structural feature.
 * 
 * @param be a BaseElement
 * @param feature the structural feature to search for
 * @return the value of the extension element
 */
Object findExtensionAttributeValue(BaseElement be, EStructuralFeature feature) {
    for (ExtensionAttributeValue eav : be.getExtensionValues()) {
        for (FeatureMap.Entry entry : eav.getValue()) {
            if (entry.getEStructuralFeature() == feature) {
                return entry.getValue();
            }
        }
    }
    return null;
}

/**
 * Add a new extension element to the given BaseElement.
 * 
 * @param be a BaseElement
 * @param feature the structural feature
 */
void addExtensionAttributeValue(BaseElement be, EStructuralFeature feature, Object value) {
    ExtensionAttributeValue eav = null;
    if (be.getExtensionValues().size()>0) {
        // reuse the <bpmn2:extensionElements> container if this BaseElement already has one
        eav = be.getExtensionValues().get(0);
    }
    else {
        // otherwise create a new one
        eav = Bpmn2Factory.eINSTANCE.createExtensionAttributeValue();
        be.getExtensionValues().add(eav);
    }
    eav.getValue().add(feature, value);
}

/**
 * Remove an extension element from the given BaseElement that matches the
 * given structural feature.
 * 
 * @param be a BaseElement
 * @param feature the structural feature
 */
void removeExtensionAttributeValue(BaseElement be, EStructuralFeature feature) {
    for (ExtensionAttributeValue eav : be.getExtensionValues()) {
        for (FeatureMap.Entry entry : eav.getValue()) {
            if (entry.getEStructuralFeature() == feature) {
                be.getExtensionValues().remove(eav);
                return;
            }
        }
    }
}

We start as usual with the createBindings() method: first we render all of the features for this object by calling super.createBindings() then, if the object is a BaseElement, call our own bindMetaData() method. Recall that only objects derived from BaseElement can have extension elements!

In our bindMetaData() method, we search for a MetaData object in the list of extension elements. If one is found, we create TextObjectEditors for name and value, and finish off with a "Remove" button. The button-push handler for "Remove" deletes the MetaData object from the extension elements.

If there is no MetaData extension element, we simply display an "Add" button. In the handler code for this button we create a new MetaData, initialize it, and add it to the parent object's extension elements list.

Note that unlike our previous example, here it is OK to create a transaction for this model change because a deliberate user action changes the visual state of the editor.


Replacing existing property tabs

It is also possible to overwrite an existing property tab. There for you simply can add the 'replaceTab' attribute to your propertyTab extension:

 <propertyTab
           afterTab="org.imixs.bpmn.propertytab.task.workflow"
           class="org.imixs.bpmn.ui.task.ProcessApplicationPropertySection"
           id="org.imixs.bpmn.propertytab.task.application"
           label="Application"
           replaceTab="org.eclipse.bpmn2.modeler.activity.io.tab"
           runtimeId="org.imixs.workflow.bpmn.runtime"
           type="org.eclipse.bpmn2.Task">
 </propertyTab>

This will replace the BPMN2 property tab 'org.eclipse.bpmn2.modeler.activity.io.tab' with a new tab called 'org.imixs.bpmn.propertytab.task.application'.

If you want to control that the property tab only replaces an existing tag for a specific model object you can overwrite the method doReplaceTab() to verify if selcection is matching your model object:


 @Override
 public boolean doReplaceTab(String id, IWorkbenchPart part,
 			ISelection selection) {
   		// get the current business object form the selection...
 		EObject businessObject = BusinessObjectUtil
 				.getBusinessObjectForSelection(selection);
     		// test if the object matches to the customTask extension....
 		EStructuralFeature feature = ModelDecorator.getAnyAttribute(
 				businessObject, "processid");
 		if (feature != null && feature instanceof EAttribute) {
 			if (ImixsRuntimeExtension.targetNamespace
 					.equals(((EAttributeImpl) feature).getExtendedMetaData()
 							.getNamespace())) {
 				// match - so replace tab....
 				return true;
 			}
 		}  		
 		// no match - we do not replace this tab....
 		return false;
	}

Copyright © Eclipse Foundation, Inc. All Rights Reserved.