Elastic Path Commerce Development

Data Binding

Data Binding

Commerce Manager uses the Eclipse Data Binding Framework (DBF) to bind the UI controls to the underlying model objects. The binding framework (along with our enhancements) can handle data validation, conversion to model datatypes, and display/clearing of messages related to data entry errors. The main interface is com.elasticpath.cmclient.core.binding.EpControlBindingProvider.

When you have a new control that you want to bind to a model object, you need a DataBindingContext within which to bind the control. Typically, one editor, one dialog box, or one wizard page will have one DataBindingContext, and once the Context has no validation errors then the page is considered to be in a state that can be persisted.

Elastic Path Commerce enhances the data binding framework so that when a control is bound to the model, eclipse FieldAssist Decorators are used to display validation errors in the UI next to the field which is failing validation.

The following sections will review the basics of databinding for different controls, and then how to use databinding in Wizard Pages, Dialog Boxes, and Editors, since the architecture to setup binding in each is slightly different.

Where to bind controls in different top-level composites

Binding in Wizard Pages

Typically when creating a WizardPage, you follow a process of creating controls, populating the controls, and then binding the controls. Right after binding the controls, you need to make a call to the WizardPageSupport class' creation factory method. This uses the Decorator pattern to add data binding support to your wizard page, which will automatically ensure that the "Finish" and "Next" buttons in the wizard are not enabled unless and until all the validation errors in the page's DataBindingContext are resolved.

By default, WizardPages show any validation messages just under the title (one at a time, the most recent is always the one displayed). Since we're using field decorations instead, you must override the WizardPage method setErrorMessage(String errorMessage) to do nothing rather than setting the error text in the page header area.

Binding in Dialog Boxes

Data binding in an implementation of AbstractEpDialog is similar to data binding within a WizardPage, but you need to make call to the EpDialogSupport class' create() method after you bind your controls.

Binding in Editors

Typically an editor will contain Managed Forms containing SectionPart s which are implementations of AbstractCmClientEditorPageSectionPart. This class and its supers require that you override the abstract bindControls() method, within which you will use the EpControlBindingProvider as usual to bind your controls.

How Databinding Works

The Data binding framework provides support for binding individual Value s and for binding List s of objects. Since we currently don't have any code that uses the List support, this document will focus on the DBF support for Value binding.

EpControlBindingProvider API

There are three API methods that will be used most often, and the one you need to use depends on how your control should be bound to your model.

All of the following returns a EpValueBinding object containing the newly created Binding, and the ControlDecoration used in the binding. This allows bindings to be added and removed at runtime.

bind(DataBindingContext, control, target, fieldname)

The first method is the most basic, and should be used when you are not concerned with validating the input from your control. Perhaps your control is a Button which can only have binary states, so there's no point in validating or converting input.

bind(DataBindingContext, control, target, fieldname, validator, converter, hideDecorationOnFirstValidation)

This second method will be used when you need to bind your UI control's content directly to a field on your Model (target) object. If you require validation and/or conversion, you must provide a validator and a converter so that the binding framework knows how to validate the input and convert the Control's data format (typically a String) into the model's data format (if also a String then no conversion is required, but it might be e.g. an int). If either the Converter or the Validator are not provided, it will be assumed that they are not required.

The last argument is special. At the moment when a control is bound to a model object, its validation routines are called. However, if the control is being created with no data in it (e.g. your form is for creating a brand new Model object that has never been persisted), then you don't want the validation error decoration to be displayed on the first validation pass and this should be true.

bind(DataBindingContext, control, validator, converter, updateStrategy, hideDecorationOnFirstValidation)

This third method is the most complex, and is used when your control's value cannot be bound directly to the model object's field but must perform some extra processing to ensure that the model value is properly set. It is often used in such cases as when the model field that must be set depends on the selected dialog box Locale. For example, if you have a dialog box for entering a "Product Name" that allows the user to select the Locale that is currently being edited, then there does not exist a one-to-one mapping between the Control and a Model field called "productName"; you might be editing the French product name or you might be editing the English product name. In such cases you would write a custom ObservableUpdateValueStrategy implementation, overriding the doSet() method to call the appropriate setter method on the model object.

removeEpValueBinding(DatabindingContext context, EpValueBinding binding)

Although you are not required to explicitly remove binding on a control when your composite is closed, you might need to remove a binding depending on user input and when it's appropriate for a model should cease to be updated. This method will remove the EpValueBinding from the DataBindingContext, stop validation on the field and its decorations will be hidden. The aggregate validation status will also be affected automatically.

Detailed databinding architecture

Ultimately, to bind a Control to a Model object's field the DataBindingContext requires four things:

  1. TargetObservableValue - an IObservableValue implementation, typically the UI widget that you want to bind to. You must provide this to the EpControlBindingProvider.
  2. ModelObservableValue - an IObservableValue implementation of a field in the bean representing the model object that you want to bind to. If you pass in the model object and the name of the field to which you want to bind, the EpControlBindingProvider will actually construct this for you using the DBF BeansObservables factory; however, if you cannot map your control's value directly to a model object's field (e.g. you have a control whose value depends on the current locale), then don't provide the target and field name. In that case, the EpControlBindingProvider will fake one for you.
  3. TargetToModel UpdateStrategy - a class that tells the DBF how to validate and convert the given Control's value so that it's suitable for setting on the Model object. The EpControlBindingProvider usually creates a default one and sets the given validator and converter, but you can provide your own of required.
  4. ModelToTarget UpdateStrategy - a class that tells the DBF how to validate and convert the given Model object's field so that it's suitable for setting in the Control. With the Commerce Manager, we do not use the DBF to update the UI based on changes in the Model (we handle this manually), so the EpControlBindingProvider will use a statically defined POLICY_NEVER to indicate that this should never happen.

The UpdateValueStrategy required by the DBF has been enhanced through creation of an ObservableUpdateValueStrategy. The enhancement allows an UpdateStrategy to be observed so that when validation is performed on a Control's input then listeners can be informed of the new Validation status of the control.

Determining aggregate validation status

Determining the aggregate validation status of an entire Databinding Context is usually performed by the EP framework code, but on the rare occasion when custom code must perform such a task you have a couple of options:

  1. Call getBindings() on the DBC to get a list of all the bindings and loop through them all, checking the validation status on each Binding. Not recommended.
  2. Use the AggregateValidationStatus class that is part of the DBF. An example follows.

The following code was pulled from EpDialogSupport. You can see that it not only uses the AggregateValidationStatus to determine the validation status of a DBC, but it also adds a change listener to the aggregate status to perform an action when the status changes:

                     com.elasticpath.cmclient.core/src/main/java/com/elasticpath/cmclient/core/binding/EpDialogSupport.java
aggregateStatus = new AggregateValidationStatus(dbc.getBindings(), AggregateValidationStatus.MAX_SEVERITY);
aggregateStatus.addValueChangeListener(new IValueChangeListener() {
	public void handleValueChange(final ValueChangeEvent event) {
		currentStatus = (IStatus) event.diff.getNewValue();
		handleStatusChanged();
                  

Binding different types of Controls (examples)

The types of controls currently supported by the databinding framework are:

  • org.eclipse.swt.widgets.Text
  • org.eclipse.swt.widgets.Combo and org.eclipse.swt.custom.CCombo
  • org.eclipse.swt.widgets.Button
  • org.eclipse.swt.widgets.List

Our implementation of the ComboBox binding assumes that ComboBoxes only have one object at a time selected, but the underlying framework does not have such a restriction.

Lists are bound in a different manner than the other controls due to the fact that they can have multiple objects selected in them. Typically, listboxes are used only to select existing instances of an object, so the use of a validation and binding framework is not strictly necessary and can usually be avoided. As of this writing we do not bind any listboxes in the Commerce Manager.

Text boxes

//create
Text usernameTextbox = new Text();
//populate
usernameTextbox.setText("myUsername");
//bind
EpValueBinding userNameBinding =
  EpControlBindingProvider.getInstance().bind(
       myDataBindingContext,
       usernameTextbox,
       myUser,
       "username",
       EpValidatorFactory.STRING_255,
       null,
       hideDecorationOnFirstValidation);

...
//Remove Binding
EpControlBindingProvider.removeEpValueBinding(myDataBindingContext, userNameBinding );

Combo boxes

Binding a combo box is a bit tricky because the combo box usually has an index that doesn't correspond to the value that must be assigned to the model object's value. In this case you need to use a custom UpdateStrategy that tells the databinding framework how to translate the selected combobox index into the value that the model object is expecting. Validation is not usually required in these cases because combo boxes are not usually editable, and the converter is also not required because all the work can be done in the UpdateStrategy. The following code snippet shows how you might do this:

                  com.elasticpath.cmclient.fulfillment/src/main/java/com/elasticpath/cmclient/fulfillment/editors/customer/CustomerDetailsProfileBasicSection.java
bindingProvider.bind(bindingContext, this.statusCombo, null, null, new ObservableUpdateValueStrategy() {
	@Override
	protected IStatus doSet(final IObservableValue observableValue, final Object value) {
		int status;
		final int selectionIndex = (Integer) value;
		switch (selectionIndex) {
		case 0:
			status = Customer.STATUS_ACTIVE;
			break;
		case 1:
			status = Customer.STATUS_DISABLED;
			break;
		default:
			return new Status(IStatus.WARNING, FulfillmentPlugin.PLUGIN_ID, "Can not set the customer status."); //$NON-NLS-1$
		}
		customer.setStatus(status);
		return Status.OK_STATUS;
	}

}, true);
               

Buttons

Radio Buttons and CheckBox buttons often need to be bound. Nothing special has to be done for these controls because they don't need to be validated or converted; they're typically either true or false.

EpControlBindingProvider.getInstance().bind(
					getBindingContext(),
					this.cmUserButton,
					UserDetailsPage.this.getModel(),
					"cmAccess"); //$NON-NLS-1$

List boxes

We do not currently bind listboxes.

Two-way databinding

Adding property change support

The model object being bound must implement the property change listener methods as per the JavaBean specification. Additionally, any property you wish to create a model to ui binding for needs to trigger a property change event when its setter is called.

To get the basic property change methods you can extend the class AbstractListenableEntityImpl (for objects representing domain entities with guids) or AbstractListenableValueObjectImpl (for other domain objects). To trigger the property change events you need to modify your setters like in the example below.

                  core/ep-core/src/main/java/com/elasticpath/domain/order/impl/AbstractOrderShipmentImpl.java
protected void setItemSubtotal(final BigDecimal itemSubtotal) {
	BigDecimal oldItemSubtotal = this.itemSubtotal;
	this.itemSubtotal = itemSubtotal;
	firePropertyChange("itemSubtotal", oldItemSubtotal, itemSubtotal); //$NON-NLS-1$
}
               

Binding configuration

If you require two-way databinding (ui control to model as well as model to ui control) then you need to use the EpBindingConfiguration object to specify your binding settings. This is then passed into the EpControlBindingProvider which handles the actual binding. This method can also be used to create a one-way ui to model binding or a one-way model to ui binding. There are a couple different constructors for the EpBindingConfiguration object depending on whether you need to use a custom update strategy or not. The most common way of creating a new EpBindingConfiguration is as follows:

EpBindingConfiguration bindingConfig =
		new EpBindingConfiguration(
			bindingContext,
			control,
			model,
			"fieldName"); //$NON-NLS-1$

After this you are required to call at least one of the configure*() methods before passing it into the EpControlBindingProvider.

Configure UI to Model binding

To add ui to model binding to your binding configuration you need to call one of the configureUiToModelBinding(...) methods on it. The most commonly used one, which takes a converter and validator, is as follows:

bindingConfig.configureUiToModelBinding(
			new EpStringToBigDecimalConverter(),
			EpValidatorFactory.BIG_DECIMAL,
			false);

Configure Model to UI binding

To add model to ui binding to your binding configuration you need to call the configureModelToUiBinding(...) method on it as follows:

bindingConfig.configureModelToUiBinding(
			new EpBigDecimalToStringConverter(),
			UpdatePolicy.UPDATE);

Perform the binding

Once your binding configuration is complete, you can set the binding as follows:

EpControlBindingProvider.getInstance().bind(bindingConfig);

Examples

final EpControlBindingProvider bindingProvider = EpControlBindingProvider.getInstance();

/**
 * Example 1: one-way model to ui binding
 *
 * Use this when all of the following are true:
 *    - you have an uneditable control displaying a property of the model object
 *    - the value of that property in the model could be changed by some other event
 *    - you require the control to update itself when that property is changed
 */
EpBindingConfiguration bindingConfig =
		new EpBindingConfiguration(
			bindingContext,
			itemSubTotalText,
			shipment,
			"itemSubtotal"); //$NON-NLS-1$
bindingConfig.configureModelToUiBinding(
			new EpBigDecimalToStringConverter(),
			UpdatePolicy.UPDATE);
bindingProvider.bind(bindingConfig);

/**
 * Example 2: two-way binding
 *
 * Use this when all of the following are true:
 *    - you have an editable control displaying a property of the model object
 *    - you require the model to be updated when the control is modified
 *    - the value of that property in the model could be changed by some other event
 *    - you require the control to update itself when that property is changed
 */
bindingConfig = new EpBindingConfiguration(
			bindingContext,
			shippingCostText,
			shipment,
			"shippingCost"); //$NON-NLS-1$
bindingConfig.configureUiToModelBinding(
			new EpStringToBigDecimalConverter(),
			EpValidatorFactory.BIG_DECIMAL,
			false);
bindingConfig.configureModelToUiBinding(
			new EpBigDecimalToStringConverter(),
			UpdatePolicy.UPDATE);
bindingProvider.bind(bindingConfig);