Swing Data Binding

  • Easy to use, understand and to extend to support custom ui elements
  • Fast and light
  • Support PropertyChangeSupport if you want it
  • Swap out your model objects for active bindings (rebind)
  • Unbind will remove all listeners
  • Supply user feedback with ease
  • Out-of-the-box support for JXDatePicker and Joda Time
  • Provides a fluent interface if that's your bag

Quick Start

Download Swing Data Binding binaries, sources and javadoc, or if you are using Maven:

        <dependency>
            <groupId>no.tornado.databinding</groupId>
            <artifactId>databinding</artifactId>
            <version>1.0</version>
        </dependency>

        <!-- Optional support for JXDatePicker and Joda Time -->
        <dependency>
            <groupId>no.tornado.databinding</groupId>
            <artifactId>jxdatepicker-support</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>no.tornado.databinding</groupId>
            <artifactId>jodatime-support</artifactId>
            <version>1.0</version>
        </dependency>

    

Let's start by instantiating a fictive Person model object and bind it's username property to a JTextField:

Person person = new Person();
JTextField usernameField = new JTextField();
                        
BindingGroup bindings = new BindingGroup(person);
bindings.add(usernameField, "username");
bindings.bind();
                    

That's all you need to get started! Read on to learn how to configure different binding/validation strategies, how to provide feedback etc.

Example Application

There is an example application that showcases some basic use of the framework.

Although most of the code deals with Swing/UI specifics, the setupBinding() method shows some more binding examples.

Binding and Validation strategies

There are 3 separate modes for both binding and validation. The BindingGroup contains a default, and you can override individual bindings if you like. The 3 modes are:

  • ONCHANGE - Fires when a value or selection changes (on click, on key typed depending on the component)
  • ONBLUR - Fires when the ui field looses focus
  • ONFLUSH - Fires explicitly through bindings.flushUIToModel() or bindings.flushModelToUI()

The default BindingStrategy is BindingStrategy.ONFLUSH and the default ValidationStrategy is ValidationStrategy.ONCHANGE. You can override the default on the BindingGroup:

bindings.defaultBindingStrategy(BindingStrategy.ONBLUR);
bindings.defaultValidationStrategy(ValidationStrategy.ONBLUR);
                    

You can configure individual bindings with a different strategy as well:

Binding usernameBinding = bindings.add(usernameField, "username");
usernameBinding.validationStrategy(ValidationStrategy.BLUR);                        
                    

Note that since BindingStrategy.ONFLUSH is default, you have to do bindings.flushUIToModel() in your save action. I find that this is the most desired behavior. Normally you will check that all bindings are valid before flushing:

public void onSave() {
	if (bindings.isValid())
		bindings.flushUIToModel();
}

If you want to conditionally enable your save-action when all bindings are valid, you can hook in a lifecycle listener:

bindings.addBindingStateListener(new ChangeListener() {
    public void stateChanged(ChangeEvent e) {
        Binding binding = (Binding) e.getSource();
        setSaveEnabled(binding.isValid() && bindings.isValid());
    }
});

Your setSaveEnabled() would either just enable/disable the save button, or for example in the context of a NetBeans Platform application, enable/disable the SaveCookie in your TopComponent lookup.

PropertyChangeSupport

You can have arbitrary changes to your model objects automatically update the GUI by enabling PropertyChangeSupport, provided that your model object supports it. PropertyChangeSupport can also be enabled on a per binding basis

// Enable for all bindings
bindings.usePropertyChangeSupport(true);

// Enable for specific binding
usernameBinding.usePropertyChangeSupport(true);                        
                    

Validators

The only validator provided by default is the required validator. You can enable it on a binding like this:

// Enable the built-in required validator
usernameBinding.required();

// The explicit way, which also shows how to install custom validators
binding.validator(new RequiredValidator("Custom required-message here"));
                    

Custom validators

To write a custom validator, all you have to do is implement the simple Validator interface:

public interface Validator<T> {
    public ValidationResult validate(T o);
}
                    

Creating an email address validator could be done easily like this:

public class EmailValidator <String> {
    public static final Pattern EMAIL_PATTERN =
        Pattern.compile("^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*((\\.[A-Za-z]{2,}){1}$)");

    public ValidationResult validate(String email) {
        return EMAIL_PATTERN.matcher(field.getStringValue()).matches() ?
            ValidationResult.OK :
            ValidationResult.error("Supply a valid email address");
    }
}                        
                    

Converters

Converters are used to translate between the value displayed in the GUI field and the property of your model object. Swing Data Binding comes with converters for most popular data types, so you normally don't need to think about them. You can however override the default converter used on a per binding basis:

usernameBinding.setConverter(myCustomConverter);
                    

Custom converters

To create a converter for a custom datatype, you have to implement the Validator interface:

public interface Converter<FORWARD, REVERSE> {
    public REVERSE convertForward(FORWARD value) throws ConversionException;
    public FORWARD convertReverse(REVERSE value) throws ConversionException;
}                        
                    

You supply conversion in both directions, so that you don't need separate converter implementations for UI to Model and Model to UI. Her is the full implementation of the custom Joda Time DateTimeToDate converter:

public class DateTimeToDateConverter implements Converter<DateTime, Date> {
    public static final DateTimeToDateConverter INSTANCE = new DateTimeToDateConverter();

    public Date convertForward(DateTime value) throws ConversionException {
        return value.toDate();
    }

    public DateTime convertReverse(Date value) throws ConversionException {
        return new DateTime(value);
    }
}                    

To install your custom converter, you can register it in the ConverterRegistry explicitly, or automatically. This is how one would install the DateTimeToDate converter if it wasn't already automatically registered:

ConverterRegistry.add(DateTimeToDateConverter.INSTANCE, DateTime.class, Date.class);                        
                    

If you want to install your custom converters automatically, you can do so using the Java 6 ServiceLoader interface. Create a file in your JAR called META-INF/service/no.tornado.databinding.converter.ConverterProvider with a reference to a class that implements the ConverterProvider interface. Here is the JodaConverterProvider, which provides support for DateTimeToDate and DateTimeToString conversion by default:

public class JodaConverterProvider implements ConverterProvider {
    public List<ConverterDescription> getConverterDescriptions() {
        return Arrays.asList(
                new ConverterDescription(DateTimeToDateConverter.INSTANCE, DateTime.class, Date.class),
                new ConverterDescription(DateTimeToDateConverter.INSTANCE, DateTime.class, String.class)
        );
    }
}
                    

This works very nicely in a NetBeans Platform application as well, no manual configuration needed.

Providing feedback

Both conversion and validation can fail, and often you would want the error to be visible to the user. Swing Data Binding has a concept of a StatusMonitor which can be connected to a Binding. By default we include one implementation of the StatusMonitor interface, but it is easy to create your own, let's say to put the error messages in a status bar, or even a popup. To put status messages in a label, you can do this:

JLabel usernameMessages = new JLabel();
usernameMessages.setForeground(Color.RED);
usernameBinding.statusMonitor(JLabelStatusMonitor.create(usernameMessages));
    

Now all status messages from conversion and validation will automatically show up in the usernameMessages JLabel. A look at this implementation of StatusMonitor shows how easy it is:

public class JLabelStatusMonitor implements StatusMonitor {
    private JLabel label;

    // Constructor that takes a JLabel
    public JLabelStatusMonitor(JLabel label) {
        this.label = label;
    }

    // Convenience method to create a new StatusMonitor
    public static JLabelStatusMonitor create(JLabel label) {
        return new JLabelStatusMonitor(label);
    }

    // Called when there is no conversion/validation messages
    public void clearStatus() {
        label.setText(null);
    }

    // Called when conversion/validation state changes
    public void setStatus(String message) {
        label.setText(message);
    }
}
    

Swapping out the model object

Some times you want to change the model object that is represented in the gui. You can easily swap the model object for all or selected bindings:

// Change all bindings that uses the default model object
bindings.defaultModel(myNewUserObject);

// Create a binding that doesn't bind to the default model object
Binding usernameBinding = bindings.add(usernameField, otherModelObject, "username");

// Change the model object of this binding
usernameBinding.setModel(myNewUserObject);

// You can also swap the model for all bindings that use the same model object
bindings.changeModelObjectForBindings(oldUser, newUser)
    

This can be done even if the binding or bindings are bound at the moment, no need to even call unbind before changing the model!

Supporting custom components

A data binding framework should make it easy to use third party/custom Swing components. You shouldn't need to worry about wether your components of choice is already supported - with Swing Data Binding you create the support yourself in a matter of minutes! We use a notion of a UIBridge, which takes care of transporting the values to and from your custom component. This is the UIBridge interface:

public interface UIBridge<UI_TYPE, UI_LISTENER_TYPE, UI_VALUE_TYPE> {
    public Class<UI_TYPE> getUIClass();
    public void setUIValue(UI_TYPE component, UI_VALUE_TYPE value) throws ConversionException;
    public UI_VALUE_TYPE getUIValue(UI_TYPE component) throws ConversionException;
    public Class<? extends UI_VALUE_TYPE> getUIValueType();
    public UI_LISTENER_TYPE addValueChangeListener(UI_TYPE component, ChangeListener listener);
    public void removeValueChangelistener(UI_TYPE component, UI_LISTENER_TYPE listener);
}
    

Let's look at a real implementation with comments to understand the different methods. We use the JXDatePickerBridge in this example. This is all the code that is needed to make sure JXDatePicker works with Swing Data Binding:

        // The type parameters describe the the ui element, the listener type created to support onchange and the gui value type
        public class JXDatePickerBridge implements UIBridge<JXDatePicker, DocumentListener, Date> {
            static JXDatePickerBridge INSTANCE = new JXDatePickerBridge();
            private JXDatePickerBridge() { }

            // Return the type of the ui element
            public Class<JXDatePicker> getUIClass() {
                return JXDatePicker.class;
            }

            // Return the type of the value saved in the ui element
            public Class<? extends Date> getUIValueType() {
                return Date.class;
            }

            // Method to set the value into this component
            public void setUIValue(JXDatePicker component, Date value) throws ConversionException {
                component.setDate(value);
            }

            // Method to get the value from this component
            public Date getUIValue(JXDatePicker component) throws ConversionException {
                return component.getDate();
            }

            // Add a changelistener that should be notified about changes to the value of this component
            // Swing Data Binding comes with a wrapper for DocumentListener that automatically dispatches
            // events to the ChangeListener. Most other Swing components use the ChangeListener interface
            // so normally you would just add the ChangeListener provided to the ui elements listener list.
            public DocumentListener addValueChangeListener(JXDatePicker component, ChangeListener listener) {
                DocumentListener documentListener = new DocumentChangeListener(listener);
                component.getEditor().getDocument().addDocumentListener(documentListener);
                return documentListener;
            }

            // Method to remove the listener returned from addValueChangeListener() above. Called when
            // unbind is called on the binding.
            public void removeValueChangelistener(JXDatePicker component, DocumentListener listener) {
                component.getEditor().getDocument().removeDocumentListener(listener);
            }
        }
    

To register your custom UI bridge, you can use either an explicit call to UIBridgeRegistry.addBridge(yourBridge) or create a file called META-INF/services/no.tornado.databinding.uibridge.UIBridgeProvider with a single line containing the classname of your UIBridgeProvider. This is the UIBridgeProvider for the JXDatePicker support:

public class JXDatePickerUIBridgeProvider implements UIBridgeProvider {
    public List<UIBridgeDescription> getBridgeDescriptions() {
        return Arrays.asList(
                new UIBridgeDescription(JXDatePickerBridge.INSTANCE, JXDatePickerBridge.INSTANCE.getUIClass())
        );
    }
}
    

The second argument is simply the type of the UI class, so that Swing Data Binding will know what bridge to use for what ui element.

Convenience support classes

Swing is not big on Collections and Generics support yet. Often, you'll find yourself wanting to use for example a JComboBox backed by options from a List. Also, you need a custom renderer to display the right property in the dropdown.

        // Load list of people
        List<Person> people = Service.getPeople();

        // Create combobox
        JComboBox parentField = new JComboBox();

        // Bind the people to the options in the combobox
        parentField.setModel(new ListComboBoxModel(people));

        // Use the name property of each Person object as the list value
        parentField.setRenderer(new PropertyListCellRenderer("name"));

        // Perform the binding between the list of people and the parent field of the model object
        bindings.add(parentField, "parent");
    

If you find bugs, wants a specific converter or validator included, or have suggestions to improve Swing Data Binding, please send me an email at edvin@sysedata.no. I hope you enjoy the easiest data binding framework for Swing!