E4/UI/Toolkit Model/org.eclipse.e4.emf.javascript

From Eclipsepedia

< E4‎ | UI‎ | Toolkit Model
Jump to: navigation, search


EMF Javascript support

The EMF Javascript support is based on Mozilla'a Javascript implementation (Rhino) and lets you use EMF objects in Javascript code. Besides Javascript's existing support for using EMF objects as native Java objects, there is EMF-specific support for

  • using Ecore attributes (EAttribute) and references (EReference) as Javascript properties, with the . and [] notations,
  • implementing Ecore operations (EOperation) with Javascript code
  • adding Javascript functions as methods to Ecore classes (EClass) and individual objects (EObject)
  • listening to change notifications and calling event-handling methods
  • databinding, by means of dependency recording

Each of these is covered in the sections below. But, first we give a small introduction to the Javascript object model.

The Javascript object model

In most object-oriented languages, the behavior (attributes and methods) of an object is determined by the class, from which it is instantiated. Both attributes and methods are defined in the class, and the instances created from a class will have a value for each defined attribute and the methods you may call are those defined in the class. If inheritance is supported, a class can be seen as the composition of its own attributes and those defined in its superclasses.

In Javascript, there is no classes and class-based instantiation. Instead you create empty objects and dynamically add properties to them. Methods are just properties with function values. While properties usually have simple Java-like names, they are in general String or integer keys. The dot notation is used for simple names, e.g. obj.simpleName, while brackets ([]) is used for the general case, e.g. obj[1] or obj['strange name'].

To create two objects with similar behavior, you simply add the same properties and methods to them. Alternatively, a mechanism for inheriting behavior from other objects is provided, by means of an object's prototype. If you try to access the value of a property that has not (yet) been added to an object, the property of the object's prototype is used instead. Hence, if two empty objects share the same prototype, they will effectively have the same default property values and methods. Note that since the prototype is a normal object with its own prototype, there really is a prototype chain providing default property values and methods. By carefully constructing partly shared prototype chains, you can get an effect similar to Java's single inheritance class hierarchy. This is what we do in our Javascript support for EObjects. The general prototype chain mechanism is, however, a lot more flexible than Java's inheritance, since the prototype chain and each prototype objects may be modified at any point. It can also be very confusing, and messing with the prototype structure requires great care.

Note that properties that are not handled by the EMF-specific mechanisms described below, default the Rhino's default behavior for Java objects.

Using Ecore attributes and references as Javascript properties

When EObjects are used as Javascript objects, the attributes (EAttribute) and references (EReference) may be referred to as properties, using the dot notation. This means that the Javascript code used for accessing them, may be similar to its Java counterpart. E.g. to access the name attribute of obj, you write obj.name. However, you may also use the bracket notation, as in obj['name']. Since features in Ecore must have Java-like names, the bracket notation is typically used for arrays or when you compute the the property name as in obj[useFullName ? 'fullName' : 'name'].

A second kind of Javascript property is supported as a convenience. It is fairly common for an EObject to contain other EObjects, where the contained EObjects have a 'name' attribute that is unique among siblings. E.g. Ecore EPackages contain EClasses, EClasses contain EStructuralFeatures and EOperations contain EParameters. In such a case, a contained EObject may be retrieved using the name prefixed by '$' as the property. E.g. anObj.$aName will retrieve the EObject named 'aName' contained in anObj. This makes it easier to navigate in some EObject structures, particularly the Ecore meta-object structure. Note that this is limited to getting the object as a property, not setting it.

Implementing Ecore operations (EOperation) with Javascript code

In Ecore, methods are modelled using operations (EOperation). Since Javascript methods are properties with function values, operations are returned as such. This only has meaning for implemented operations, and this is supported by annotating the EOperation with Javascript code that defines the corresponding function. We use Ecore's existing EAnnotation mechanism, so this may be done using the default Ecore editor support.

E.g. suppose you have a class with a name attribute and an operation named 'getSuffixedName'. The method takes a parameter named 'suffix', which should return the 'name' attribute with the suffix appended. You first add an annotation with the source (URI) set to http://www.eclipse.org/e4/emf/ecore/javascript/operationBody. On this annotation you define a detail entry with key as 'js' and value as return this.name + suffix;. this.name will retrieve the name attribute and suffix will refer to the parameter name. When the method is called, the 'js' entry of the annotation will be evaluated as (the body of) a Javascript function definition, returned as a function value and applied to the argument.

Adding Javascript functions as methods to Ecore classes (EClass) and individual objects (EObject)

While the annotation mechanism allows implementing Ecore operations, it is not very convenient, since you must edit them individually using the property sheet and not as a whole in a code editor. Therefore we support implementing all the operations of Ecore classes as Javascript functions in separate .js files, one for each EClass. The .js file for an EClass is located by construction a URI from the EClass' and using the URIConverter of the ResourceSet that loaded the EClass, so the location is configurable. Typically, it will be placed in the folder and have the same name as the EClass with a .js file extension.

The .js file for an EClass will define the set of Javascript functions that will be callable as methods of EObjects of that EClass. Hence, the this keyword will refer to instances of that EClass inside the body of each function. The same technique is used for defining methods on existing classes, like Ecore's meta-classes. In the folder /src/org/eclipse/e4/emf/ecore/javascriptEClass.js in the org.eclipse.e4.emf.javascript plugin there are .js files for EClass, EDataType, EEList and EObject that define methods for (instances of) these classes. E.g. in EClass.js the create methods is defined as follows:

function create() {
	var factory = this.ePackage.eFactoryInstance;
	var eObject = factory.create(this);
	return eObject;

So to create an instance of a class named 'C' in package 'p', you can write $p.$C.create();.

Listening to change notifications and calling event-handling methods

In many applications, a lot of behavior is implemented be means of change notifications and handlers (called adapters in Ecore). EObjects have built-in support for notifications and we provide support for handling change notifications with Javascript code. There are two ways of attaching handlers:

  1. You explicitly attach a specific handler to a specific notifier by means of the adaptTo(notifier, handler) method. This is an EMF-specific Javascript method that may be called in Javascript code.
  2. You attach a single handler to an EObject hierarchy that makes the EObject handle its own notifications. This is achieved by means of JavascriptSupport.supportNotifications method, which must be called in Java code, typically after loading a resource.

In the first case, a specific handler is used for handling notifications on some other source object, while in the second case the object will handle notifications with itself as the source. But which method will be called to handle the notification? In both cases, the name of the method to be called is constructed from the pattern 'on'<eventType><featureName>, where eventType is one of 'Set', 'Unset', 'Add', 'Remove', 'AddMany', 'RemoveMany', and 'Move', and featureName is the name of the changed feature. E.g. if the notification resulted from setting an attribute named 'date', the 'onSetDate' method will be called to handle the notification. If the method doesn't exist, it tries again with eventType set to 'Change', e.g. 'onChangeDate'.

Databinding, by means of dependency recording

To support Databinding, there is support for recording all read accesses to EMF properties during the execution of Javascript code and reacting to changes to those properties. Suppose you 1) computed some values based on the state of some EObjects and 2) change the state of some other EObjects. You would like to redo step 2) when the values from step 1) changes. This may be done by recording the dependencies during step 1), attaching listeners to the appropriate objects and redo both steps (while re-recording dependencies) when the appropriate change notifications are received.