Notice: This Wiki is now read only and edits are no longer possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.
EEF/Architecture
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.
Contents
- 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 to provide the ability to define a form based user interface with a dedicated domain specific language and without relying on a code-generation phase. 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. Because Sirius is dynamic and relies on runtime interpretation of configuration models, the new version of EEF is also completely dynamic and, contrary to the older version, does not need any code generation. While the integration of the EEF-defined forms into 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 the tabs of the page in the ways we needed, in particular to support the dynamic nature of EEF definitions (the legacy framework assumed a fixed configuration defined through static extension points). 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.
Because of these limitations, and considering that the legacy framework had not seen any activity for years, we decided to fork it into our own plug-in, org.eclipse.eef.properties.ui
, where we could fix the limitations that blocked us. 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 the original framework everything was static, so these redundant refreshes were not a real problem, but in our situation the description of the user interface requires that we evaluate 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!
Compatibility Layer
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 original framework by parsing the extension points of the original framework. As such, users should not have to modify 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 was used. We have fixed most of the issues encountered with the old properties view of Sirius but we found some that we could not be fixed directly when using the old version of Eclipse EEF 1.5.x. The previous version of EEF used reflection in order to gain access 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 dedicatedIEEFSection
,IEEFSectionDescriptor
andIEEFTabDescription
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 assumed 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 EObject
s across 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 Eclipse's Properties view with our fork and its compatibility layer but now 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
to 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 withorg.eclipse.eef.core.api.EEFExpressionUtils.SELF
for its name and an instance oforg.eclipse.eef.core.api.InputDescriptor
withorg.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 classorg.eclipse.sirius.common.interpreter.aql.AQLInterpreter
which even have a constructor parameterizable by anIQueryEnvironment
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 implementations supporting the Sirius integration with EMF Transaction while other would just execute theRunnable
given inperformModelChange
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 byjava.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 modifying the model in a recording command thanks to the command stack and warn the model change listeners thanks to a resource set listener listening 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 atorg.eclipse.sirius.ui.properties.internal.TransactionalEditingDomainContextAdapter
for inspiration. - An
IEEFDomainClassTester
used to determine if anEObject
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 asEEFExpressionUtils.INPUT
.
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 shell 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 responsibility to refresh the current page which can be done by starting over the whole procedure 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:
-
org.eclipse.sirius.ui.properties.internal.dialog.PropertiesFormDialog
-
org.eclipse.sirius.ui.properties.internal.wizard.PropertiesWizardDialog
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
. 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 managers will be used to handle the operations related to the lifecycle of the user interface: createControls
, aboutToBeShown
, refresh
, aboutToBeHidden
, dispose
. The EEFSectionLifecycleManager
is used 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 mandatory in our APIs, they are recommended. Some common super-classes have been introduced to prevent duplication of the code.
-
AbstractEEFLifecycleManager
: adds support for the validation rules forEEFSectionLifecycleManager
,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 read-only 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 specifiers. 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 specifier configure the appearance of the user interface and eef.ecore
in interpreted by EEF at 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:
-
org.eclipse.sirius.properties.core.api.DialogModelOperationPreprocessor
-
org.eclipse.sirius.properties.core.api.WizardModelOperationPreprocessor
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. Two new model operations have been defined (using a new generic extension point added for this purpose): Open Dialog and Open Wizard. Thee model operations are handled by their own model operation manager which executes the following tasks:
-
org.eclipse.sirius.ui.properties.internal.dialog.DialogTask
-
org.eclipse.sirius.ui.properties.internal.wizard.WizardTask
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.
Default rules
In order to provide a good user experience from scratch, we have decided to provide 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: there is no "cheating" involved, just a very generic VSM which is used instead of a user-supplied one by default. 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.