This document is intended to serve as a Java developer's introduction to the wotonomy user interface framework and how to use it to build a graphical user interface application with Swing.
The wotonomy user interface package contained in net.wotonomy.ui provides a design pattern that further abstracts and decouples the model, view, and controller portions of a graphical user interface application. It lets you do more with less code.
Let's begin by looking at the standard model-view-controller (MVC) architecture, and then we'll look at how the design changes with wotonomy.
A typical Java application can be described in a very high-level way as simply reading a bunch of data objects into memory from the file system or JDBC or elsewhere and rendering a graphical representation of the data for the user. Additionally, the user may be allowed to edit some or all the data represented, and in that case the modifications are copied back from whence they were read.
In MVC parlance, the model is an object-oriented representation of your data and business logic. In the typical application example, the data objects are the model. To afford maximum reuse, these data objects have no knowledge of and no dependencies on the view or controller portions of your application. In the Enterprise Java Bean (EJB) world, entity beans are usually good examples of data objects.
The view is the user interface of your application, where the data objects are represented on screen using labels, text fields, sliders, and the like. While you might need to know a little bit about the data you are trying to visualize when constructing the user interface, the view classes have no code dependencies on the data object classes in the model. And the view similarly has no code dependencies on the controller. To afford maximum flexibility, the view consists only of the Swing components arranged in the proper layouts and nothing more.
The controller, on the other hand, contains the logic specific to the Java Swing application you are writing, and it is tightly coupled with and has deep code dependencies on the model and view.
The controller is responsible for fetching the data objects that will be viewed or edited, and is responsible for populating each user interface component in the view with data from the data objects. This means that the controller directly calls methods on both the model's data objects and the view's user interface components.
If the application supports editing, the controller is responsible for even more. It must detect changes to the view, like edited text fields or moved sliders, and update the model to reflect the changes. And it must detect changes in the model and update the view components accordingly (especially for side-effect changes like a task's status changing in the data model when the user assigns a worker). These responsibilities deepen the couplings with both the model and the view.
Furthermore, the controller must track commits and reverts of sets of changes to the data model, and it must track lists of edited, added, and deleted objects to efficiently update the persistent data store. And these responsibilities deepen the complexity of the logic contained in the controller.
Because so much code resides in the controller portion of an application, and because the controller is the least generic and therefore least reusable code in an application, wotonomy is designed to abstract and simplify the responsibilities of the controller to reduce the cost of writing and maintaining it.
With wotonomy, the only requirement on the model portion of your application is that the data objects adhere to Java Bean naming conventions for properties. Wotonomy further decouples your model from the rest of your application by working with it only through the use of bean properties.
The view portion of your application remains the same. The controller portion, however, is vastly simplified.
The fetched data objects are placed into an EditingContext. The editing context monitors the objects for changes, and tracks insertions and deletions, and handles commits and reverts. The controller is only responsible for providing the glue to allow the editing context to talk to whatever persistent object storage is used for the application. If your data is read only or if your data model is fairly simple, then the editing context can be omitted and your job is even easier.
A DisplayGroup then retrieves some or all of those objects from the EditingContext. A display group maintains an ordered collection of objects, monitoring them for changes. The display group additionally tracks the concept of selection: a subset of objects in the display group are selected, and the selection might contain zero, one, many, or all objects in the group. The display group broadcasts notifications of changes to the data object or to the selection to interested listeners. The controller is only responsible for creating the display group and deciding what objects from the editing context should be placed in the group.
An Association is then created for each user interface component in the view. Each association is bound to the display group as a listener for change notifications. Different types of associations correspond to different kinds of user interface objects: a JTextField with a TextAssociation will display the value of a property for the selected object in the bound display group, while a TableColumn with a TableColumnAssociation will display the value of a property for all objects in the bound display group, highlighting those objects that are selected. The controller is only responsible for creating the associations, connecting them to the user interface components, and binding them to the display group.
Your controller will not need to directly touch any data objects, and will only need to access the view to get references to the user interface components and pass them on to the associations. After all the pieces are connected, they'll just work and it's pretty much fire and forget.
On the whole, an application may have one or more editing contexts, each of which may be used by one or more display groups, each of which is observed by one or more associations, each of which is connected to a single user interface component. But there's a little more to this story.
As the diagram shows, a display group actually has a data source which has an editing context, and editing contexts have object stores which can in turn be editing contexts. This means that editing contexts can be nested within one another. And there are associations that can control a display group instead of a Swing component, which means that display groups can also be nested.
However, you can write a fully-functional application using just display groups and associations, skipping the editing contexts and data sources completely.
Wotonomy makes the simple things easy while still making complex things possible.
Wotonomy is a design pattern as well as a Java library, so you'll want to understand how to structure your application to best leverage the framework.
In short, any objects that have getter and setter methods can be used in a display group. Your objects probably have those already. Even better are objects that use Java Bean naming conventions to define properties. You probably do that too. For change notifications, extending java.util.Observable is helpful, but since your objects probably don't already do that, there is an alternate way to post change notifications. Or you can choose to not post notifications at all. The requirements of wotonomy on your data model are minimal.
A java bean property is defined by having a method of the form getProperty() where Property is the name of the property, for example, a name property is defined by the method getName(). The get method can have no parameters and the return type defines the type of the property. (The alternate form for getters that omits the get in the method name, e.g. property(), is also supported.)
An editable property is defined by additionally having a method of the form setProperty( Object aProperty ) where the return value is void and the type of the parameter matches the type of the getter method. (When a property has a getter method but no setter method, that property is considered-read only.)
An indexed property works the same way but the type is either an Array (like Object[] or String[]) or a Collection (like Set or List). Indexed properties typically define one-to-many relationships in your data model. Indexed properties are useful for nested display groups and recursive tree associations.
The Java way for objects to post change notifications is to extend java.util.Observable and call notifyObservers() after your change has taken place. If your data object extends Observable, DisplayGroups will register themselves as observers and expect notifications for changes that occur to the object, whether from user interaction or from external influence.
The Observable approach may be undesirable if you need to subclass a third party object that does not extend Observable or if you don't want a strong reference to an observer from your data object that may need to be manually cleared later.
For objects that do not extend Observer, you can get much the same effect by calling the static method EOObserverCenter.notifyObserversObjectWillChange() before your change takes place.
Wotonomy uses EOObserverCenter exclusively within the framework and it presents a preferrable alternative to Observable because it uses weak references to track objects and it relaxes the requirement that observed objects extend a particular class by requiring that observing objects implement the EOObserving interface. The EOObserverCenter can also coalesce change events if the observing object extends EODelayedObserver.
Please note that if your object does not post change notifications, most of wotonomy will continue to work. The only problem will then be that changes external to wotonomy - changes made to your data model by your data model or by a timed refresh or something similar - will not be automatically reflected in the user interface. Manually calling EODisplayGroup.updateDisplayedObjects() can refresh the display.
Display groups are objects that manage an ordered list of data objects. Of this list, a subset are considered to be "displayed" objects that are visible to the user. Of the displayed objects, a subset are considered "selected". In common usage, all of the objects are displayed, but only one object and sometimes no objects are selected.
Display groups monitor changes to the list, changes to what objects are in the list, changes to which objects are selected, as well as changes to the properties of the objects themselves, and notify any bound associations of these changes.
If the display group has a delegate, the delegate is informed of these changes and more so that the delegate can fine-tune the behavior of the display group.
A display group is created very simply with the default constructor. This creates a display group with no data source, no delegate, and no objects to manage.
EODisplayGroup displayGroup = new EODisplayGroup();
You can populate the display group with objects in two ways.
The direct way is easiest. Simply pass in a List containing the objects to be managed in the order they should appear.
List objectList = new LinkedList(); objectList.add( new TestObject() ); displayGroup.setObjectArray( objectList );
Alternately, you can specify a data source for the display group to use to retrieve objects.
EODataSource is an abstract class that you will extend with methods to create, insert, and delete objects from whatever persistent object store you are using. This class is particularly useful for rendering recursive tree-like object relationships that the data objects do not explicitly model.
If you will be using an editing context, you will need to use a data source.
EODataSource dataSource = new MyDataSource(); displayGroup.setDataSource( dataSource ); displayGroup.fetch();
The displayed objects are a subset of the list of objects that the display group manages. By default all objects are displayed.
You can change this behavior by specifying a qualifier for the display group. This is an instance of EOQualifier and it will have evaluateWithObject called for every object in the display group. Only those objects for which the return value is true will be in the displayed objects list. Call updateDisplayedObjects() on the display group to apply the qualifier.
You would typically use this feature when you want to preload a large number of data objects into the display group so that the user can quickly and repeatedly apply different filters to sift through the data.
// show only Powers in San Antonio Map matchValues = new HashMap(); matchValues.put( "lastName", "Powers" ); matchValues.put( "city", "San Antonio" ); EOQualifier qualifier = EOQualifier.qualifierToMatchAllValues( matchValues ); displayGroup.setQualifier( qualifier ); displayGroup.updateDisplayedObjects();
Display groups also track which of the displayed objects are selected. By default no objects are selected, but you can change the selection programmatically. Certain associations allow the user to change the selection as well.
If you do not provide the user with an association that can change the selection, you should make sure to set the selection to some object.
// select all objects displayGroup.setSelectedObjects( displayGroup.displayedObjects() ); // select the third object List selectionList = new LinkedList(); selectionList.add( new Integer( 2 ) ); displayGroup.setSelectedIndexes( selectionList ); // select the next object displayGroup.selectNext(); // clear the selection displayGroup.clearSelection();
You can programmatically insert or delete objects from a display group, although only if they are displayed. Any associations that display all of the displayed objects (like TableColumns) will be updated automatically.
Deleted objects that were selected are removed from the selection. Inserted objects are not selected, so you will need to select them manually if desired. Default values for inserted objects can be specified with setInsertedObjectDefaultValues().
If a data source is used, the data source will be informed of the insertion or deletion, which will notify the editing context if it has one, which would update the persistent store on commit.
Also, if a data source is used, the display group can be asked to simply create an object to be inserted into the group, which will call createObject() on the data source.
// insert an object displayGroup.insertObjectAtIndex( new TestObject(), 0 ); // delete that object displayGroup.deleteObjectAtIndex(); // select the next object displayGroup.selectNext(); // delete that object displayGroup.deleteSelection(); // insert a new object displayGroup.insertNewObjectAtIndex( 0 );
Display groups check with their delegate before performing almost any operation, you can easily customize the behavior of a display group by providing a class that implements the DisplayGroup.Delegate interface.
Display groups ask their delegate for permission to fetch, insert, delete, edit, create, and change selection, and then notify their delegate after each of these operations has taken place. Wotonomy provides a DelegateAdapter class that you can easily subclass to implement the required interface.
Associations are the bridge between the controller and the view. An association has exactly one "controlled object", which is usually a user interface component. They also have one or sometimes more "aspects", each of which has a name, like ValueAspect or EnabledAspect. You bind an aspect of an association with a display group and/or a key. Then you establish the connection between the association and the controlled object and you're done and you can forget about it. The association will keep the user interface object synchronized with the objects in the display group.
You create an association by calling the constructor of the subclass of EOAssociation that is appropriate for the controlled object. Most associations are applicable to many kinds of objects, so the constructor is expected to take an Object. (This means that if you specify the wrong kind of object for the association, you won't hear about it until runtime.)
EOAssociation association = new TextAssociation( textField );
Associations fall into one of two categories: overview associations and detail associations. Almost all associations are used to control some kind of user interface component.
Overview associations show some property of all the items in the display group. Overview associations usually allow the user to select and deselect objects in the display group.
For example, a JList might be used with a ListAssociation to show the "name" property of all of the displayed objects in the display group, in the same order. Clicking the fourth name in the list would cause the fourth object in the display group to be selected, and - depending on the JList's selection model - cause whatever was previously selected to become unselected.
Detail associations show some property of only the first selected object in the display group. If there is no selection, the detail association causes the controlled component to be blank or otherwise empty. If multiple objects are selected, the detail association shows the property of only the first object in the selected objects list.
In our example, a JTextField might be used with a TextAssociation to display the "email" property of the selected item in the display group. Clicking the fourth name in the list causes the fourth item in the display group to be selected, which populates the text field with the value of the email property for that object. Changing the value in the text field changes the value of that object's email property automatically.
While nearly all associations control user interface components, the MasterDetailAssociation is worthy of note because it does not.
A MasterDetailAssociation controls a display group and binds to an indexed property of the objects in another display group. It is a detail association, with the bound display group as the "master" and the controlled display group as the "detail" component.
Again using our example, another JList might be used with a ListAssociation to show all the dependents of the person selected in the first JList.
First we would create a new display group and then create a MasterDetailAssociation with our new display group as the parameter to the constructor. Then we would bind that association to our original master display group with the "dependents" property (assuming there is a method on our data object called getDependents() that returns an array or a collection of objects). Then we would create our new ListAssociation controlling our new JList, and bind that to our new display group with the "name" property. Seeing the source might make this clearer - that's coming in the next section.
Clicking on a name in the name list would then display the email address in the text field and a list of names of all the dependents.
An association has one or more aspects. Most of the time, you'll just need one aspect, sometimes called the "primary aspect". Aspects are referred to by name, like ValueAspect, and the names of an association's aspects are static members of that association's class.
The primary aspect is usually responsible for populating the controlled object with data from the display group. The other aspects are useful, but they'll do different things depending on the nature of the association, so you should consult the documentation for the association you will be using.
To bind an aspect, you call bindAspect() with the name of the aspect, a display group and a string "key".
Usually, all three are specified, and the key is a property name. The association will display or edit the value of this property for the objects in the display group.
The key may be an empty string, and this is considered the "identity property", and the value of the actual object in the display group will be used instead of a property on that object. This is sometimes useful for view components like JTrees for which you want to write custom renderers: with the identity property, the renderer receives the value of the actual object.
The display group may be left null, in which case the key is treated as a value, acting as if all objects in the display group for the given property returned that value. This is useful for certain aspects of certain associations, like the EnabledAspect of TextAssociation, or the EditableAspect of TableColumnAssociation.
After all the aspects have been bound, you need to call establishConnection() to put the association to work. Importantly, you do not need to retain a reference to the associations you create - they will exist as long as their controlled components exist, and when the controlled component is garbage collected, the association will be garbage collected.
// set up the master display group EODisplayGroup masterGroup = new EODisplayGroup(); masterGroup.setObjectArray( getMyUsers() ); EOAssociation association; // set up names list association = new ListAssociation( usersList ); association.bindAspect( EOAssociation.TitlesAspect, masterGroup, "name" ); association.establishConnection(); // set up email field and for fun make it non-editable association = new TextAssociation( emailField ); association.bindAspect( EOAssociation.ValueAspect, masterGroup, "email" ); association.bindAspect( EOAssociation.EditableAspect, null, "false" ); association.establishConnection(); // set up detail display group EODisplayGroup detailGroup = new EODisplayGroup(); association = new MasterDetailAssociation( detailGroup ); association.bindAspect( EOAssociation.ParentAspect, masterGroup, "dependents" ); association.establishConnection(); // set up dependents' names list association = new ListAssociation( dependentsList ); association.bindAspect( EOAssociation.TitlesAspect, detailGroup, "name" ); association.establishConnection(); // we're done!
Editing contexts allow multiple display groups to manipulate the same set of objects, such that objects edited in one display group are updated in the associations of all other display groups that contain that object.
Editing contexts monitor all inserts, edits, and deletes in the display groups that use them, allowing these sets of changes to be committed or rolled-back as a single transaction.
Furthermore, editing contexts can be nested, so that a new editing context will reflect the existing state of another editing context, but all changes made to the new context are not applied to the parent context until they have been committed in the child context.
Two concepts are critical to an understanding of editing contexts: (1) An editing context must have an object store, and (2) an editing context is an object store.
Because you need an object store to instantiate an editing context, there must be some implementation of EOObjectStore that is not an editing context. That object store will be responsible for directly reading and writing to the persistent storage.
Because editing contexts do implement object stores, you can then create other editing contexts that use the first editing context, which uses the first object store. This is what allows editing contexts to be nest.
To use an editing context in your application, you must create an object store. Your implementation of EOObjectStore will act as the glue between wotonomy and whatever persistent storage mechanism your application uses.
You will then create your primary editing context, specifying your object store as the parameter to the constructor. You will probably have only one object store per application, and one primary editing context that uses it. Any other editing contexts you create will use your primary context as their object store.
// initial application setup EOObjectStore objectStore = new MyObjectStore(); EOEditingContext mainContext = new EOEditingContext( objectStore ); // set up a child display group EOEditingContext childContext = new EOEditingContext( mainContext ); EODataSource dataSource = new MyDataSource( childContext ); EODisplayGroup displayGroup = new EODisplayGroup(); displayGroup.setDataSource( dataSource ); displayGroup.fetch();
This document gives you a very basic overview and brief introduction to the major players in the wotonomy user interface framework. You'll probably want to check out the API Reference next