Generic Data-types for User-interfaces

I’m playing around making an application for model-based testing.  It’s a standard GUI application, so of course, I am relying on the model-view-controller design pattern for the overall structure.  I am, however, too lazy to set up a big-ol’ object model, so the basic structure of my model is just this:

[java] public class Project extends NotificationObservable {
private final ObservableList<Service> services = new ObservableList<>(new ArrayList<>());
private final ObservableList<InputMapping> inputMappings = new ObservableList<>(new ArrayList<>());
private final ObservableList<Model> models = new ObservableList<>(new ArrayList<>());
private final ObservableList<Test> tests = new ObservableList<>(new ArrayList<>());
private final ObservableList<Environment> environments = new ObservableList<>(new ArrayList<>());

At each level, most things are just lists of other things and a few properties thrown in here and there.

Normally, when using the MVC pattern, I just attach the view to the model and make my controller alter the view.  Java comes with a simple Observer structure, the class/interface-pair Observable and Observer.  Essentially, models extend the Observable class, and views extend the Observer interface.  Easy-peasy.

Unfortunately, the standard collection library of Java comes with no such thing.  JavaFX has an ObservableList and a utility class FXCollections.  JavaFX is a Swing replacement that comes bundled with Java 8, but not in the non-Oracle OpenJDK often used on Linux.

Googling around a bit reveals that there’s a similar function in some Eclipse library, some Groovy library, and Apache Commons Events.  The first has the disadvantage of pulling in a bunch of Eclipse dependencies, the second seems quite outdated (it doesn’t support generics, for example), and the last seems super-abandoned.

Back when I wrote the BRITNeY Suite, I made my own simple implementation of an ObservableList.  Which is somewhere.  It turns out, I ported that over to some ProM extension I did as well.  Last week, I just gave up on the world, and salvaged my old implementation, dusted it off a bit and gave it a fresh coat of make-up, and released it as the Observable Collections library.

The release version 1.0.0 covers basic functionality: observable lists, sets, and maps.  They wrap any collection implementing the corresponding interface, inheriting performance characteristics of the actual collection.  Whenever the collection is altered, all observers are notified.

Now, a view can simply attach itself to a collection of model objects and update itself.  The main window of the MBT Workbench contains views corresponding pretty closely to the model above:

Each component of the view simply listens to the collection in the project corresponding to the panel.  Whenever a controller adds a new object to the services list, the Services view automatically updates.  Neat.

The library also contains a bridge, which can listen on an observable collection and relay the notifications to another observable object.  That way, an object can just listen on the project to get notified whenever any part of the project is changed, regardless of whether its the services, environments or other objects.  The Project constructor contains the following code to facilitate this:

[java] public Project() {
final CollectionNotificationListener observer = new CollectionNotificationListener(this);

That’s pretty much it for the first version of the library.  Since then, my needs have grown and I have added and planned some extensions.

The first extension is proper support for views.  For example, the List interface contains a method subList, which returns a view on the original list comprising a specified sublist of the original elements.  This is a prover view on the object, so altering the sub-list (e.g., adding, replacing or removing and element) also alters the original list.  This means sublists can be constructed very quickly (constant-time regardless of size) and memory-efficient (backed by the original list).  It also means that it is possible to efficiently do pagination by showing a full list, but letting the original full list contain all objects.

Unfortunately, such views are not properly supported by the first version of my library.  This all comes down to the fact that altering the backing collection does not yield notifications to observers.  The simple implementation in version 1.0.0 of my library, just returned a view on the underlying collection for simplicity.  In 1.1.0, we instead return an observable view on the collection, so all changes made on the returned sublist will yield proper notifications to listeners.

Maps and SortedSets (also new in 1.1.0) also support such views on the original collection, and similarly provide proper observable views on them in version 1.1.0.  That means that if you alter, for example, an entrySet or a headSet (my, that’s a dumb name), listeners will be notified appropriately.

Why is this important?  Well, because of another feature of version 1.1.0: custom views.  The library contains custom views that can be attached to any observable collection.  This means you can create a SortedSet view on a regular set, for example.  When elements are added to/removed from either, they are also added to/removed from the other.  There’s also a ListView, which makes any collection look like a list.

This is useful if you want to show, say, a Map in a user-interface.  From the MBT Workbench source:

[java] final JXList mappingsList = new JXList(
new ListNotificationModel<Map.Entry<String, Function>, Map.Entry<String, Function>>(
new ListView<>(
new SortedSetView<>(
new KeyComparator<>())));

Yeah, baby, that’s the good stuff! Here, we make a SortedSetView (l. 4) on the entrySet of a Map contaioning mappings (l. 5) using a generic comparator projecting onto the key component (l. 6). This sorted set is then bridged to a ListView (l. 3) which is wrapped in a generic ListModel, and shown in a JList (really a JXList, because I found an old version of SwingX floating around the internets).

In short, this code makes using entirely generic components a bridge from a HashMap to a JList view, and changes in any intermediate structure is reflected back in the original structure. In addition, the list is kept alphabetized and not random-sorted as you would traditionally get with the HashMap.  This is put to use in the new input mapping of the MBT Workbench:

Here the variable view at the top is a HashSet of variable names, sorted and turned into a list for display using views.  The actual mappings at the bottom maps from XPath expressions pointing into the target document to actual mapping implementations, but are transformed using the code from the snippet above into the JList you see.

The library currently contains a SortedSet and a List view, but that might get extended.

The second and last new feature I’m planning is deep notifications.  Presently, listeners on a collection only get notifications when the list structure itself is changed.  Using deep notifications, listeners on a collection will also get notifications when an element of the list is changed.

In the MBT Workbench example above, the age variable is used in the mapping showed in the Groovy code.  If I decided to change the name of the variable in the upper table, the change would normally not be reflected automatically.  If I had made the variable observable (which it is), and the map it contains deep observable, the code generator would simply have to listen on all the variables and regenerate the code in the mappings map whenever a variable is updated.  Similarly, when an element of the mappings map is changed, a deeply observable map would relay the notification to the view, which would immediately reflect the change.

In effect, deep notifications is the reverse of the CollectionNotificationListener from version 1.0.0 of the library, closing the gap allowing automatic propagation of notifications throughout object trees.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.