Tuesday, April 23, 2013

Databinding with radio buttons and contextual binding contexts

The Eclipse databinding framework is a powerful framework that help developers link (bind) their data model to graphical components. It basically provides a way to reflect a user interaction with some internal data (and vice versa) without the use of painful listeners.
It also provide an easy way to validate and convert the data between the graphical interface and the internal java model.

The eclipse databinding framework provides a rich set of classes to bind SWT and JFace graphical components on one side and Java Beans and Pojos on the model side. EMF is also a very good candidate  for data binding as it provides the modification notifications required for the data binding process.

In this post I will not explain the basics of data binding (that has been done perfectly by others here and here for example), but I will talk about a particular use case that some of you may encounter.

The idea is to make wizard with multiple choices to select a way to retrieve data.
Here is the look of the wizard page.


First you need a model to represent those data, there are many ways but here is one.

public class FeatureRepositories {

    public static enum POLICY {
        DEFAULT_REPO,
        REMOTE_REPO,
        LOCAL_FOLDER
    }

    private POLICY policy;

    private String remoteRepoUriStr;

    private String localRepoPathStr;
...

This is a Plain Old Java Object (pojo) with 3 fields and their associated setters and getters (not shown here). This is not a Java Beans meaning I did not implement any property change notifications as I do not require the model to change from outside a user interaction.

Radio button binding

So the first problem is how to bind the policy enum to the radio button selection ? I had trouble finding that information over the net so here to me the most elegant solution using : org.eclipse.core.databinding.observable.value.SelectObservableValue.


//Create the Select Observable for our enum type
SelectObservableValue repoPolicyObservable = new SelectObservableValue(FeatureRepositories.POLICY.class);
//bind the default radion button selection to the right enum value
IObservableValue btnDefaultRemoteSitesObserveSelection = SWTObservables
            .observeSelection(btnDefaultRemoteSites);
repoPolicyObservable.addOption(POLICY.DEFAULT_REPO, btnDefaultRemoteSitesObserveSelection);

// bind remote custom Repo button selection to the right enum value
IObservableValue btnCustomUpdateSiteObserveSelection = SWTObservables
        .observeSelection(btnCustomUpdateSite);
repoPolicyObservable.addOption(POLICY.REMOTE_REPO, btnCustomUpdateSiteObserveSelection);

// bind local folder button selection to the right enum value
IObservableValue btnLocalFolderObserveSelection = SWTObservables.observeSelection(btnLocalFolder);
repoPolicyObservable.addOption(POLICY.LOCAL_FOLDER, btnLocalFolderObserveSelection);

//finally bind the selectable to the model attribute
bc.bindValue(repoPolicyObservable,
            PojoObservables.observeValue(updateWizardModel, "featureRepositories.policy")); //$NON-NLS-1$



The updateWizardModel is an instance of the wizard model that contains a field of the type FeatureRepositories that defines our page model.
public class UpdateWizardModel {

    private FeatureRepositories featureRepositories;
    public FeatureRepositories getFeatureRepositories() {
        return this.featureRepositories;
    }

    public void setFeatureRepositories(FeatureRepositories featureRepositories) {
        this.featureRepositories = featureRepositories;
    }

}
This way you can see how the binding is done using a PojoObservable (only one way binding from GUI to model) and using a nested property "featureRepositories.policy"
Please also note that the code above does not show the binding done to enable text fields and buttons according to the selected radio button, I leave it to the readers hands. 


Error notifications

As mentioned in the introduction, databinding also provides means to validate the data before or after it gets propagated to the model.
So in our example we would like to check that the path is a valid existing folder or the url is a valid one, if not we would like the wizard to say so, like this.


The validation is a basic databinding feature, here is how to do it :

UpdateValueStrategy localFolderValidator = new UpdateValueStrategy().setAfterConvertValidator(new UpdateWizardModel.LocalRepoFolderValidator());
IObservableValue localFolderTextObserveTextObserveWidget = SWTObservables.observeText(localFolderText, SWT.Modify);
bc.bindValue(localFolderTextObserveTextObserveWidget, PojoObservables.observeValue(updateWizardModel, "featureRepositories.localRepoPathStr"), localFolderValidator, null);

where the validator implementation looks like this
static class LocalRepoFolderValidator implements IValidator {
    @Override
    public IStatus validate(Object value) {
        String uriStr = (String) value;
        if (uriStr == null || "".equals(uriStr)) { //$NON-NLS-1$
            return ValidationStatus.info(Messages.getString("UpdateWizardModel.local.folder.required.error")); //$NON-NLS-1$
        }
        File folder = new File(uriStr);
        if (!folder.exists()) {
            return ValidationStatus.error(Messages.getString("UpdateWizardModel.local.folder.must.exist.error")); //$NON-NLS-1$
        }
        if (!folder.isDirectory()) {
            return ValidationStatus.error(Messages.getString("UpdateWizardModel.local.folder.must.be.folder")); //$NON-NLS-1$
        }
        return ValidationStatus.ok();
    }
}


One of many great thing with data binding is support for JFaces components such as Dialogs, Trees, Lists and Wizards. And it provides a way to bind the validation status messages to the Wizard message field with one single line of code
// bind the validation messages to the wizard page
WizardPageSupport.create(this, bc);

Where bc is the data binding context used from the beginning of this post and this is the wizard page.
You'll also notice that the WizardPageSupport handle the page completion that will be enable or disable the next or finish button according to the current binding status.


Issues with a single DataBindindContext 

Now if you implement the 3 radio buttons with all the previous bindings, and validation and error support using a single DataBindingContext  you'll notice that the error message is not reflecting the current radio selection but rather always reflects the first error message in the page.

To get around this expected problem you need to create a new DataBindingContext for each radio button and their associated components. You have to consider each radio button group as a new context for messages, and only this context will be able to represent the error state of the page.

As for the SelectObservableValue that observes the radion button selection to update the model according to the selection, it has to be bound to yet another DataBindingContext.
So here is the final code :

SelectObservableValue featureRepoPolicyObservable = new SelectObservableValue(FeatureRepositories.POLICY.class);
// define default Repo bindings
{
    DataBindingContext defaultRemoteBC = new DataBindingContext();
    IObservableValue btnDefaultRemoteSitesObserveSelection = SWTObservables.observeSelection(btnDefaultRemoteSites);
    featureRepoPolicyObservable.addOption(POLICY.DEFAULT_REPO, btnDefaultRemoteSitesObserveSelection);

    // fake binding to trigger the validation of the defaultRemoteBC to reset the validation message
    defaultRemoteBC.bindValue(btnDefaultRemoteSitesObserveSelection, new WritableValue());
    // bind the validation messages to the wizard page
    WizardPageSupport.create(this, defaultRemoteBC);
}
// define remote custom Repo url bindings
{
    DataBindingContext remoteRepoBC = new DataBindingContext();
    // bind selection to model value
    IObservableValue btnCustomUpdateSiteObserveSelection = SWTObservables.observeSelection(btnCustomUpdateSite);
    featureRepoPolicyObservable.addOption(POLICY.REMOTE_REPO, btnCustomUpdateSiteObserveSelection);

    // bind selection to enable text field
    IObservableValue textObserveEnabled = SWTObservables.observeEnabled(CustomSiteText);
    remoteRepoBC.bindValue(textObserveEnabled, btnCustomUpdateSiteObserveSelection, null, null);
    // bind text modification to model with validation
    UpdateValueStrategy afterConvertRemoteRepoValidator = new UpdateValueStrategy()
            .setAfterConvertValidator(new UpdateWizardModel.RemoteRepoURIValidator());
    IObservableValue customSiteTextObserveText = SWTObservables.observeText(CustomSiteText, SWT.Modify);
    remoteRepoBC.bindValue(customSiteTextObserveText,
            PojoObservables.observeValue(updateWizardModel, "featureRepositories.remoteRepoUriStr"), //$NON-NLS-1$
            afterConvertRemoteRepoValidator, null);
    // bind the validation messages to the wizard page
    WizardPageSupport.create(this, remoteRepoBC);
}
// define local folder Repo bindings
{
    DataBindingContext localRepoBC = new DataBindingContext();
    // bind selection to model
    IObservableValue btnLocalFolderObserveSelection = SWTObservables.observeSelection(btnLocalFolder);
    featureRepoPolicyObservable.addOption(POLICY.LOCAL_FOLDER, btnLocalFolderObserveSelection);

    // bind selection to text fiedl enabled
    IObservableValue localFolderTextObserveEnabled = SWTObservables.observeEnabled(localFolderText);
    localRepoBC.bindValue(localFolderTextObserveEnabled, btnLocalFolderObserveSelection, null, null);
    // bind selection to browse button enabled
    IObservableValue localFolderBrowseButtonObserveEnabled = SWTObservables.observeEnabled(localFolderBrowseButton);
    localRepoBC.bindValue(localFolderBrowseButtonObserveEnabled, btnLocalFolderObserveSelection, null, null);
    // bind text field to model with validation
    UpdateValueStrategy afterConvertLocalFolderValidator = new UpdateValueStrategy()
            .setAfterConvertValidator(new UpdateWizardModel.LocalRepoFolderValidator());
    IObservableValue localFolderTextObserveText = SWTObservables.observeText(localFolderText, SWT.Modify);
    localRepoBC.bindValue(localFolderTextObserveText,
            PojoObservables.observeValue(updateWizardModel, "featureRepositories.localRepoPathStr"), //$NON-NLS-1$
            afterConvertLocalFolderValidator, null);
    //
    // bind the validation messages to the wizard page
    WizardPageSupport.create(this, localRepoBC);
}
DataBindingContext radioBC = new DataBindingContext();
radioBC.bindValue(featureRepoPolicyObservable,
        PojoObservables.observeValue(updateWizardModel, "featureRepositories.policy")); //$NON-NLS-1$

Also notice the fake databinding used to reset the error message on the first button selection because this button does not trigger any validation.

So that is about it for Databinding.
To me this is a great framework that really reduce the use of listeners that are to error prone hence increase the code quality and robustness.

No comments:

Post a Comment