This small guide is for those who want to contribute to the code of the EEF project and the Properties view support in Eclipse Sirius, here are some information regarding the development of those projects: Why things are how they are, what motivated us to do things this way etc. This document will start from the goal of the project to integrate Sirius with Eclipse's Properties view and it will walk through the various layers of the framework from the Properties view to the core EEF components.
- 1 Goal of the architecture
- 2 Fork of the original framework
- 3 Compatibility Layer
- 4 Integration with the Properties view
- 5 Integration in the user interface
- 6 From the EEFTab to the user interface
- 7 A story of two domain specific languages
- 8 Integration of the dialog and wizards
- 9 Default rules
- 10 Lifecycle of the refresh
Goal of the architecture
EEF has been re-created from scratch in order to provide the ability to define a form based user interface with a dedicated domain specific language. This new version uses a different namespace (org.eclipse.eef) than the previous one (org.eclipse.emf.eef) in order to let end users install both at the same time. The main goal of the framework is to give Eclipse Sirius the ability to manipulate the behavior of the Properties view in Eclipse. While the integration in the Properties view is the main objective, it is important that we keep the ability to integrate EEF within any SWT based user interface like an editor or another kind of view. In order to achieve this, we have separated the code EEF components from the Properties view specific ones.
Fork of the original framework
During our work in order to integrate EEF inside of the Tabbed Property Sheet Page framework in Eclipse, we have encountered several major limitations which have required us to fork the framework. One key limitation of the original framework came from the impossibility to extend the final class org.eclipse.ui.views.properties.tabbed.TabContents. It prevented us from manipulating completely the tabs of the page as we desired. It was also impossible for us to support validation rules properly in the original framework since its form based interface is not realized completely with a form. The header of the Tabbed Property Sheet Page is just a rectangle with a gradient from blue to white. We cannot display any message or validation inside and we could not control its title either. The fork of the framework was done very quickly. We made only a minimal amount of changes compared to the original framework in order to fix the limitation found. We also introduced generics since the original code seems to have been written with the support of Java SE 4 in mind. Finally we have removed some usage of arrays in favor of lists. We have also improved performances by reducing the number of time the registry is called during a refresh from 3 to 1. In our situation the description of the user interface requires that we evalute several expressions so it was important to reduce useless calls to compute those descriptions. We have modified the creation of the controls of the tabbed property sheet page in order to create a real form with a message manager in org.eclipse.eef.properties.ui.api.EEFTabbedPropertySheetPage.createControl(Composite). As a result, we now have the ability to add messages and validation in the user interface. We have also moved tons of internal classes into their own dedicated files and renamed all classes to add the prefix EEF. As such, if you want to find to fork of TabbedPropertySheetPage, you should look for EEFTabbedPropertySheetPage. Any change to the fork of the tabbed property sheet page framework should be very carefully done and thought since we do not have the knowledge of the impact of any change. Here be dragons!
In order to support the integration of tabs defined with the previous version of the framework, we have created a compatibility layer in the plugin org.eclipse.eef.properties.ui.legacy. This compatibility layer implements all the necessary APIs of the fork and then calls the old existing APIs. This compatibility layer is very simple but quite difficult to navigate given that everything looks the same on both frameworks. After its creation and stabilization early on, we have not modified this compatibility layer. If you think you have a bug inside of the compatibility layer, you have 99% chance that the bug is somewhere else. We have also made sure that we could leverage existing tabs written with the originalframework by parsing the extension points of the original framework. As such, users should not have to modified anything in order to be able to reuse their old work. We had some issues in the beginning with the compatibility layer since we had to consider the various ways in which the old API ws used. We have fixed most of the issues encountered with the old properties view of Sirius but we found some that we could not fixed directly when using the old version of Eclipse EEF 1.5.x. The previous version of EEF used reflectivity in order to gain acces to some specific private fields for some operations. Since we have changed the name of the framework, this code could not work anymore. Some modification have been made in the code of EEF in order to support both of original tabbed property sheet page framework and our fork. This part of the code of the old EEF is not the prettiest one ever, you don't want to look at it.
Integration with the Properties view
In order to keep a proper architecture, independent from the Properties view, which could be reused in any context we have separated our code in various plugins:
- org.eclipse.eef.properties.ui for the fork of the properties view
- org.eclipse.eef.properties.ui.legacy for the compatibility layer
- org.eclipse.eef.ide.ui.properties for the bridge between the core and ui components of EEF and the Properties view with dedicated IEEFSection, IEEFSectionDescriptor and IEEFTabDescription which will call the remaining parts of the EEF framework.
In the original tabbed property sheet page framework and in its fork, all the tabs and sections have an identifier which is used for the highly critical task of refreshing the user interface. This identifier is supposed to be static since it should have been possible to statically determine the appearance of the user interface. With our expression based approach, this is not possible anymore. As a result, we had to compute an identifier to match the state of the user interface that we will create. We first decided that each page has only one section in which all the widgets will be created. This has no impact whatsoever on the user interface created. In order to compute this identifier in org.eclipse.eef.ide.ui.properties.api.EEFTabDescriptor.getId() we have decided to base it on some part of the description provided by EEF. If an identifier changes, the user interface will be disposed and a new one will be computed from scratch. We decided to use the identifier (the URI) of several elements including the pages and groups to be displayed along with the URI of the variables self for each page and group. In order to ensure that we did not have any issue with a change in the URI of an EObject (for example, if the name of an EClass changes, its URI is impacted), an adapter is used to track EObjects accross changes in a similar fashion as in Eclipse Sirius, see org.eclipse.eef.ide.ui.properties.internal.RefreshIdsHolder.
Integration in the user interface
We have seen how we have integrated the framework into the Eclipse's Properties view with our fork and its compatibility layer but let's have a look at the entry point of the framework. The first class used to integrate EEF into a SWT-based user interface is org.eclipse.eef.ide.ui.api.EEFTab. This class is inspired by the original tabbed property sheet page framework with its major methods:
- createControls to create the SWT controls
- aboutToBeShown to connect the listeners
- refresh to refresh the user interface
- aboutToBeHidden to disconnect the listeners
- dispose the destroy the controls created
In order to be instantiated, the EEFTab will require an EEFPage in its constructor. The EEFPage, along with the EEFGroups are the result of the evaluation of the description of the user interface. You can retrieve the EEFPages from the EEFView created by the org.eclipse.eef.core.api.EEFViewFactory.EEFViewFactory. In order to create the EEFView, you will need to following elements:
- An EEFViewDescription, the description of the user interface using EEF domain specific language.
- An IVariableManager used to hold all the variables, you should initialize it with both a variable with org.eclipse.eef.core.api.EEFExpressionUtils.SELF for its name and an instance of org.eclipse.eef.core.api.InputDescriptor with org.eclipse.eef.core.api.EEFExpressionUtils.INPUT as its name.
- An IInterpreter used to evaluate the expressions of the description. If you want to use aql, you can use the class org.eclipse.sirius.common.interpreter.aql.AQLInterpreter which even have a constructor parameterizable by an IQueryEnvironment giving you the ability to add custom Java services.
- An EditingContextAdapter used to execute the modification of the data. This interface allow us to have implementation supporting the Sirius integration with EMF Transaction while other would just execute the runnable given in performModelChange directly. It is also used to support the appearance and disappearance of the locks in the user interface. When the model is changed, the modification should be transmitted to the model change listeners (implemented by java.util.function.Consumer<List<Notification>>). They are responsible for the refresh of the user interface. This refresh mechanism works best if you have a transactional editing domain since you can execute the runnable modyfing the model in a recording command thanks to the command stack and warn the model change listeners thanks to a resource set listener listening only for post-commit events only. You could thus easily give the notification from the post-commit event to the model change listeners. You can have a look at org.eclipse.sirius.ui.properties.internal.TransactionalEditingDomainContextAdapter for inspiration.
- An IEEFDomainClassTester used to determine if an EObject matches a specific Domain class. An interface has been used in order to let Eclipse Sirius provide its specific behavior.
- An InputDescriptor used to indicate the original selection and the semantic element it represents. For example, in a tree editor, a tree item can be the selected element and it may represent a specific model element. This input descriptor is the one you should put in the variable manager.
Once you have your EEFPage, you can create the EEFTab and create its control to use it in a SWT-based user interface. The methods EEFTab#createControls uses two parameters, the parent composite just like in any SWT control and an implementation of org.eclipse.eef.common.ui.api.IEEFFormContainer. The form container is supposed to be an implementation which can manipulate the Form in which the EEF user interface will be integrated. It should be able to return the form and the sheel used along with the widget factory (which can just be created directly with new EEFWidgetFactory()). It is also responsible for indicating if the user interface is currently being created with the operation IEEFFormContainer#isRenderingInProgress() and finally it is its responsability to refresh the current page which can be done by starting over the whole precedure if necessary. The internal updater of EEF, a model change listener, which reacts to model change notifications from the editing context adapter will trigger this need for refresh. When the controls of the EEFTab have been created, you will have to call EEFTab#setInput(...) in order to initialize its input and then aboutToBeShown and refresh. When you need to deactivate the user interface, you should call aboutToBeHidden and when you are ready to destroy it, you should call the method dispose. Examples of the integration of the EEF runtime outside of a Properties view can be found in the support of Sirius dialogs and wizards
Both implementations still rely on the editing context adapter, interpreters and other concepts specific to Eclipse Sirius.
From the EEFTab to the user interface
The EEFTab will delegate most of its behavior to an EEFSectionLifecycleManager (createControls, aboutToBeShow, refresh, aboutToBeHidden, dispose). The only behavior specific to the EEFTab is the initialization of the input and the configuration of the updater used to detect changes and refresh the user interface. From there a chain of lifecycle manager will be used to handle the operations related to the lifecycle of the user interface (createControls, aboutToBeShown, refresh, aboutToBeHidden, dispose). The EEFSectionLifecycleManager isused to handle the whole page. It manages some parts of the form's message manager and it executes the creation of the group lifecycle managers. The group lifecycle managers are used to create the user interface of each group with SWT Forms sections. It is in charge of the group validation rules and style. Under the group lifecycle managers, the EEFControlSwitch is used to create all the widgets, the containers and evaluate the dynamic mappings contained in a group. Inside an EEFContainerLifecycleManager, the EEFControlSwitch is also executed in order to create recursively the various widgets and containers. All the lifecycle managers are creating a controller where most of the non-user interface related behavior is executed. While controllers are not compulsary in our APIs, they are recommended. Some common superclasses have been introduced to prevent duplication of the code.
- AbstractEEFLifecycleManager: adds support for the validation rules for EEFSectionLifecycleManager, EEFGroupLifecycleManager and the lifecycle managers of the widgets
- AbstractEEFWidgetLifecycleManager: adds support for the layout of the widgets with its label and help. It also handles the appearance and disappearance of the locks on the widgets along with the switch to readonly of the user interface.
In order to validate our approach for the development of custom widgets, documented in the Sirius and EEF documentation, the reference widget has been implemented using the APIs of the custom widgets. As such, we know for sure that our APIs can be used to create real widgets with the same level of quality as the one we have created.
A story of two domain specific languages
In order to reuse the EEF runtime in any SWT based environment using a mostly declarative approach, we built a DSL in order to parameterize it. In the mean time, we also wanted to integrate it properly in the odesign editor for Sirius users. This integration in Sirius would require our DSL to extends the Sirius DSL and it would also be necessary to follow Sirius' lifecycle in order to regenerate the code of our meta-model. On top of that the experience acquired in Sirius show us that having the same language for both the end users and our runtime could produce some issues. We have thus decided to have two languages, properties.ecore in Sirius used to let the end user configure the appearance of the user interface and eef.ecore in EEF interpreted by the runtime. This architecture allowed us to create syntactic sugar on top of the DSL for our end users while keeping the DSL of the runtime quite small. As an example, it has been possible for us to support inheritance between our concept along with the use of the runtime in dialogs and wizards without any impact on the DSL of the runtime. For the first release of the Properties view support in Sirius, both DSLs were almost indistinguishable one from the other. This design decisions comes at a cost since we sometimes have to create changes for both version of the DSL and we have to maintain a transpilation layer between the two DSLs. The main transpilation layer between starts with the org.eclipse.sirius.properties.core.api.ViewDescriptionConverter. This class will take a list of Sirius PageDescription and it will provide an EEFViewDescription. For that it will navigate recursively on the Sirius model and using implementations of org.eclipse.sirius.properties.core.api.IDescriptionConverter, it will create the EEF DSL objects from the Sirius ones. Before the transpilation realized by the converters, a first step is realized by the preprocessors in order to transpile the syntactic sugar of the Sirius DSL into raw concepts of the same Sirius DSL. For example, the inherited values are applied directly to produce a flat version of the odesign without its inheritance links etc. The main entry point for the preprocessing is the class org.eclipse.sirius.properties.core.api.ViewDescriptionPreprocessor. The descriptions of the dialog and wizard are also preprocessed using the following classes:
In order to integrate properly the Properties DSL into the odesign editor, we had to create properties view for our Properties DSL. Those properties view were generated using an old Acceleo 2 based generator (don't ask, you don't want to know). Since those properties view required quite a lot of manual configuration, they are not generated anymore and only manually maintained in the plugin org.eclipse.sirius.editor.properties. While this solution will not scale, the fact that our DSL does not move a lot anymore make this situation manageable.
Integration of the dialog and wizards
In order to integrate wizards and dialogs in Sirius, most of the work has been realized on the Sirius side. The model operations are handled by their model operation manager which execute the following tasks:
Then the DialogTask will create a PropertiesFormDialog while the WizardTask will create a PropertiesWizardDialog with which embeds directly or not the EEFTab (the wizard has first a PropertiesWizard and a PropertiesWizardPage) with its regular behavior.
In order to provide a good user experience from scratch, we have decided to provided a default version of the Properties view automatically. This default version is based only on a specific odesign file using mechanisms available to all odesign descriptions. This odesign is located in the following bundle org.eclipse.sirius.properties.defaultrules.
Lifecycle of the refresh
Once the user interface is created by EEF, the listeners are connected to the user interface by the lifecycle managers in the methods named abouttoBeShown. When the user interacts with the user interface, by click a button for example, a listener is triggered and it will, most of the time, evaluate an expression in a runnable executed by the editing context adapter. The editing context allows developers to integrate the EEF runtime in a context supporting transactions. Once the expression is executed, it may change the model. If it does, notifications will be sent by EMF. Developers integrating the runtime should send back those notifications to the listeners of model changes in the editing context adapter. Once those listeners have been executed, they will trigger a refresh of the user interface. First a new version of the description of the widgets to display will be computed. If its identifier matches the identifier of the currently displayed user interface, it will not trigger a global refresh, only the content displayed by each widgets will be refreshed. If it does not match, the user interface will be disposed and everything will be re-created from scratch.