Skip to main content

Binding too restrictive?

4 replies [Last post]
rbair
Offline
Joined: 2003-07-08
Points: 0

Some thoughts regarding the current Binding implementation:

Binding is too narrow in scope. It only allows binding between a datamodel and a component. Why? Why not bind to anything? After all, the Binding is absolutely responsible for knowing how to change the component/object based on the current data anyway. So why limit it to components?

One good reason to consider binding to any object is that you could implement master/detail relationships between datamodels in this way. The master DataModel is bound by an MDBinding to a detail DataModel. When the master DataModel changes, the MDBinding updates the contents of the detail DataModel.

Different MDBindings can be created to do things such as: based on the fieldName, get a collection of objects from the master and put those objects into the detail; based on some RowSet, execute a query to get the detail items from a jdbc DataSource and put the results in the detail; based on the fieldName get a URL and open it, loading whatever is there into the detail DataSource. As long as the MDBindings have a simple API for people to extend (such as a single method, maybe two) you could easily provide any behavior you want. Shucks, a custom binding could open a connection to the datasource and save the user's selection as some preference if it wanted to.

Why is are the methods of this interface package private? Is getFieldName() even necessary?

There should be a read-only property on the binding. Some components/objects will happily display data, but changes in it shouldn't be propagated up. For instance, some MDBindings (such as the sql/rowset variety) don't want to ever push data back up to the master DataModel. On a related note, bindings should provide an enable/disable feature. This is handy in many cases (such as when data is not loaded into the DataModel, perhaps, or when the window is disabled because the data loaded in the model is read-only). It MAY propogate the enable/disable on to its bound object/UI component.

Richard

Reply viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
rbair
Offline
Joined: 2003-07-08
Points: 0

BTW, while you guys have been playing around at JavaOne (:-)), I've been digging into the source. I have a working implementation of the RowSetDataModel, and a lot of other toys. I know y'all are super busy right now with the convention, but I'm just in Sacramento and could meet anyone down in San Fran today or tomorrow evening and go over code and API.

Rich

Mark Davidson
Offline
Joined: 2006-02-17
Points: 0

Hey Rich,

Glad to see that you have a lot of enthusiasm and that you have been digging into the code. You will be happy to know that the JDNC project has been very well recieved at JavaOne. I was working at the JDNC booth all day and I got a lot of interest, feedback and ideas. We knew there was a need for this in the community.

I don't think we have the time to meet during J1. The JDNC engineering team is based in Santa Clara. Perhaps we can meet in a couple of weeks.

--Mark

Anonymous

[written last night, but couldn't post because forum seemed to be down]
This is timely. I literally just came from a JavaOne Birds-of-a-Feather session on data binding given by Karsten of JGoodies fame, so data-binding is fore-front on my mind.

Karsten's done some very good work here, and I think there are definitely concepts (if not also the library) worth leveraging. After j1 (and a little vacation) I plan to do some more exploration of the options and I'll initiate a more detailed design discussion so we can move this forward. But, I'll make a few quick comments now from my JavaOne hotel room....

> Binding is too narrow in scope. It only allows
> binding between a datamodel and a component. Why? Why
> not bind to anything? After all, the Binding is
> absolutely responsible for knowing how to change the
> component/object based on the current data anyway. So
> why limit it to components?

To summarize the way it currently works now (for those who haven't looked): We have introduced an interface called org.jdesktop.swing.data.DataModel, which is intended to be used as a lightweight wrapper around arbitrary domain model objects (JavaBean, RowSet, etc), but provides a consistent api for:
- getting list of field names in model (translates to list of properties on bean or list of columns in rowset)
- getting the total number of records in the domain model (number of beans or number of rows)
- getting/setting the current record index in the model
- setting/getting field value for current record
- getting field meta-data (type, edit constraints, etc) for current record
- registering listener for value changes on fields in the model

We provide concrete implementations of DataModel for beans (JavaBeanDataModel), TabularDataModel (TabularDataModelAdapter), [eventually RowSet], and a simple single-record map (DefaultDataModel).

Currently, the Binding classes take this as a parameter so that they have a consistent mechanism for pulling and pushing values between the UI components and the domain model and thus one doesn't have to write binding classes specific to domain model types.

So, you can use the Binding classes to hook up to almost anything, however if it's not a JavaBean, a TabularDataModel, or [eventually] a RowSet, you'd have to create the DataModel wrapper yourself.

I confess I hemmed and hawed over this design. An earlier version introduced a smaller interface, called a ValueAdapter, which handled just the setValue/getValue/addValueChangeListener tasks for an individual field in a given domain model. The binding classes then took just the metadata object and the value adapter as parameters (rather than the DataModel and fieldName). I abandoned this because of concern that it was becoming more complex than it needed to be, and because it would result in lots of tiny little objects.

Anyhow, I'd like to better understand your MDBinding suggestion below. Can you provide some code examples on how it might look?

>
> One good reason to consider binding to any object is
> that you could implement master/detail relationships
> between datamodels in this way. The master DataModel
> is bound by an MDBinding to a detail DataModel. When
> the master DataModel changes, the MDBinding updates
> the contents of the detail DataModel.

[snip]

>
> Richard

rbair
Offline
Joined: 2003-07-08
Points: 0

Hey Aim,

I wish I could be there at JavaOne to discuss this stuff with y'all personally (it sure would've been fun).

I apologize for the verbosity of this posting, but I just don't have anywhere else to put code and link to! Hopefully it is readable enough (even though the forum software hoses the formatting of the code).

First, I think you made the right choice with the binding framework. In my jgui project I forced the components to implement a binding sort of framework, which meant extending every swing component and any other I wanted to use. The Binding architecture works a lot better.

Second, I personally find the TabularDataModel and TabularDataModelAdapter a clumsy way of handling JTables. Instead, I wrote a little TableBinding which does the trick (and a bit more, which I'll talk about next).

Third, the TableBinding. Using this binding DOES impose a restriction on the JTable -- it cannot have a custom table model. However, if you wanted to hook a table model up to a binding you could. Here it is:

////////////////////////////////////////////////////
package org.jdesktop.swing.binding;

import javax.swing.JComponent;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableModel;

import org.jdesktop.swing.data.DataModel;
import org.jdesktop.swing.data.ModelChangeEvent;
import org.jdesktop.swing.data.ModelChangeListener;

/**
* @author Richard Bair
*/
public class TableBinding extends AbstractUIBinding {
private JTable table;

/**
* @param component
* @param dataModel
* @param fieldName
* @param validationPolicy
*/
public TableBinding(JTable component, DataModel dataModel) {
super(component, dataModel, null, TableBinding.AUTO_VALIDATE_NONE);
//construct a TableModel for the table based on the given dataModel.
TableModel tm = new DataModelTableModel(dataModel);
table.setModel(tm);
}

/* (non-Javadoc)
* @see org.jdesktop.swing.binding.AbstractUIBinding#getBoundComponent()
*/
protected JComponent getBoundComponent() {
return table;
}

/* (non-Javadoc)
* @see org.jdesktop.swing.binding.AbstractUIBinding#setBoundComponent(javax.swing.JComponent)
*/
protected void setBoundComponent(JComponent component) {
if (!(component instanceof JTable)) {
throw new IllegalArgumentException("TableBindings only accept a JTable or one of its child classes");
}
this.table = (JTable)component;
}

/* (non-Javadoc)
* @see org.jdesktop.swing.binding.AbstractUIBinding#getComponentValue()
*/
protected Object getComponentValue() {
//a table component never updates its parent data model in this way
return null;
}

/* (non-Javadoc)
* @see org.jdesktop.swing.binding.AbstractUIBinding#setComponentValue(java.lang.Object)
*/
protected void setComponentValue(Object value) {
//means nothing to this binding
}

private static final class DataModelTableModel extends AbstractTableModel {
private DataModel dm;
private String[] fieldNames;

public DataModelTableModel(final DataModel dm) {
this.dm = dm;
fieldNames = dm.getFieldNames();
//register with the data model
dm.addModelChangeListener(new ModelChangeListener() {
/* (non-Javadoc)
* @see org.jdesktop.swing.data.ModelChangeListener#metaDataChanged(org.jdesktop.swing.data.ModelChangeEvent)
*/
public void metaDataChanged(ModelChangeEvent event) {
fieldNames = dm.getFieldNames();
fireTableStructureChanged();
}

/* (non-Javadoc)
* @see org.jdesktop.swing.data.ModelChangeListener#dataChanged(org.jdesktop.swing.data.ModelChangeEvent)
*/
public void dataChanged(ModelChangeEvent event) {
fireTableDataChanged();
}
});
}

public Class getColumnClass(int columnIndex) {
return dm.getMetaData(fieldNames[columnIndex]).getElementClass();
}

public String getColumnName(int column) {
return dm.getMetaData(fieldNames[column]).getLabel();
}

public boolean isCellEditable(int rowIndex, int columnIndex) {
return !dm.getMetaData(fieldNames[columnIndex]).isReadOnly();
}

public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
dm.setValueAt(fieldNames[columnIndex], rowIndex, aValue);
}

/* (non-Javadoc)
* @see javax.swing.table.TableModel#getRowCount()
*/
public int getRowCount() {
return dm.getRecordCount();
}

/* (non-Javadoc)
* @see javax.swing.table.TableModel#getColumnCount()
*/
public int getColumnCount() {
return dm.getFieldCount();
}

/* (non-Javadoc)
* @see javax.swing.table.TableModel#getValueAt(int, int)
*/
public Object getValueAt(int rowIndex, int columnIndex) {
return dm.getValueAt(fieldNames[columnIndex], rowIndex);
}

}

/////////////////////////////////////////////////////

Oh, and fourth, I added a new listener to the DataModel called "ModelChangeListener" that is used for notifying of changes to the model (such as a change in schema). With this listener and this TableBinding, any JTable attached to the binding will automatically set up its columns and data when the underlying DataModel changes. This little proof of concept shows how solid the underlying design is that the JDNC team has come up with! Really, the Binding concept and the MetaData concept are my favorites.

Ok, again, since I don't have anywhere to post code and link to, here's an example of the MDBinding concept. It includes 2 new classes and changes to the JavaBeanDataModel:

/////////////////////////////////////////////////////

/**
* @author Richard Bair
*/
public abstract class AbstractMasterDetailBinding extends AbstractBinding {
protected DataModel detail;
protected boolean enabled = true;

/**
* @param detailDataModel
* @param dataModel
* @param fieldName
* @param validationPolicy
*/
public AbstractMasterDetailBinding(DataModel detailDataModel, DataModel masterDataModel,
String fieldName, int validationPolicy) {
super(detailDataModel, masterDataModel, fieldName, validationPolicy);
}

/* (non-Javadoc)
* @see org.jdesktop.swing.binding.Binding#getBoundObject()
*/
public Object getBoundObject() {
return detail;
}

/* (non-Javadoc)
* @see org.jdesktop.swing.binding.Binding#pull()
*/
public boolean pull() {
return doPull(super.dataModel, detail);
}

protected abstract boolean doPull(DataModel master, DataModel detail);

/* (non-Javadoc)
* @see org.jdesktop.swing.binding.Binding#push()
*/
public boolean push() {
// by default, do not update the master
return false;
}

/* (non-Javadoc)
* @see org.jdesktop.swing.binding.Binding#isEnabled()
*/
public boolean isEnabled() {
return enabled;
}

/* (non-Javadoc)
* @see org.jdesktop.swing.binding.Binding#setEnabled(boolean)
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

/* (non-Javadoc)
* @see org.jdesktop.swing.binding.Binding#isReadOnly()
*/
public boolean isReadOnly() {
return true;
}

/* (non-Javadoc)
* @see org.jdesktop.swing.binding.AbstractBinding#setBoundObject(java.lang.Object)
*/
protected void setBoundObject(Object detail) {
if (!(detail instanceof DataModel)) {
throw new IllegalArgumentException("The bound object of a MasterDetailBinding MUST be a DataModel!");
}
this.detail = (DataModel)detail;
}

/////////////////////////////////////////////////////

/////////////////////////////////////////////////////

/**
* A default implementation of a javabean based master/detail binding.
* This binding implementation does not care what kind of DataModel is used
* for the master, but the detail MUST be an implementation of the
* JavaBeanDataModel.

* This implementation will query the master for the value of a specific field.
* If the value is a Collection, Iterator, Enumeration, or Array, then the
* contents of the data structure are inferred to be the detail items. Any other
* type of object will be considered the one and only item in the detail DataModel.
* For example, consider the following situation:
*

    *
  • The master DataModel contains a field named "orders"
  • *

  • The class of the field "orders" is java.util.List
  • *

  • The list contains 3 values, each of which are Order objects
  • *

* In the above scenario, when a new row is made current in the master DataModel, then
* the orders list will be retrieved from the current row. It will be expanded and the
* 3 Order objects will be placed in the detail (which was previously cleared, of course).
*

* In contrast, consider the following scenario:
*

    *
  • The master DataModel contains a field named "employee"
  • *

  • The class of the field "employee" is com.mycompany.Employee
  • *

* When a new row is now selected in the master DataModel, then the detail DataModel will have
* a single object/row in it -- the Employee object.
* @author Richard Bair
*/
public class JavaBeanMasterDetailBinding extends AbstractMasterDetailBinding {

/**
* @param detailDataModel
* @param masterDataModel
* @param fieldName
* @param validationPolicy
*/
public JavaBeanMasterDetailBinding(JavaBeanDataModel detailDataModel,
DataModel masterDataModel, String fieldName, int validationPolicy) {
super(detailDataModel, masterDataModel, fieldName, validationPolicy);
}

/* (non-Javadoc)
* @see org.jdesktop.swing.binding.AbstractMasterDetailBinding#doPull(org.jdesktop.swing.data.DataModel, org.jdesktop.swing.data.DataModel)
*/
protected boolean doPull(DataModel master, DataModel detail) {
if (!(detail instanceof JavaBeanDataModel)) {
throw new IllegalArgumentException("Detail was not a JavaBeanTableModel");
}
Object value = master.getValue(super.getFieldName());
((JavaBeanDataModel)detail).replaceData(value);
return true;
}

/////////////////////////////////////////////////////

/////////////////////////////////////////////////////

Changes to the JavaBeanDataModel include these two
new methods:

/* (non-Javadoc)
* @see org.jdesktop.swing.data.DataModel#replaceData(java.lang.Object)
*/
public void replaceData(Object newData) {
data.removeAll();
appendData(newData);
}

/* (non-Javadoc)
* @see org.jdesktop.swing.data.DataModel#appendData(java.lang.Object)
*/
public void appendData(Object newData) {
if (newData != null) {
Class klazz = newData.getClass();
if (Collection.class.isAssignableFrom(klazz)) {
Collection c = (Collection) newData;
data.addAll(c);
} else if (Iterator.class.isAssignableFrom(klazz)) {
List lvalue = new ArrayList();
Iterator itr = (Iterator)newData;
while (itr.hasNext()) {
lvalue.add(itr.next());
}
data.addAll(lvalue);
} else if (Enumeration.class.isAssignableFrom(klazz)) {
data.addAll(Collections.list((Enumeration)newData));
} else if (klazz.isArray()) {
Object[] arrayValue = (Object[]) newData;
List list = new ArrayList();
Collections.addAll(list, arrayValue);
data.addAll(list);
}
}
}

/////////////////////////////////////////////////////

Oh, and the Binding of course has been changed so that it doesn't have a getComponent/setComponent method pair anymore, but it has a getBoundObject and setBoundObject method pair, so you don't have to bind directly to a UI component anymore.

Richard