Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Difference between revisions of "BPMN2-Modeler/DeveloperTutorials/CustomPropertyTabs"

m
Line 23: Line 23:
 
       EStructuralFeature feature = ctd.getModelDecorator().getEStructuralFeature(be, "taskConfig");
 
       EStructuralFeature feature = ctd.getModelDecorator().getEStructuralFeature(be, "taskConfig");
 
       ModelDecorator.addExtensionAttributeValue(myTask, feature, taskConfig, true);
 
       ModelDecorator.addExtensionAttributeValue(myTask, feature, taskConfig, true);
 +
  }
 
}
 
}
 
</pre>
 
</pre>
  
In this example a new empty taskConfig element is created if it does not yet exists. The new object is added to the Task object. But it is also possible ro add some default values to the taskConfig element. See the following example:
+
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:
  
 
<pre>
 
<pre>
Line 37: Line 38:
 
</pre>
 
</pre>
  
In the same way you can evaluate existing values and change them to your needs.
+
----
 +
 
 +
'''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 [http://en.wikipedia.org/wiki/Chicken_or_the_egg 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
 +
 
 +
<pre>taskConfig = ModelFactory.eINSTANCE.createTaskConfig();</pre>
 +
 
 +
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 [[BPMN2-Modeler/DeveloperTutorials/Adapters discussion of Special Adapters]] for a more detailed explanation of this mechanism.
 +
 
 +
----
  
 
===Bind Attributes===
 
===Bind Attributes===
Now as 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:
+
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:
  
 
<pre>bindAttribute(parent, paramAddress,"value","Please enter the address here");</pre>
 
<pre>bindAttribute(parent, paramAddress,"value","Please enter the address here");</pre>
Line 47: Line 60:
  
 
===Create custom controls===
 
===Create custom controls===
If the bind methods provided by the AbstractDetailComposite did not fit your needs, you can create input widgets manually. Also here some convenience methods are provided by BPMN2. See the following example which creates the same result as before:
+
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:
  
 
<pre>
 
<pre>
 
TargetRuntime rt = getTargetRuntime();
 
TargetRuntime rt = getTargetRuntime();
 
CustomTaskDescriptor ctd = rt.getCustomTask(ImixsTaskFeatureContainer.PROCESSENTITY_TASK_ID);
 
CustomTaskDescriptor ctd = rt.getCustomTask(ImixsTaskFeatureContainer.PROCESSENTITY_TASK_ID);
EStructuralFeature feature= ctd.getModelDecorator()
+
EStructuralFeature feature= ctd.getModelDecorator().getEStructuralFeature(paramAddress, "value");
.getEStructuralFeature(paramAddress, "value");
+
 
ObjectEditor editor = new TextObjectEditor(this,paramAddress,feature);
 
ObjectEditor editor = new TextObjectEditor(this,paramAddress,feature);
 
editor.createControl(parent,"Please enter the Address here");
 
editor.createControl(parent,"Please enter the Address here");
Line 59: Line 71:
  
 
==Custom Layout==
 
==Custom Layout==
You can also customize the layout of your property section. In this case you overwrite the method initialize(). This method is called before createBindings(). So this is the right place to add custom composites and layout your section.
+
You can also customize the layout of your property section in createBindings().
  
 
<pre>
 
<pre>
public void initialize() {
+
....
super.initialize();
+
@Override
Section mailAttributesSection = createSection(parent,
+
public void createBindings(EObject be) {
"Mail Konfiguration", true);
+
  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);
 +
}
 +
</pre>
 +
 
 +
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.
  
mailAttributesSection.setLayoutData(new GridData(SWT.FILL, SWT.TOP,
+
To hide selected object features in your Java code, you will need to override the following three methods from '''AbstractDetailComposite''':
true, true, 3, 1));
+
Composite mailAttributesComposite = toolkit
+
.createComposite(mailAttributesSection);
+
mailAttributesSection.setClient(mailAttributesComposite);
+
mailAttributesComposite.setLayout(new GridLayout(3, false));
+
this.createLabel(mailAttributesComposite, "Some text");
+
  
}
+
<pre>
 +
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)
 
</pre>
 
</pre>
  
====Hide the default property list====
+
In each of these methods, you can check for the feature by name or feature ID, for example:
If you subclass a custom property composite from ‘AbstractDetailComposite’ this will automatically bind the properties to a list element which is shown in top of your property section. To avoid the generation of such an default list element you need to place at least one element into the ‘AttributesParent’ composite provided by the ‘AbstractDetailComposite':
+
  
 
<pre>
 
<pre>
setTitle("Mail Configuration");
+
@Override
bindAttribute(this.getAttributesParent(), paramAddress,"value","Please enter the address here");
+
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);
 +
}
 
</pre>
 
</pre>
  
Line 116: Line 142:
 
public class MailPropertySection extends AbstractBpmn2PropertySection {
 
public class MailPropertySection extends AbstractBpmn2PropertySection {
  
public MailPropertySection() {
+
    public MailPropertySection() {
super();
+
        super();
}
+
    }
  
@Override
+
    @Override
protected AbstractDetailComposite createSectionRoot() {
+
    protected AbstractDetailComposite createSectionRoot() {
// This constructor is used to create the detail composite for use in
+
        // This constructor is used to create the detail composite for use in
// the Property Viewer.
+
        // the Property Viewer.
return new MyTaskDetailComposite(this);
+
        return new MyTaskDetailComposite(this);
}
+
    }
  
@Override
+
    @Override
public AbstractDetailComposite createSectionRoot(Composite parent, int style) {
+
    public AbstractDetailComposite createSectionRoot(Composite parent, int style) {
// This constructor is used to create the detail composite for use in
+
        // This constructor is used to create the detail composite for use in
// the popup Property Dialog.
+
        // the popup Property Dialog.
return new MyTaskDetailComposite(parent, style);
+
        return new MyTaskDetailComposite(parent, style);
}
+
    }
  
/**
+
    /**
* Here we extract the bpmn task element from the current ISelection
+
    * Here we extract the bpmn task element from the current ISelection
*/
+
    */
@Override
+
    @Override
public EObject getBusinessObjectForSelection(ISelection selection) {
+
    public EObject getBusinessObjectForSelection(ISelection selection) {
  
EObject bo = BusinessObjectUtil
+
        EObject bo = BusinessObjectUtil.getBusinessObjectForSelection(selection);
.getBusinessObjectForSelection(selection);
+
        if (bo instanceof BPMNDiagram) {
if (bo instanceof BPMNDiagram) {
+
            if (((BPMNDiagram) bo).getPlane() != null && ((BPMNDiagram) bo).getPlane().getBpmnElement() != null)
if (((BPMNDiagram) bo).getPlane() != null
+
                return ((BPMNDiagram) bo).getPlane().getBpmnElement();
&& ((BPMNDiagram) bo).getPlane().getBpmnElement() != null)
+
        }
return ((BPMNDiagram) bo).getPlane().getBpmnElement();
+
        return bo;
}
+
    }
return bo;
+
}
+
  
public class MyTaskDetailComposite extends AbstractDetailComposite {
+
    public class MyTaskDetailComposite extends AbstractDetailComposite {
  
public MyTaskDetailComposite(AbstractBpmn2PropertySection section) {
+
        public MyTaskDetailComposite(AbstractBpmn2PropertySection section) {
super(section);
+
            super(section);
}
+
        }
  
public MyTaskDetailComposite(Composite parent, int style) {
+
        public MyTaskDetailComposite(Composite parent, int style) {
super(parent, style);
+
            super(parent, style);
}
+
        }
  
public void initialize() {
+
        @Override
super.initialize();
+
        public void createBindings(EObject be) {
}
+
            TaskConfig taskConfig = null;
  
@Override
+
            if (!(be instanceof Task)) {
public void setBusinessObject(EObject be) {
+
                System.out.println("WARNING: this proeprty tab in only working with Tasks Please check por plugin.xml!");
super.setBusinessObject(be);
+
            }
}
+
  
@Override
+
            TargetRuntime rt = getTargetRuntime();
public void createBindings(EObject be) {
+
            // Get the CustomTaskDescriptor for this Task.
TaskConfig taskConfig = null;
+
            CustomTaskDescriptor ctd = rt.getCustomTask(ImixsTaskFeatureContainer.PROCESSENTITY_TASK_ID);
  
if (!(be instanceof Task)) {
+
            Task myTask = (Task) be;
System.out
+
.println("WARNING: this proeprty tab in only working with Tasks Please check por plugin.xml!");
+
  
}
+
            // 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();
  
TargetRuntime rt = getTargetRuntime();
+
                // initalize values
// Get the CustomTaskDescriptor for this Task.
+
                initializeProperty(taskConfig, "txtMailSubject", "");
CustomTaskDescriptor ctd = rt
+
                initializeProperty(taskConfig, "namMailReceiver", "");
.getCustomTask(ImixsTaskFeatureContainer.PROCESSENTITY_TASK_ID);
+
                initializeProperty(taskConfig, "keyMailReceiverFields", "");
 +
                initializeProperty(taskConfig, "namMailReceiverCC", "");
 +
                initializeProperty(taskConfig, "keyMailReceiverFieldsCC", "");
 +
                initializeProperty(taskConfig, "rtfMailBody", "");
  
Task myTask = (Task) be;
+
                // 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");
  
// Fetch all TaskConfig extension objects from the Task
+
                // Add the newly constructed TaskConfig object to the Task's
List<TaskConfig> allTaskConfigs = ModelDecorator
+
                // Extension Values list.
.getAllExtensionAttributeValues(myTask, TaskConfig.class);
+
                // Note that we will delay the actual insertion of the new
if (allTaskConfigs.size() == 0) {
+
                // object until some feature
// There are none, so we need to construct a new TaskConfig
+
                // of the object changes (e.g. the Parameter.name)
// which is required by the Property Sheet UI.
+
                ModelDecorator.addExtensionAttributeValue(myTask, feature, taskConfig, true);
taskConfig = ModelFactory.eINSTANCE.createTaskConfig();
+
  
// initalize values
+
            } else {
initializeProperty(taskConfig, "txtMailSubject", "");
+
                // Else reuse the existing TaskConfig object.
initializeProperty(taskConfig, "namMailReceiver", "");
+
                taskConfig = allTaskConfigs.get(0);
initializeProperty(taskConfig, "keyMailReceiverFields", "");
+
            }
initializeProperty(taskConfig, "namMailReceiverCC", "");
+
initializeProperty(taskConfig, "keyMailReceiverFieldsCC", "");
+
initializeProperty(taskConfig, "rtfMailBody", "");
+
  
// Get the model feature for the "taskConfig" element name.
+
            setTitle("Mail Configuration");
// Again, this must match the <property> element in <customTask>
+
            bindAttribute(this.getAttributesParent(), getProperty(taskConfig, "namMailReceiver"), "value", "To");
EStructuralFeature feature = ctd.getModelDecorator()
+
            bindAttribute(this.getAttributesParent(), getProperty(taskConfig, "txtMailSubject"), "value", "Subject");
.getEStructuralFeature(be, "taskConfig");
+
            bindAttribute(this.getAttributesParent(), getProperty(taskConfig, "rtfMailBody"), "value", "Body");
 +
        }
  
// 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.
+
    * This method verifies if a specific property still exists. If not the
taskConfig = allTaskConfigs.get(0);
+
    * method initializes the value
}
+
    *
 +
    * @param taskConfig
 +
    * @param propertyName
 +
    */
 +
    protected Parameter initializeProperty(TaskConfig taskConfig, String propertyName, String defaultVaue) {
  
setTitle("Mail Configuration");
+
        // test all parameters if we have the propertyName
bindAttribute(this.getAttributesParent(), getProperty(taskConfig,"namMailReceiver"), "value", "To");
+
        EList<Parameter> parameters = taskConfig.getParameters();
bindAttribute(this.getAttributesParent(), getProperty(taskConfig,"txtMailSubject"), "value", "Subject");
+
        for (Parameter param : parameters) {
bindAttribute(this.getAttributesParent(), getProperty(taskConfig,"rtfMailBody"), "value", "Body");
+
            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 verifies if a specific property still exists. If not the
+
    * THis method returns the Parameter object for a specific object. If the
* method initializes the value
+
    * object did not exist the method creates an empty new parameter
*  
+
    *  
* @param taskConfig
+
    * @param taskConfig
* @param propertyName
+
    * @param propertyName
*/
+
    * @return
protected Parameter initializeProperty(TaskConfig taskConfig,
+
    */
String propertyName, String defaultVaue) {
+
    protected Parameter getProperty(TaskConfig taskConfig, String propertyName) {
  
// test all parameters if we have the propertyName
+
        // test all parameters if we have the propertyName
EList<Parameter> parameters = taskConfig.getParameters();
+
        EList<Parameter> parameters = taskConfig.getParameters();
for (Parameter param : parameters) {
+
        for (Parameter param : parameters) {
if (param.getName().equals(propertyName)) {
+
            if (param.getName().equals(propertyName)) {
// param allready exists
+
                // param allready exists
return param;
+
                return param;
}
+
            }
}
+
        }
  
// the property was not found so we initialize it...
+
        // we have not found this param - so we add a new one....
Parameter param = ModelFactory.eINSTANCE.createParameter();
+
        return initializeProperty(taskConfig, propertyName, "");
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, "");
+
}
+
 
}
 
}
 
</pre>
 
</pre>
Line 284: Line 292:
 
[[Image:screen_07.png|screen07]]
 
[[Image:screen_07.png|screen07]]
  
And this is like the bpmn task element will look in the bpmn2 file:
+
The XML for the Task element with these extensions will look like this in the bpmn2 file:
  
 
<pre>
 
<pre>
 
  <bpmn2:task id="Task_2" imixs:type="MyTask" imixs:Imixs="Hello World" imixs:benefit="0" name="Task 2">
 
  <bpmn2:task id="Task_2" imixs:type="MyTask" imixs:Imixs="Hello World" imixs:benefit="0" name="Task 2">
<bpmn2:extensionElements>
+
  <bpmn2:extensionElements>
<imixs:taskConfig>
+
  <imixs:taskConfig>
<imixs:parameter xsi:type="imixs:Parameter" name="txtMailSubject" value="Hello"/>
+
    <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="namMailReceiver" value="ralph.soika@imixs.com"/>
<imixs:parameter xsi:type="imixs:Parameter" name="keyMailReceiverFields" value=""/>
+
    <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="namMailReceiverCC" value=""/>
<imixs:parameter xsi:type="imixs:Parameter" name="keyMailReceiverFieldsCC" value=""/>
+
    <imixs:parameter xsi:type="imixs:Parameter" name="keyMailReceiverFieldsCC" value=""/>
<imixs:parameter xsi:type="imixs:Parameter" name="rtfMailBody" value="Some message...."/>
+
    <imixs:parameter xsi:type="imixs:Parameter" name="rtfMailBody" value="Some message...."/>
</imixs:taskConfig>
+
  </imixs:taskConfig>
</bpmn2:extensionElements>
+
  </bpmn2:extensionElements>
<bpmn2:incoming>SequenceFlow_3</bpmn2:incoming>
+
  ...
<bpmn2:outgoing>SequenceFlow_4</bpmn2:outgoing>
+
 
  </bpmn2:task>
 
  </bpmn2:task>
 
</pre>
 
</pre>
  
==Changing a BPMN extension dynamically==
+
==The mysterious IllegalStateException==
As shown in the XML example before the data structure used in this tutorial is not restricted to a fix set of attributes. Each parameter is defined by the attributes ‘name’ and ‘value’.
+
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:
 
+
<pre><imixs:parameter xsi:type="imixs:Parameter" name="...." value=""/></pre>
+
 
+
This means that you can extend the structure later or dynamically during runtime. But the behavior of the Eclipse EMF model and the Grafiti framework is little bit tricky if you try to change the structure after a EObject was read from your bpmn file. Normally you can add an additional attribute like this:
+
  
 
<pre>
 
<pre>
...
+
@Override
Parameter param = ModelFactory.eINSTANCE.createParameter();
+
public void createBindings(EObject be) {
param.setName(propertyName);
+
  super.createBindings(be);
param.setValue(defaultVaue);
+
  ...
taskConfig.getParameters().add(param);
+
  Parameter param = ModelFactory.eINSTANCE.createParameter();
 +
  param.setName(propertyName);
 +
  param.setValue(defaultVaue);
 +
  taskConfig.getParameters().add(param);
 +
}
 
</pre>
 
</pre>
  
But if you do so during runtime you will get a java.lang.IllegalStateException with the error message: “Cannot modify resource set without a write transaction”
+
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 might be tempted to enclose your model change code in an EMF transactional editing domain like this:
 
+
To avoid this restriction you need to embed your code into a EMF transactional editing domain. This allows you to extend existing structures dynamically. See the following example:
+
  
 
<pre>
 
<pre>
Line 331: Line 336:
 
     public void doExecute() {
 
     public void doExecute() {
 
         // do the changes here
 
         // do the changes here
        bindAttribute(newparam, "value", "....") ;
+
      Parameter param = ModelFactory.eINSTANCE.createParameter();
 +
      param.setName(propertyName);
 +
      param.setValue(defaultVaue);
 +
      taskConfig.getParameters().add(param);
 
     }
 
     }
 
   });
 
   });
Line 338: Line 346:
 
</pre>
 
</pre>
  
If you run into this situation be careful if you add new ExtensionAttributesValue into a new taskConfig object:
+
'''Don't do it!!!''' Model changes should '''only''' be made in response to some user action that causes a visual change in the editor, like moving a graphical figure, or changing the name of a figure. Simply switching between tabs in a Property sheet should not cause model changes. As you know, all model changes are stored on the undo/redo stack. An undo of this particular change, which has no apparent effect on the appearance of the editor, will only confuse and irritate the user.
 
+
<pre>
+
ModelDecorator.addExtensionAttributeValue(task, feature,
+
taskConfig, false); // !! need to be false!!
+
</pre>
+
 
+
The last param of the method addExtensionAttributesValue() need to be set to ‘false’. Otherwise the ExtensionAttributeValue will be added only when the values are changed the first time. This can be too late if you need to manipulate your extensions dynamically.
+
 
+
== Advanced Topics ==
+
The AbstractDetailComposite bindAttribute, bindReference and bindList methods try very hard to determine how best to render an EObject' structural feature by examining the type of the attribute or reference. For atomic data types, this is relatively straight-forward: EString renders a text editing widget, EInt an integer editor and so on. The package ''org.eclipse.bpmn2.modeler.core.merrimac.dialogs'' contains several specialized editing widgets which may be of interest to the developer.
+

Revision as of 12:55, 12 March 2015

Versions

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

Managing custom property tabs

In this tutorial we will go into more detail about how to manage properties of a custom task element and how to implement custom property sections. This tutorial assumes that you have already extended a custom task element as explained in the CustomTask Tutorial.

createBindings

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 BPMN2-Modeler/DeveloperTutorials/Adapters 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 might be tempted to enclose your model change code in an EMF transactional editing domain like this:

@Override
public void createBindings(EObject be) {
   super.createBindings(be);
   TransactionalEditingDomain domain = TransactionUtil.getEditingDomain(taskConfig);
   if (domain != null) {
     domain.getCommandStack().execute(new RecordingCommand(domain) {
     public void doExecute() {
        // do the changes here
       Parameter param = ModelFactory.eINSTANCE.createParameter();
       param.setName(propertyName);
       param.setValue(defaultVaue);
       taskConfig.getParameters().add(param);
     }
   });
  }
 }

Don't do it!!! Model changes should only be made in response to some user action that causes a visual change in the editor, like moving a graphical figure, or changing the name of a figure. Simply switching between tabs in a Property sheet should not cause model changes. As you know, all model changes are stored on the undo/redo stack. An undo of this particular change, which has no apparent effect on the appearance of the editor, will only confuse and irritate the user.

Back to the top