Abstract
Today we can see strong progress in client side Java, both in performance and deployment area. However, although we have plenty of user interface toolkits (like Swing or SWT), miscellaneous beautifully looking component suites and many advanced development environments, developers still face dificulties of client-side application development. Long awaited Java 1.5.0 unfortunatelly does not simplify Swing programmimg. Ease of use in GUI development still remains to be adressed.
This paper analyses reasons, why programming clients in Java is not very popular. In the second part introduces MVC Mediator, lightweight easy to use binding and validating framework, which fills the gap between low level UI components and application domain models. The framework solves real problems and annoyances present in typical Java GUI programming and encourages to rapid prototyping and development of applications with high levels of usability as well as enforces good design and programming practices.
This section discusses why it is not easy to to create perfect client side applications. We will see this problem from three perspectives: user, system architect and developer.
There is widespread belief that users take care about look and feel of user interface. There is too much focus and emotions in Java community about this. However, except few colleagues programmers I never met user who would have some principal objections against default Swing look and feel. You may not agree, but it is really minor issue. The real problem is not on component level, but on application level: application look, feel and usability.
Users exploit our applications to run their business and much more then excelent look and feel they expect good application usability:
Logical, intuitive, nondistracting user interface with functional and reasonably aesthetic layout. Such interface does not contain unnecessary and useless items, supports user productivity, remembers frequently accessed objects (directories, files, etc.)
Safety - makes difficult to corrupt or loose data. User is human beeing and makes accidental mistakes. Changes to data should be buffered and aplied after explicit commit. User should have posibility to undo made changes.
Data entry validation with nonintrusive error and warning feedback. It prevents users from commiting wrong or supiscious data, while leaving enough freedom and comfort to prepare data for commit.
Application look and feeel consistency - number and date formats, keyboard shortcuts, used icons, fonts, button order, default actions and so one
In-depth discussion of above features is behind scope of this paper. Good tips about usability can be found in [ 1].
Any serious maintenable application has separated user interface from application model and logic. There are many patterns devoted to help solve this problem, like most popular MVC (Model View Controller) pattern family (HMVC (Hierarchical MVC), MVP (Model View Presenter) or Model-UI).
Classic MVC pattern schema is shown on Figure 1. The model contains data and commands. The data are displayed by the view, which register itself as subscriber to the model events, which model publish when its state changes and the view needs be updated. Controller updates state of the model depending on user interaction with view (it subscribes to events published by the view).
Well it is nice in theory and in Smalltalk, but does not work in Java. After so many years we still have no free, lightweight, easy to use and widely accepted MVC type toolbox for rapid development of high quality Java client applications. There are few implementations (like Scope framework [4]) but they did not reached wider audience.
Important note presents Steven J. Metsker in [2]: "The creators of MVC envisioned that the look of a component-its 'view'-might be separable from its feel-its 'controller.' In practice, the appearance of a GUI component and its support for user interaction are tightly coupled, and Swing does not divide views from controllers. The value of MVC is to push the 'model' out of an application into its own domain."
Swing creators slightly modified MVC pattern, joining controller with view. This pattern, called Model-UI works well in Java also on application level to create windows, forms, etc. The pattern is ilustrated on Figure 2.
Controller is now joined with user interface code and is responsible for updating model and UI controls. It acts as Mediator in the design pattern with the same name (see [2]).
Detailed description of Swing MVC architecture can be found in articles [7] and [8].
There is useful to recognize two kinds of models:
Domain model, which contains business logic and/or data
Application model, which contains user interaction logic implementation
User interaction is business problem like any other, with its own requirements (application workflow, validation, undo management, interaction buffering, persistent configuration, guidance with wizards, common application services etc.) Good application interaction definitely requires its model - an application model. Underestimated application models tend to totally desintegrated applications with bad usability.
An application model holds data reflecting an application state and set of commands available to the user. But why remember state if components like checkbox or textfield holds their state by themselves? Isn't this breaking of basic rule, that everything in a system should be stored only in one place? Definitely no!
Think of application model as data about application, active modules, editors, panels etc. For example it can contain list of opened windows, their coordinates, last used files and so on. Application model should be serializable (preferably to some text format like XML). When user closes an application the saved model allows recreate application state when she opens application next time. All this things are important and system architect must choose reasonable level of detail - the more complex application, the more advanced will be an application model.
Domain model can be totally separated from application (like J2EE server side EJB components, SQL data and stored procedures, a server game engine or UNIX command line find program) or it can be set of Java objects which run directly in client program (some data structures and algorithms).
For both kind of models application designer must solve following issues:
How to receive events from models?
How to reflect changes in models into current user interface?
How to update models?
How to invoke model commands?
When and how to validate data?
In case of distributed application, what protocols use - JMS, CORBA, RMI, JDBC, SOAP,... ?
Although the Model-UI pattern works well, there is one drawback: yellow box on Figure 2 in the Section called System architect point of view is assumed not only to synchronize user interface state with model, which itself is not easy, but also provide validation, buffering, undo and other stuff contributing to application usability. It is ideal candidate to become "spaghetti code" and best place for bugs.
Author perceives this as main reason why writing client programs in Java is difficult and unpopular. Modern development environments helps You to layout forms and then pollute code with event listeners, calling this often "very clean code". Java Community used to criticize Visual Basic for its insane approach to application design and development, but in fact after so many years Java IDEs show off the same weaknesses.
Java contains wonderfully designed Swing toolkit with many available beatiful look and feels and sofisticated layout managers. Unfortunately Java does not contain any higher level support in language or library for rapid and reliable GUI application programming. The problem is ilustrated on Figure 3.
Nothing. Well, almost. But this is not probably answer You expect from this paper, right?
List model, combo box model, spinner model, selection model, table model - sounds familiar? With Swing GUI toolkit, You cannot have neutral relationship to it: You either love it or hate it. The programmer must create component, set desired properties, write and manage appropriate event listeners and often component specific adapters providing data from domain/application models. If we take programmer as a user of the Swing library, we must conclude, that Swing from his point of view has poor usability.
Swing provides extremely flexible abstraction of common user inteface toolkit controls. Like POSIX or WindowsNT APIs provide abstraction of computer hardware the Swing abstractions provides foundation for higher level layers. The problem and drawback is, that JDK does not contain any such layer on top of the Swing.
Smalltalk programmers enjoy legendary productivity comparing to their Java colleagues not only because of language itself, but also thanks to higher level libraries supporting application programming. See Cincom Smalltalk (former VisualWorks) documentation [6] or play with Smalltalk/X [5].
What are implications of low-level Swing usage, as one can see in miscellaneous tutorials and articles on the net?
Proliferation of inner listener classes. Flawed listener management, often leading to memory leaks (yes, also in Java!)
Proliferation of combo box, table and list model inner classes. Together with other inner classes they increase total application memory consumption.
Source code full of synchronizing user interface with application/domain model ("back and forth" formatting, parsing and value copying).
Absence of application level concepts like value binding, validation, buffering, undo.
Not very maintenable source code
Frustrated programmer
Possible performance/memory issues
To make things even worse, Swing toolkit not always behaves as one would expect. Programmer often has to implement miscellaneous workarounds which further decrease code readability.
To illustrate the problem, below are few screenshots of very trivial
application which implements typical use case: user enter some data and
presses OK or Enter
to close the dialog with good faith that data which she sees are data
that really go to the application logic. She expects behavior shown
on Figure 4. The first text field is
instance of JFormattedTextField, the second is instance
of JTextField managed by Mediator
framework (the example is part of MVC Mediator Demo application which is
freely available including source code on the web - see
[3]).
Lets assume that the field in the application is optional and user decides
to clear the value she entered. She expects null
will be commited. But here is surprise:
commited will be old value as shown on
Figure 5! How it is possible? Class
JFormattedTextField uses JDK
NumberFormat to parse numbers, but this parsing
class considers empty string to be illegal and throws an exception (this
"feature" is one of annoyances of JDK
libraries).
Unfortunatelly the text field story does not end here. User enters some really
big number, which does not fit to Integer boundaries.
She can make a typo and add one more zero or just paste some big number
from other application. Application programmer would consider this as an
error, but JFormattedTextField silently lets
overflow the value and commits complete nonsense. The bug is shown
on Figure 6. Correct solution would be
for example to disable show command (as can be seen
with text field managed by Mediator).
Chellenges mentioned in the first part of this paper requires reusable and robust and solution. In this section we present detailed overview of MVC Mediator with code examples. You will see, how easy and with few lines of code You can get required application features.
For illustration purposes lets consider hypothetical application which manages computer user accounts, as shown on Figure 7. The example is part of MVC Mediator Demo application which is freely available including source code on the web - see [3].
Main classes of the application are shown on following listings:Example 1. Listing of UserInfo.java
import java.beans.PropertyChangeSupport;
/**
* Java Bean class holding basic information about user computer
* account. It holds four values and one commad.
*/
public class UserInfo implements Serializable {
private static final Integer DEFAULT_DISK_QUOTA = new Integer(512);
private String fFullName;
private String fLoginName;
private String fHomeDir;
private Integer fDiskQuota;
private PropertyChangeSupport fChangeSupport =
new PropertyChangeSupport(this);
public void addPropertyChangeListener(PropertyChangeListener l) {
fChangeSupport.addPropertyChangeListener(l);
}
public void removePropertyChangeListener(PropertyChangeListener l) {
fChangeSupport.removePropertyChangeListener(l);
}
public String getFullName() {
return fFullName;
}
public void setFullName(String fullName) {
fFullName = fullName;
}
public String getLoginName() {
return fLoginName;
}
public void setLoginName(String loginName) {
fLoginName = loginName;
}
public String getHomeDir() {
return fHomeDir;
}
public void setHomeDir(String homeDir) {
fHomeDir = homeDir;
}
public Integer getDiskQuota() {
return fDiskQuota;
}
public void setDiskQuota(Integer diskQuota) {
fDiskQuota = diskQuota;
fChangeSupport.firePropertyChange("diskQuota",
null, diskQuota);
}
/**
* Command - setup default quota.
*/
public void useDefaultQuota() {
setDiskQuota(DEFAULT_DISK_QUOTA);
}
} |
Second model class (application model) of editor holds list of system users and details of current selected user. The class is incomplete (contains TODO: items) and will be refined later:
Example 2. Listing of UsersEditorModel.java
import java.util.Collections;
import java.util.List;
import java.beans.PropertyChangeSupport;
/**
* Application model of user editor. Contains list of users to display,
* single selected user and command to remove selected user.
*
* This class will be refined later.
*/
public class UsersEditorModel {
private List fUsers = Collections.EMPTY_LIST;
private UserInfo fSelectedUser;
private PropertyChangeSupport fChangeSupport =
new PropertyChangeSupport(this);
public void addPropertyChangeListener(PropertyChangeListener l) {
fChangeSupport.addPropertyChangeListener(l);
}
public void removePropertyChangeListener(PropertyChangeListener l) {
fChangeSupport.removePropertyChangeListener(l);
}
public List getUsers() {
return fUsers;
}
public UserInfo getSelectedUser() {
return fSelectedUser;
}
public void setSelectedUser(UserInfo user) {
fSelectedUser = user;
fChangeSupport.firePropertyChange("selectedUser",
null, user);
// TODO: what if user is null ????
// what if user unremovable, like administrator
// command for remove should be disabled in some way
}
public void removeSelectedUser() {
if (getSelectedUser() != null) {
fUsers.remove(getSelectedUser());
}
// TODO: what about event ???
// TODO: what if selected user is administrator?
// System administrator cannot be removed!
}
} |
UsersEditorUI class which will
be shown later.
Below we explain basic concepts needed to understand advantages of Mediator framework. Whenever it is possible, the concepts are illustrated with real code snippets from our example application.
Bound Property is term used in
Java Beans specification to describe
such value provided by bean which when changed,
PropertyChangeEvent is fired.
Property selectedUser provided by
getSelectedUser and
setSelectedUser methods in listing
of UsersEditorModel is
example of bound property.
A binding is a connection betweeen application or domain model element and user interface element. We can recognize two kinds of bindings: value binding and command binding.
Main task of the value binding is
to synchronize model and user interface: when model changes, user interface
element is updated and when user changes user interface element, the change
is propagated back to the model. In our example we want bind
diskQuota property value to a text field or
selectedUser property to selected row in a list.
A value binding can be buffered: user modification is stored in temporary buffer and model change is delayed until explicit commit. This allows to perform revert - abandon modified value and to re-fetch original value from the model. Note that buffering can be applied to any user actions: editing value in a textfield, selection of item in combo box, selection of multiply items in list, scrollbar movement or window resizing.
The value binding can also perform validation. Since it is responsible for setting values to the model, it can hold error and warning rules which examines before model updating. In case of error it should notify some handler that commit is not possible. In our example there is much to validate:
User loginName cannot be empty and its length
must be in reasonable bounds (lets say 3-15). It can contain only
ASCII characters and digits.
User diskQuota can be empty,
but when filled it must be valid integer and also in reasonable bounds
(between 10MB and 1GB).
User homeDir home directory should
exist - if not, than it could
be considered as warning.
A command binding connects action-type user interface item
(like button, menu item, pressing OK or Esc
on a keyboard) to the command implemented by model.
The binding should trace command state and enable/disable given user interface item.
Commands will be explained in more detail below.
In our example commands are implemented as removeSelectedUser()
and useDefaultQuota().
Binder is facade that builds and manages bindings. It centralizes validation, value commiting, contains single point for destroying of all bindings.
Binder is implemented in class
com.danmich.bindf.Binder. In code snippets below
binder will be presented by variable name binder
.
Example of binder creation:
Example 3. Binder creation and clearing
public final class UsersEditorUI extends JPanel {
/**
* This binder is used for binding of currently selected user.
*/
private Binder fUserBinder = Binder.createBuffered(); (1)
/**
* This is main binder, used for {@link UsersEditorModel}
*/
private Binder fBinder = Binder.createUnbuffered(); (1)
// Components, layout, etc.
...
public void setModel(UsersEditorModel m) {
fBinder.unbind(); (2)
/* Bind "selectedUser" property from model to private
* "setSelected" value in this panel.
* Because the method is private, we must to specify class name
* too lookup method. And because this class is not designed
* for inheritance we can use "this" instead of full qualified
* class name.
*/
fBinder.bindValues( (3)
fBinder.valArgs(m).boundProp("selectedUser"),
fBinder.valArgs(this).setter("this.setSelected")
);
...
}
private void setSelected(UserInfo user) {
/* Remove bindings for previously selected user */
fUserBinder.unbind(); (2)
// binding code
...
}
/**
* Removes all links to model. Call this method to avoid memory leaks.
*/
public void destroy() {
fBinder.unbind(); (2)
fUserBinder.unbind(); (2)
}
} |
setSelected will be called.User interface does not know, how to obtain value to display. The
value can be stored somewhere as a bean property, item in a list or map,
element of a xml document, result of SQL query etc.
Value Access is abstraction used by binding
to get value from a model, set value to the model and validate the value.
Schema of IValueAccess interface is shown on
Figure 8.
Value access needs model instance as a context for its accessing logic. For example method needs object on which it can be called (unless it is static) or XPath evaluation needs XML document. In the framework such object is called root.
Very important feature of value access is ability to form a chain, as shown on Figure 9. In this way it is possible to:
define arbitrary complex logic for value accessing. For example get hash map from the model, get number stored under some key in the hash map and format the number to the string. Note that this works also in opposite direction: parse given string to a number and put to the same map under the same key.
add behavior to the value accessing - like buffering (see explanantion above), adding validation rules, advanced type conversion and so one.
In our example root can be UsersEditorModel
instance and value access chain of methods corresponding to
root.getSelectedUser().getLoginName()
or more complex
root.getSelectedUser().getDiskQuota()<->Int2StringConverter
Example 4. Value binding
public final class UsersEditorUI extends JPanel {
private JTextField fLoginName = new JTextField();
private JTextField fFullName = new JTextField();
private JTextField fHomeDir = new JTextField();
private JTextField fDiskQuota = new JTextField();
...
private void setSelected(UserInfo user) {
fUserBinder.unbind();
if (user == null) {
user = UserInfo.EMPTY;
}
BindingArgs.Builder valArgs = fUserBinder.valArgs(user);(1)
fUserBinder.bindSingleValue(
valArgs.boundProp("fullName"), (2)
fFullName
);
fUserBinder.bindSingleValue(
valArgs.boundProp("homeDir")
.warn(new ValidDirectoryExists()), (3)
fHomeDir
);
fUserBinder.bindSingleValue(
valArgs.boundProp("loginName")
.err(new ValidStringLengthMax("Login name", 15))(4)
.err(new ValidStringLengthMin("Login name", 3)),(5)
fLoginName
);
fUserBinder.bindSingleValue(
valArgs.boundProp("diskQuota")
.name("Disk Quota") (6)
.errCompareTo(">=", new Integer(10)) (7)
.errCompareTo("<=", new Integer(1000)) (8)
.int2Str(), (9)
fDiskQuota
);
...
}
} |
fullName
to text field fFullName.homeDir
with custom validation warning which tests if given directory exists:
public class ValidDirectoryExists implements IStrFunction {
public String eval(Object arg) {
File f = new File((String)arg);
if (f.exists()) {
return (f.isDirectory()) ? null : arg + " is not directory.";
}
return arg + " does not not exist.";
}
} |
loginName chained with
with 2 validations controlling string size. String used in constructors
is used for formatting error message.diskQuota chained with
with 2 validations controlling integer bounds. Finally chained
with converter which converts between Integer and String
(using int2String; there are other convertes
which uses java.text.NumberFormat instances).
Use of name is for formatting of error messages.Domain or application model contains not only data but also implements commands (open, save, close, compute something...).
Command is presented on Figure 10. Command is
invoked simply by executing method execute().
More advanced command is IAppCommand,
which holds bound property enabled. When model simply
disables the command it will be not execucted (even accidentaly)
and appropriate user interface item
to which the command is bound will be disabled. Binder
holds list of commit commands (typically associated with
OK button) and depending on validation state
enables/disables them.)
Commands in Java can be implemented as inner classes or methods. Most common aproach (at least in miscellaneous tutorials) is use of inner classes, but it is ineficient and inconvenient for programmer (in terms of amount of source code and readability). Framework supports both implementation styles with many helpers. Method style implementation is more convenient, does not lead to proliferation of listener classes, but there is price for it: they are not type safe (there is need to specify method name as string and there is no chance to verify at compile time if the method really exists).
In our example commands are implemented with methods
UserInfo.useDefaultQuota()
- when invoked model is updated with default quota setting and the change
is propagated to the corresponding text field
UsersEditorModel.removeSelectedUser()
- this command should be enabled only if user in a list is selected
and it is not unremovable user like administrator.
Example 5. Command binding
public class UsersEditorModel implements Serializable {
public final IAppCommand cmdRemoveUser = (1)
MethodCommand.simpleCall(this,
"this.removeSelectedUser").setEnabled(false);
...
public void setSelectedUser(UserInfo user) {
...
cmdRemoveUser.setEnabled( (2)
user != null && user.isEditable());
}
private void removeSelectedUser() {
fUsers.remove(getSelectedUser());
}
}
public final class UsersEditorUI extends JPanel {
private JButton fApply = new JButton("Apply");
private JButton fRemoveUser = new JButton("Remove");
private JButton fDefaultQuota = new JButton("Set default");
...
public void setModel(UsersEditorModel m) {
...
fBinder.bindCommand(m.cmdRemoveUser, (3)
fRemoveUser);
}
private void setSelected(UserInfo user) {
...
fUserBinder.bindCommand( (4)
MethodCommand.simpleCall(user,"useDefaultQuota"),
fDefaultQuota);
fUserBinder.bindCommand( (5)
fUserBinder.commitCmd(),
fApply);
fDefaultQuota.setEnabled(user.isEditable());
fApply.setEnabled(user.isEditable());
}
} |
removeSelectedUser is for
safety reasons made private.Commands are not just pointers to zero argument methods. They can have bound arguments for method invocation. This allows to any method adapt to the command interface. In following listing user example
Example 6. Command with bound argument
public class FahrenheitToCelsiusDemoWindow extends WindowModel {
private FahrenheitToCelsiusModel fModel = new FahrenheitToCelsiusModel();
public JComponent createContent() {
FahrenheitToCelsiusUI ui = new FahrenheitToCelsiusUI();
ui.setModel(fModel,
MethodCommand.simpleCall(this, "this.cmdClose", ui));(1)
return ui;
}
private void cmdClose(FahrenheitToCelsiusUI ui) {
ui.destroy();
...
}
}
public class FahrenheitToCelsiusUI extends JPanel {
private JButton fQuit = new JButton("Quit");
...
public void setModel(FahrenheitToCelsiusModel model, IAppCommand close) {
...
binder.onEvent(fQuit, SwingEvents.ACTION_PERFORMED, close);(2)
}
} |
There is one more element in the puzzle for binding to work: how to recognize, that model or user interface element changed and that update on opposite side of the binding should be performed. This problem solves design pattern called Observer design pattern (known also as Publisher/Subscriber). Binding framework can use any observer strategy which helps easily integration with existing solutions.
Framework strictly separates change event handling from model and value accessing. It means, that a model is not required to be an event source. For example hash map, list or XML document are perfect models, but they do not fire any events when they are updated. Object beeing an event source can be different than model and can implement any observer pattern You like.
In our example both UserInfo and
UsersEditorModel are also event sources.
For simple property changes they use
java.beans.PropertyChangeEvent to signal property
value changes.
Java has 2 observer patterns:
java.util.Observable, java.util.Observer pair
and widely used Java Beans style
event source/listener pattern.
Framework contains event handling abstraction defined by
IObserverPatternStrategy, with implementations
for java.util.Observable, java.util.Observer
and generic listener support for Java Beans style
event handling. Other implementations are possible.
Observer strategy is shown on Figure 11.
Framework contains definitions for most often used events in Swing and AWT. Adding support for other (custom) event listener types is matter of definition within 3 lines code. This is illustrated on Figure 12.
Lets return to our example and see, how to define binding which uses some event on source side:
Example 7. Explicit definition of observer strategy
public void setModel(UsersEditorModel m) {
fBinder.unbind();
/* First, most convenient way
fBinder.bindValues(
fBinder.valArgs(m).boundProp("selectedUser"),
fBinder.valArgs(this).setter("this.setSelected")
);
*/
/* Equivalent code, more explicit: */
fBinder.bindValues(
fBinder.valArgs(m).getter("getSelectedUser")(1)
.observerStrategy(JavaEvents.PROPERTY_CHANGE)(2)
.evtArg("selectedUser"), (3)
fBinder.valArgs(this).setter("this.setSelected")
);
...
}
|
We can summarize concepts explained above in schema of value binding, shown on Figure 13.
It is natural to represent list of items with arrays or
implementation of java.util.List. On user interface
side there are many components which conceptually works with lists
(combo boxes, lists, tables, spin boxes, check box groups, radio button
groups etc.). Binding framework implements mechanisms which allow
seamlessly use of lists and arrays with any such components. For simplicity
in further text we will discuss only lists, since framework uses list adapter
for arrays.
Thanks to java.util.AbstractList it is very
easy to create list adapter classes for existing structures and concepts.
For example integer spin box with numbers 0..255
could use RangeList(0,255) object to define
sequence of successive spin box values.
Lists often contain items which instances of complex classes, not just
strings or numbers. In our example list of user names only would be useless.
Instead we have a list of User instances. However
list user interface element does not know, how to
display User. Default Swing implementation uses
Object.toString() method, which is not very good design
idea.
List binding contains value converter which converts one value to another. The converters are mostly bidirectional and can be also chained like Value Acces to provide almost any desired conversion. Of course, You are free to implement Your own converters.
Binding framework contains 24 converters. There are following categories of converters:
string converters which converts numbers and dates (including SQL their variants) to/from string. This converters does not have annoyances of standard JDK formats - they do not throw exceptions for null values or empty strings. They work with validation part of framework, generates localizable error messages.
collection converters which converts between arrays and lists, tokenize strings into lists
casting converters converting
generic java.lang.Number to concrete numeric
types.
other converters like map based converter, method call based converter or generic converter chain.
Each list type component support selection. Selection brings
to model some value (values), in our example currently edited
UserInfo object.
Selection can be categorized from two points of view:
single - multiple:
single selection allows user to select zero or one item (combobox, spinbox, menu, radio button, list or table in single selection mode)
multiple selection allows user to select any number of items (list or table with multiple selection mode checboxes within group in menu or panel)
value - index based:
value based selection is used
for models which are interested in selected values rather then indices
(in our example it is currently selected
UserInfo object). In case of multiple
selection
java.util.List instance is used to
get/set from model (optionally array).
index based selection is used
when model is interested in index of selected item (items).
For single selection is used int number
(-1 for empty selection),
for multiple selection is used int []
array.
For list binding there are 2 posibilities of observing models:
treat list instance like other model values and use any event to notify that list changed, without description of change details. In this case user interface component will be refreshed without any optimization. This is simple, easy to use solution and in most cases without performance problems. Below is version of our example which uses this technique:
Example 8. List changes handled as other value changes
/**
* - class UsersEditorModel -
* Refined method version, which now fires change
* event when user is removed
*/
public void removeSelectedUser() {
if (getSelected() != null) {
fUsers.remove(getSelected());
fChangeSupport.firePropertyChange( (1)
"users", null, fUsers);
}
} |
PropertyChangeEvent.Sometimes for performance reasons must be information
about list change more specific - what interval was
changed, removed or inserted. For such structural
change description framework defines ListChangeEvent
and ListChangeListener with contract specified by
IListObserverStrategy interface and
default implementation. There are 3 ways, how to
support this kind of list observing on model side:
Most convenient way. Good news is, that You can use provided list decorator which takes care of event notification:
Example 9. Transparent List changes through
ListChangeDecorator
public class UsersEditorModel {
private List fUsers = Collections.EMPTY_LIST;
public UsersEditorModel() {
List list = readListFromSomewhere();
fUsers = new ListChangeDecorator(list); (1)
}
public void removeSelectedUser() {
if (getSelected() != null) {
fUsers.remove(getSelected()); (2)
}
}
|
You can have full control how and when list change events are fired:
Example 10. Explicit list event handling in model
public class UsersEditorModel {
private List fUsers = Collections.EMPTY_LIST;
private ListChangeSupport fListChangeSupport = (1)
new ListChangeSupport(this);
public void addListChangeListener(ListChangeListener listener) {
fListChangeSupport.addListChangeListener(listener);
}
public void removeListChangeListener(ListChangeListener listener) {
fListChangeSupport.removeListChangeListener(listener);
}
public void removeSelectedUser() {
if (getSelectedUser() != null) {
int index = fUsers.indexOf(getSelectedUser());
fUsers.remove(index);
fListChangeSupport.fireIntervalRemoved( (2)
"users", index, index);
}
}
|
Since IListObserverStrategy is interface
You can implement support for Your own list type events with custom strategy.
Below is example, how to bind list of users to a list component (the same binding code would work for combo box or spinner):
Example 11. List binding
public final class UsersEditorUI extends JPanel {
private JList fUserList = new JList();
...
public void setModel(UsersEditorModel m) {
fBinder.unbind();
fBinder.bindListSingle(
fBinder.listArgs(m).prop("users") (1)
.itemGetter("getLoginName") (2)
.selBoundProp("selectedUser"), (3)
fUserList);
...
}
} |
listArgs(m) and define
list of users as model property users.getLoginName
to define that list will show user login names instead of default
toString() values.selectedUser.
If the property would be provided by another model instance we had to
add selRoot(anotherModel) to the binding chain.All previous examples used easy, functional type facade API. Using several simple chained method calls with self describing method names is much more readable and robust then creating and wiring many small objects together manually. Furthermore, it decreases amount of classes needed to learn by programmer.
Data binding domain contains four orthogonal concepts: data access, data types, event handling and user interface components. So if there is N ways of accessing data source, M ways of event handling, K data types and L user interface komponents than total amount of possible binding scenarios is N*M*K*L! Mediator algorithms works with interfaces modeling each of this orthogonal "axes":
IValueAccess for data source
accessing
IObserverStrategy for event
handling
IValueConverter for data type
conversion
several interfaces specifying component adapters
Open architecture of the framework is solid foundation for implementation of enhancements in any direction:
other user interface toolkits or component suites (SWT)
other data models (web services, SQL databases)
other event models (enterprise messaging like JMS)
We could see that MVC Mediator solves many problems which client programmers encounter in their daily work.
But it is just beginning. The framework was designed with long term plans. We would like extend framework more toward enterprise features - accessing SQL data bases and Web Services. If there will be demand we will implement bindings to SWT toolkit, to well know graphics and chart libraries. Everything is possible, but it depends solely on You: potential framework user. By licensing framework and providing valuable feedback You support its further development. See [3] website to consider licensing options.
[1] Sun Microsystems, Inc., 2001, 0-201-77582-4, Addison-Wesley, JavaTM Look and Feel Design Guidelines: Advanced Topics, http://java.sun.com/docs/books/jlfadv/.
[3] MVC Mediator Homepage, http://www.danmich.com/mvcmediator .
[4] Scope HMVC framework Homepage, http://scope.sourceforge.net .
[5] Smalltalk/X Homepage, http://www.exept.de/exept/english/Smalltalk/frame_uebersicht.html .
[6] Cincom Smalltalk Homepage, http://smalltalk.cincom.com/index.ssp .
[7] Swing Architecture, MVC pattern explanation, http://java.sun.com/products/jfc/tsc/articles/architecture .
[8] Swing Short Course part 2, MVC pattern explanation, http://java.sun.com/developer/onlineTraining/GUI/Swing2/shortcourse.html .