Skip to main content
Jump to: navigation, search

E4/UI/Toolkit Model/org.eclipse.e4.tm.examples

< E4‎ | UI‎ | Toolkit Model

The org.eclipse.e4.tm.examples plugin

The org.eclipse.e4.tm.examples plugin contains examples of Toolkit Models and how such may be combined with Ecore models and scripting to create applications.

The library example

The library example shows how Toolkit Models may be combined with Ecore models and scripting to create a CRUD application. The examples consists of several parts:

  • lib.ecore - the Ecore model for the library domain
  • lib1.xmi - sample data for the library model
  • lib.js, Library.js, Person.js, UoD.js - javascript code related to lib.ecore
  • lib.tm - the Toolkit Model of the user interface, including scripts for the widget instances

The following sections will describe each of these.

lib.ecore - the Ecore model for the library domain

lib.ecore contains the classes and structural features of the library domain. A library contains books. A book has a title and an author. A library contains loans, and a loans related a person to a book. Persons and authors have a name. The UoD (Universe of Discourse) is a singleton container class that is needed to contain all classes that are not otherwise contained. Since libraries, persons and authors may exist independently, but must be contained somewhere, an UoD object is used as the root object of a resource. Most models will need a UoD a class, unless there is a natural singleton container class.

lib1.xmi - sample data for the library model

lib1.xml contains samle data for the library model and is used for validating both the model and the user interface. If a model looks reasonable, but creating sample data is difficult, there may be something wrong with the model. SImilarly, if the user doesn't understand the relation between the user interface elements and the sample data, the design may be wrong.

lib1.xml may be edited using the standard reflective editor, or the Toolkit Model editor. You will typically use the former for the initial data, while the latter is used when developing the application. But as long as the data conforms to the model, both may be used.

lib.js, Library.js, Person.js, UoD.js - javascript code related to lib.ecore

The various .js-files contain javascript that is related to the model, i.e. lib.ecore. Library.js, Person.js and UoD.js have names (and location) corresponding to classes in the model, and thus define methods that may be called on objects of the corresponding classes. E.g. Library.js defines the getLoansForPerson function, which may be called as a method on Library instances. UoD.js contains methods for creating new Person, Author and Library instances and attaching them to this UoD object. lib.js contains global utility methods that are not natural as methods.

lib.tm - the Toolkit Model of the user interface

The Toolkit Model contains the hierarchy of widgets and scripts to populate it with data and react to user interaction. The widget hierarchy of this example contains a combo where the user may select a library, a list containing the books in the selected library and a property pane with details about the selected book. The user may edit the title of the selected book and may add a new book. The toolkit model and the corresponding concrete UI is shown below.

The toolkit model and concrete UI  of the library example

The expanded tree on the left shows the widget structure. The EClass and name/text of the EObject is shown in the label. The icon is selected based on the object's EClass, but not all subclasses of the main superclasses have their own icons. We can see three levels of composites, a ComboBox named libraryListView, a SingleSelectionList named bookListView, a PushButton named newBookButton and two Label/Text pairs. On the right we see the concrete UI with the corresponding widgets.

The appplication behavior is implemented by scripts attached to the widgets. By selecting a node, you may inspect and edit the script for the node in the Toolkit Model Script Source view. The task of each script is twofold: populate the widget with data and react to user interaction. E.g. the script for the root composite is

function init() {
	var uod = findInstance(getEditingDomain().resourceSet, $library.$UoD); 
	this.dataObject = uod;
}
function onSetDataObject(notification) { 
	var uod = this.dataObject; 
	this.$libraryListView.dataObject = uod.allLibraries;
}

The data comes from the sample data file, in this case lib1.xmi, that is loaded into the same resources set as the Toolkit Model. In fact, it is usually the other way round: You open the sample data with the Toolkit Model editor, and the Toolkit Model corresponding to the sample data's model is it automatically loaded. The initialisation of the editor, calls the init method on each scriptable object. As can be seen from the code above, the root widget gets hold of the root UoD object by means of the editor's editing domain and its resource set. The UoD object is then set as the widget's dataObject, a generic placeholder for data that each widget has. The dataObject property has the same role as the model and input properties used by Swing components and JFace viewers, respectively.

The notification handler of the underlying Javascript support, listens to all changes and automatically calls a corresponding on<Property><EventType> method. Hence, the setting of the dataObject triggers the onSetDataObject method of the same widget. This method gets the value of the UoD object's allLibraries property (uod.allLibraries) and sets the dataObject property of the contained widget named 'libraryListView' (this.$libraryListView.dataObject =).

Most of the widgets in the library reacts to changes to its dataObject, and this is the main method of populating a widget with (new) data. E.g. the libraryListView widget (a ComboBox) has something similar to the following code:

function onSetDataObject(notification) { 
	this.items.clear();
	var libraryList = this.dataObject;
	if (libraryList instanceof java.util.List) {
		this.items.addAll(libraryList.mapProperty('name'));
	}
	this.selectionIndex = 0;
} 

First the items list is cleared. The dataObject is assumed to hold a list of objects having a 'name' property, and the items list is filled with the corresponding values. In this.items.addAll(libraryList.mapProperty('name'));, addAll refers to the standard java.util.List method, while mapProperty is provided as an extra list method by EEList.js.

Changes to properties may be the result of both setting it explicitly, like the case of the dataObject property above, or a result from user interaction, or both. E.g. it is possible to set the selectionIndex property of the ComboBox in a script, as is done in the onSetDataObject method above, and it will be set automatically when the user selects an item. In both cases, the following onSetSelectionIndex method will be called:

function onSetSelectionIndex(notification) {
	var libraryList = this.dataObject;
	var library = libraryList[this.selectionIndex];
	this.$.$booksView.dataObject = library;
}

As a reaction to a change in the selectedIndex, the corresponding library is retrieved and its books are set as the dataObject of the widget named ' booksView' contained in the ComboBox's parent. The booksView widget in turn reacts to the setting of its dataObject property with the following code:

function onSetDataObject(notification) {
	bindingApply(getModelState, updateViewState, this.dataObject, this.controls[0].$bookListView);
}
function getModelState(library, bookListView) {
	return library.books;
}
function updateViewState(library, bookListView, bookList) {
	bookListView.dataObject = bookList;
}

This code is a bit different from the code we've seen so far, as it uses our support for implicit databinding. Databinding is a general mechanism for propagating changes in a data structure, called the model, to dependent data structure(s), called the view(s). In the case of the booksView, it depends on the books contained in the selected library. We already have code that handles changes to which library is selected, but we must also handle the case when the list of books changes, e.g. when a new book is added. In Eclipse's existing databinding support (part of JFace), you must explicitly add bindings between the model and the view.

The EMF Javascript databinding mechanism uses a more implicit approach, where the required bindings are recorded from computations on the model, before updating the view. We can see the separation between reading the relevant model state from updating the view state in the code above, where we have defined one function for each task, getModelState and updateViewState, respectively. The bindingApply function manages the intricacies of databinding as follows: It takes two functions as arguments, as well as extra arguments. It applies the first function argument to the extra arguments, and records all read accesses to Ecore objects during the call. Then it attaches listeners based on the recordings and applies the second function to the extra arguments and the result of calling the first function. If (at a later point) the listeners are notified of changes, the listeners are removed and the process is repeated.

In the case above, we the getModelState function is applied to this.dataObject, which is a library object, and this.controls[0].$bookListView, which is the widget to update. We see that getModelState accesses the 'books' property of the library, and since this is an Ecore feature, the access is recorded and a listener is attached to the library. The updateViewState method is applied to the same arguments as getModelState, with the addition of getModelState's return value, and sets the dataObject of its second argument to its third argument. If a book later is added to this library's list of books, the process will be repeated, i.e. getModelState will be called while recording dependencies and setViewState will be called with the relevant bindings in effect.

If we look at the script attached to the book list widget, we see this mechanism in use again:

function onSetDataObject(notification) {
	bindingApply(getModelState, updateViewState, this);
	this.selectionIndex = 0;
}
function getModelState(bookListView) {
	var bookList = bookListView.dataObject;
	return (bookList instanceof java.util.List ? bookList.mapProperty('title') : new java.util.ArrayList());
}

In this case, the relevant model state is the titles of all the books shown in the list. The titles are collected in a list by means of the mapProperty method and returned, so it may be used by updateViewState (not shown). As was the case above, listeners will be attached to relevant objects, enduring that any changes to the titles of these books will trigger a recomputation of the titles.

Back to the top