Data binding
In previous sections, you learned how to use JDNC Components to improve your user interface. In this section, you will learn about data binding, which may fundamentally change your data-centric programming model.
Data binding provides an easy mechanism for you to create a read/write link between the UI controls and the data in your application. You can bind arbitrary data objects to front-end controls. In order to bind your data to the UI control, you first adapt the data structure to a common data model interface, called a DataModel. A DataModel contains all the information about your data fields. It allows uniform access to all properties of the model regardless the nature of the data. A DataModel provides access to:
- Metadata: A description of the properties of each data field, such as the data's type.
- Value: The current value for each data field.
To bind your DataModel to a UI control, you use a binding. The binding takes care of all the wiring between the data model and the UI control. Figure 14 illustrates the basic process of data binding in JDNC.
Figure 14. How data binding works
The next panel examines a simple program that illustrates these concepts.
Suppose we have the class in Listing 12.
Listing 12. A data binding example
public class Person {
private String firstName;
private String lastName;
private int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
... all the getters and setters.
}
|
Imagine we want to bind an object of the Person class to the UI controls shown in Figure 15.
Figure 15. We want to bind the Person class to these UI controls
First, you need to get your data object and the UI controls ready, as Listing 13 illustrates.
Listing 13. Preparing your data object and UI controls
final Person person = new Person("Homer", "Simpson", 40);
JTextField textFirstName = new JTextField(6); JTextField
textLastName = new JTextField(6);
|
As I mentioned, we need to adapt our data to a data model first. Because our object is a JavaBean, we can use the JavaBeanDataModel to adapt it to a data model, as Listing 14 shows.
Listing 14. Adapting a JavaBean to a data model
DataModel dataModel = new JavaBeanDataModel(Person.class, person);
|
The JavaBeanDataModel class introspects the Person class for all its properties and create corresponding metadata to describe them. Additionally, JavaBeanDataModel retrieves the current value for each property from the JavaBean object that you pass.
Now that you have adapted your data to the proper data model, you use Binding to bind the data model to the UI controls. There are quite a few binding classes available in JDNC, including TextBinding (which binds a data field to a text control like JTextField) and TableBinding (which binds a data field to a JTable). Instead of specifying the binding class directly, we'll use the factory method provided in the BindingMap class, shown in Listing 15.
Listing 15. Binding data to our UI controls
final Binding bindingFirstName =
BindingMap.getInstance().createBinding(textFirstName,
dataModel, "firstName");
final Binding bindingLastName =
BindingMap.getInstance().createBinding(textLastName,
dataModel, "lastName");
|
Note that the createBinding() method of BindingMap takes three arguments:
- The UI control to be bound
- The data model
- The name of the field (property) in the data model that you want to bind
You can use the binding to control the communication between the data model and the UI control. You use the pull() method to retrieve the current value from the data model and update the UI, as in Listing 16.
Listing 16. Retrieving the data and updating the UI
bindingFirstName.pull();
bindingLastName.pull();
|
Listing 17 shows the push() method that you use to update the data model using the values of the UI control.
Listing 17. Updating the data model
ActionListener actionListener = new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
System.out.println("Before push: " + person);
bindingFirstName.push();
bindingLastName.push();
System.out.println("After push: " + person);
}
};
textFirstName.addActionListener(actionListener);
textLastName.addActionListener(actionListener);
|
Finally, you draw the UI, as Listing 18 shows.
Listing 18. Drawing the UI
frame.getContentPane().add(new JLabel("Name:"));
frame.getContentPane().add(textFirstName);
frame.getContentPane().add(textLastName);
frame.pack();
frame.setVisible(true);
|
All the code on this panel may seem very tedious. However, it shows you the details of how data binding works. In later panels, you will learn techniques that can simplify data binding dramatically. Before that, though, let's quickly go through two important interfaces involved in data binding: DataModel and Binding.
The DataModel interface represents a record of named data fields. A DataModel can be constructed for a row of data in a database or for an arbitrary JavaBean. DataModel not only provides the current value of each field, it also provides the corresponding metadata for each field. For example, we can use the code in Listing 19 to peek at the MetaDatas that have been created for us when we construct a JavaBean-based data model.
Listing 19. Examining metada
final Person person = new Person("Homer", "Simpson", 40);
DataModel dataModel =
new JavaBeanDataModel(Person.class, person);
System.out.println("Metadatas: ");
MetaData[] metaDatas = dataModel.getMetaData();
for (int i = 0; i < metaDatas.length; i++) {
System.out.println("Name: " + metaDatas[i].getName() + ", " +
"Element class: " + metaDatas[i].getElementClass());
}
System.out.println("\nCurrent values: ");
String[] fields = dataModel.getFieldNames();
for (int i = 0; i < fields.length; i++) {
Object value = dataModel.getValue(fields[i]);
System.out.println("Field: " + fields[i] + ", " +
"Current Value: " + value + ", Type: " + value.getClass());
}
|
When you run the code, you should see the following output:
Metadatas:
Name: age, Element class: int
Name: class, Element class: class java.lang.Class
Name: firstName, Element class: class java.lang.String
Name: lastName, Element class: class java.lang.String
Current values:
Field: age, Current Value: 40, Type: class java.lang.Integer
Field: class, Current Value:
class com.asprise.jdnc.demo.Person,
Type: class java.lang.Class
Field: firstName, Current Value: Homer,
Type: class java.lang.String
Field: lastName, Current Value: Simpson,
Type: class java.lang.String
|
Note that age, firstName, and lastName are properties defined in our Person class. The JavaBeanDataModel class introspects the object and finds the getClass() method (inherited from java.lang.Object); thus, it creates the class field.
The following are two data exchange methods declared in the DataModel interface:
-
getValue(String fieldName): Returns the current value of the specified field. -
setValue(String fieldName, Object value): Sets the current value of the specified field to the given value.
Once you have the data model ready, you can bind it to proper UI controls. To perform such binding, you need the Binding interface.
The Binding interface binds a data model to a UI control. The main functions of Binding include:
- Pulling a value from the data model into the UI control
- Validating the value contained in the UI control
- Pushing the validated value from the UI controls to the data model
The following are the key methods declared in the Binding interface:
-
pull(): Pulls the value of the data model into the UI control -
push(): Pushes the current value contained in the UI control into the data model -
isModified(): Returns a Boolean that indicates whether or not the value contained in the UI control has been modified since the value was last pushed or pulled -
isValid(): Returns a Boolean that indicates whether or not the value contained in the UI control is valid -
getDataModel(),getFieldName(): Return the data model and the field bound to the UI control, respectively -
getComponent(): Returns the UI control bound to the data model
The createBinding() method of the BindingMap class is very handy for creating a binding between a UI control and a data model:
Binding createBinding(JComponent component, DataModel model, String fieldName) |
At this point, you should have a basic understanding of how DataModel and Binding make data binding happen. In the following panels, you'll see how to customize and simplify data binding.
In this panel, we will try to bind the age property of our Person class to a UI control. Earlier in this section, you saw how to construct a data model and bind firstName and lastName to the UI controls. Listing 20 is our first attempt to bind age to a JTextField.
Listing 20. Binding age to a JTextField
JTextField textFirstName = new JTextField(6);
JTextField textLastName = new JTextField(6);
JTextField textAge = new JTextField(2);
final Person person = new Person("Homer", "Simpson", 40);
DataModel dataModel =
new JavaBeanDataModel(Person.class, person);
final Binding bindingFirstName =
BindingMap.getInstance().createBinding(textFirstName,
dataModel, "firstName");
final Binding bindingLastName =
BindingMap.getInstance().createBinding(textLastName,
dataModel, "lastName");
final Binding bindingAge =
BindingMap.getInstance().createBinding(textAge,
dataModel, "age");
bindingFirstName.pull();
bindingLastName.pull();
bindingAge.pull();
ActionListener actionListener = new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
System.out.println("Before push: " + person);
bindingFirstName.push(); bindingLastName.push();
bindingAge.push();
System.out.println("After push: " + person);
}
};
textFirstName.addActionListener(actionListener);
textLastName.addActionListener(actionListener);
textAge.addActionListener(actionListener);
frame.getContentPane().add(new JLabel("Name: "));
frame.getContentPane().add(textFirstName);
frame.getContentPane().add(textLastName);
frame.getContentPane().add(new JLabel("Age: "));
frame.getContentPane().add(textAge);
|
When you run the code, you'll see that the UI pops up as expected; it should look like Figure 16.
Figure 16. The UI looks as we expect it to
However, if you modify the age and press Enter, the following exception will be thrown:
java.lang.IllegalArgumentException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.jdesktop.swing.data.JavaBeanDataModel.setValueImpl (JavaBeanDataModel.java:115) at org.jdesktop.swing.data.AbstractDataModel.setValue (AbstractDataModel.java:45) at org.jdesktop.swing.binding.AbstractBinding.push (AbstractBinding.java:119) at com.asprise.jdnc.demo.BindingDemoV3$1.actionPerformed (BindingDemoV3.java:54) at javax.swing.JTextField.fireActionPerformed(Unknown Source) at javax.swing.JTextField.postActionEvent(Unknown Source) at javax.swing.JTextField$NotifyAction.actionPerformed (Unknown Source) at javax.swing.SwingUtilities.notifyAction(Unknown Source) ... |
When you pressed the Enter key, the action listener was invoked. bindingAge.push() pushes the current value of the JTextField -- which is a String -- to the model. But because the model only accepts Integers as values for age, we get the exception shown above. To solve this kind of problem, we need to use a converter to convert the value in the UI to the type accepted by the data model. For the age property, we use the code in Listing 21 to set up the converter.
Listing 21. Setting up the converter
...
final Person person = new Person("Homer", "Simpson", 40);
DataModel dataModel = new JavaBeanDataModel(Person.class, person);
MetaData metaDataAge = dataModel.getMetaData("age");
metaDataAge.setConverter(Converters.get(Integer.class));
...
|
Now, you can update the age property when you run the code.
In the last panel, you saw how to use a converter to convert the value contained in a UI control to the type accepted by the data model. In JDNC, the Converter interface defines a bidirectional conversion between string values and Java objects. There are only two methods in the Converter interface:
-
Object decode(String value, Object format): Converts the specified text value to an object of the specified type -
String decode(Object value, Object format): Converts the specified object to a string representation
In JDNC, the Converters class contains a static registry of a set of converters for the common Java data types. The Converters class has proper converters for the following types:
-
Boolean -
Date -
Double -
Float -
Integer -
Long -
Short -
String
To obtain a converter for a certain type, use the following method of the Converters class:
public static Converter get(Class klass) |
You can also implement a converter and put it into the Converters class as follows:
public static void put(Class klass, Converter converter) |
Once you obtain a proper converter, you can then provide it to the field through its MetaData, as in Listing 22.
Listing 22. Providing a converter to a field
MetaData metaDataAge = dataModel.getMetaData("age");
metaDataAge.setConverter(Converters.get(Integer.class));
|
The converter will be used by the data binding to perform conversion when pull() or push() methods are called.
The Validator interface represents a validator that performs validation on a value in the UI control before it can be pushed to the data model. There is only one method declared in the Validator interface:
boolean validate(Object value, Locale locale, String[] error) |
In our example code, we want to add a validator to the age property in order to validate the user input. To do so, you simply obtain the MetaData for age and add a validator to it, as in Listing 23.
Listing 23. Adding a validator to the age property
MetaData metaDataAge = dataModel.getMetaData("age");
metaDataAge.setConverter(Converters.get(Integer.class));
metaDataAge.addValidator(new Validator() {
public boolean
validate(Object value, Locale locale, String[] error) {
int val = ((Integer)value).intValue();
if(val < 0 || val > 200) {
error[0] = "Invalid age: " + val;
return false;
}
return true;
}
});
|
Now, when users enter an invalid value and presses Enter, they will be told that the value is invalid, as shown in Figure 17.
Figure 17. A validator flags an invalid value
Similarly, you can add one or more validators for other properties.
Simplify data binding with JForm and JNForm
At this point, you might feel that performing data binding in JDNC is a very tedious task. Earlier in this section, you needed to create the data model first. Then you created a UI component and a binding for each property. After that, you needed to implement action listeners to monitor UI events and push the values back to the data model. If you use JForm or JNForm, however, most of these tedious procedures are greatly simplified. In Listing 24, I've rewritten our sample application to use JNForm.
Listing 24. Our data binding application, rewritten
public class BindingDemoV5 {
public static void main(String[] args) throws
IntrospectionException {
JFrame frame = new JFrame("Simple Binding Demo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().setLayout(new FlowLayout());
final Person person = new Person("Homer", "Simpson", 40);
DataModel dataModel =
new JavaBeanDataModel(Person.class, person);
MetaData metaDataAge = dataModel.getMetaData("age");
metaDataAge.setConverter(Converters.get(Integer.class));
JNForm form = new JNForm();
try {
form.bind(dataModel, "firstName");
form.bind(dataModel, "lastName");
form.bind(dataModel, "age");
} catch (BindException e) {
e.printStackTrace();
}
frame.add(form);
frame.pack();
frame.setVisible(true);
}
}
|
If you compare this version to the original version, you'll see that the amount of code has been significantly reduced. If you run the code in Listing 24, you should see the screen illustrated in Figure 18.
Figure 18. JNForm in action
When the user clicks Reset, data is pulled from the model and filled into the UI controls. When the user clicks Submit, data is synchronized from the UI controls to the data model.
You might notice that there is no way to update the age, because it is rendered in a JLabel. Fortunately, JForm allows us to specify the control for a field in the data model; Listing 25 illustrates this.
Listing 25. Specifying the control for a field
JNForm form = new JNForm();
try {
form.bind(dataModel, "firstName");
form.bind(dataModel, "lastName");
// form.bind(dataModel, "age");
form.getForm().bind(dataModel, "age", new JTextField(2));
} catch (BindException e) {
e.printStackTrace();
}
|
Here, we've made the age field editable, as shown in Figure 19.
Figure 19. The age field is no editable
The DefaultTableModelExt class, extending the java.swing.table.AbstractTableModel class, represents a tabular data model; it's capable of loading data from a text file.
Basically, a DefaultTableModelExt consists of two portions: the structure of the model and the data cells. You use MetaData to specify the structure of the model. After the structure has been properly defined, you can either add and remove data programmatically or load data from a text file.
Suppose we have a tab-separated values (TSV) file named books.txt:
Professional Java Native Interfaces with SWT/JFace Jackwind Li Guojie 0470094591 $16.00 http://images.amazon.com/images/P/0470094591.01._SCMZZZZZZZ_.jpg Head First Java Kathy Sierra, Bert Bates 0596009208 $29.67 http://images.amazon.com/images/P/0596009208.01._SCMZZZZZZZ_.jpg Effective Java Programming Language Guide Joshua Bloch 0201310058 $28.55 http://images.amazon.com/images/P/0201310058.01._SCMZZZZZZZ_.jpg Java In A Nutshell David Flanagan 0596007736 $15.28 http://images.amazon.com/images/P/0596007736.01._SCMZZZZZZZ_.jpg ... |
We can use the code in Listing 26 to load it and display it in a JNTable.
Listing 26. Loading TSV data and displaying it in a JNTable
public class TableModelExt {
public static void main(String[] args)
throws IOException, BindException {
JFrame frame = new JFrame("DefaultTableModelExt Demo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
DefaultTableModelExt tableModelExt = new DefaultTableModelExt();
tableModelExt.setColumnCount(5);
// Metadata
MetaData columnMetaData = tableModelExt.getColumnMetaData(0);
columnMetaData.setName("Title");
columnMetaData = tableModelExt.getColumnMetaData(1);
columnMetaData.setName("Author");
columnMetaData = tableModelExt.getColumnMetaData(2);
columnMetaData.setName("ISBN");
columnMetaData = tableModelExt.getColumnMetaData(3);
columnMetaData.setName("Price");
columnMetaData = tableModelExt.getColumnMetaData(4);
columnMetaData.setName("Cover_URL");
tableModelExt.setSource("file:///F:/Writing/TUTORIAL/
code/data/books.tsv");
tableModelExt.startLoading();
frame.add(new JNTable(tableModelExt));
frame.pack();
frame.setVisible(true);
}
}
|
In the code in Listing 26, we create an instance of DefaultTableModelExt. Then we define each column with a MetaData object. After that, we use the setSource() method to set the source file from which the data should be loaded. Then we call startLoading() to load the data asynchronously. Finally, we use the table model constructed to create a JNTable and display it in the frame, as shown in Figure 20.
Figure 20. A JNTable with an extended TableModel
In the next panel, you will learn how to convert a DefaultTableModelExt into a data model and take advantage of data binding.
In the last panel, you saw how to use DefaultTableModelExt to load data from a TSV file. The TableModelExtAdapter class can adapt a DefaultTableModelExt into a data model. Thus, you can use the data model to do data binding. In the last panel, we simply displayed the book information using a JNTable. Now, we want to display the book cover when the user selects a book from the table.
First, we need to define metadata and start to load data for the TSV file, which we do with the code in Listing 27.
Listing 27. Defining metadata and loading data.
DefaultTableModelExt tableModelExt =
new DefaultTableModelExt();
tableModelExt.setColumnCount(5);
Metadata MetaData columnMetaData =
tableModelExt.getColumnMetaData(0);
columnMetaData.setName("Title");
columnMetaData = tableModelExt.getColumnMetaData(1);
columnMetaData.setName("Author");
columnMetaData = tableModelExt.getColumnMetaData(2);
columnMetaData.setName("ISBN");
columnMetaData = tableModelExt.getColumnMetaData(3);
columnMetaData.setName("Price");
columnMetaData = tableModelExt.getColumnMetaData(4);
columnMetaData.setName("Cover_URL");
tableModelExt.setSource("file:///F:/Writing/TUTORIAL/
code/data/books.tsv");
tableModelExt.startLoading();
|
Because the data is loaded asynchronously, we want to wait until it finishes loading. The code in Listing 28 does this.
Listing 28. Waiting for the data to finish loading
// wait till data loaded.
while(true) {
if(!tableModelExt.isLoading())
break;
try {
Thread.sleep(20);
}catch(InterruptedException ie) {
continue;
}
}
|
Then we adapt the DefaultTableModelExt into a TableModelExtAdapter:
final TableModelExtAdapter adapter = new TableModelExtAdapter(tableModelExt); |
In Listing 29, we create an instance of JXImagePanel, which is a JPanel with image display capability.
Listing 29. Creating an instance of JXImagePanel
final JXImagePanel imagePanel = new JXImagePanel();
imagePanel.setPreferredSize(new Dimension(200, 200));
|
We have the data model and the UI control ready. Now we can use a binding to bind them together:
ImagePanelBinding binding = new ImagePanelBinding(imagePanel, adapter, "Cover_URL"); |
ImagePanelBinding binds a field with type java.lang.String with a JXImagePanel. For this binding, the metadata is quite clear, but what are the current values (because there are so many rows)? By default, a TableModelExtAdapter does not have any current value. You can use the setRecordValue() method to use the specified row to supply current values. For example, you use could setRecordValue(0) to take the values contained in the first row as the current values to be supplied by the TableModelExtAdapter.
In Listing 30, we create the JNTable for displaying book information and add a selection listener to it.
Listing 30. Creating the JNTable
final JNTable table = new JNTable(tableModelExt);
table.getTable().setSortable(false);
table.getTable().getSelectionModel().addListSelectionListener(
new ListSelectionListener() {
public void valueChanged(ListSelectionEvent se) {
adapter.setRecordIndex(table.getTable().getSelectedRow());
}
});
frame.add(table, BorderLayout.CENTER);
frame.add(imagePanel, BorderLayout.SOUTH); frame.pack();
frame.setVisible(true);
|
When you run the code in Listing 30, you should see the screen illustrated in Figure 21.
Figure 21. Adding the image panel
When you select a book, the corresponding book cover will be displayed in the bottom panel.


