Data Binding

Data binding is the process of automatically mapping values between a set of user interface elements and an internal data representation; for example, from a order entry form to a collection of database fields or vice versa. Data binding can help simplify development by eliminating some or all of the tedious boilerplate code that often goes along with this type of programming.

In Pivot, data binding is controlled by the load() and store() methods of the Component class:

public void load(Object context) {...}
public void store(Object context) {...}

The Object value passed to these methods provides the "bind context". It may be either an implementation of the org.apache.pivot.collections.Dictionary interface or a Java bean value that can be wrapped in an instance of org.apache.pivot.beans.BeanAdapter, which implements Dictionary.

The bind context is essentially a collection of name/value pairs representing the data to which the components will be bound. Each bindable property of a component can be assigned a "bind key" that associates the property with a value in the context. Data is imported from the context into the property during a load, and exported from the property to the context during a store. Bind contexts may be flat or hierarchical; JSON path syntax can be used to retrieve nested values (e.g. "foo.bar.baz" or "foo['bar'].baz").

Many Pivot components support data binding including text inputs, checkboxes and radio buttons, list views, and table views, among others. Some components support binding to multiple properties; for example, a caller can bind to both the list data as well as the selection state of a ListView component.

Data Binding in Stock Tracker

The Stock Tracker demo uses data binding to populate the fields in the quote detail view. The bind context is actually the row data retrieved from the web query for the selected stock. This is why the application requests more data than it seems to need from the GET query: the extra fields are used to fill in the data in the detail form.

The bound components, in this case, are labels - Stock Tracker maps values from the retrieved quote data to the text property of each. The name of the key to use for each label is specified via the "textKey" property:

            
            <Form styles="{padding:0, fill:true, showFlagIcons:false, showFlagHighlight:false,
                leftAlignLabels:true}">
                <Form.Section>
                    <bxml:define>
                        <stocktracker:ValueMapping bxml:id="valueMapping"/>
                        <stocktracker:ChangeMapping bxml:id="changeMapping"/>
                        <stocktracker:VolumeMapping bxml:id="volumeMapping"/>
                    </bxml:define>

                    <Label bxml:id="valueLabel" Form.label="%value"
                        textKey="value" textBindMapping="$valueMapping"
                        styles="{horizontalAlignment:'right'}"/>
                    <Label bxml:id="changeLabel" Form.label="%change"
                        textKey="change" textBindMapping="$changeMapping"
                        styles="{horizontalAlignment:'right'}"/>
                    <Label bxml:id="openingValueLabel" Form.label="%openingValue"
                        textKey="openingValue" textBindMapping="$valueMapping"
                        styles="{horizontalAlignment:'right'}"/>
                    <Label bxml:id="highValueLabel" Form.label="%highValue"
                        textKey="highValue" textBindMapping="$valueMapping"
                        styles="{horizontalAlignment:'right'}"/>
                    <Label bxml:id="lowValueLabel" Form.label="%lowValue"
                        textKey="lowValue" textBindMapping="$valueMapping"
                        styles="{horizontalAlignment:'right'}"/>
                    <Label bxml:id="volumeLabel" Form.label="%volume"
                        textKey="volume" textBindMapping="$volumeMapping"
                        styles="{horizontalAlignment:'right'}"/>
                </Form.Section>
            </Form>
            
        

The actual binding occurs when the selection changes in the table view; the selection change handler calls the refreshDetail() method in response to a selection change event:

            
            @SuppressWarnings("unchecked")
            private void refreshDetail() {
                StockQuote stockQuote = null;

                int firstSelectedIndex = stocksTableView.getFirstSelectedIndex();
                if (firstSelectedIndex != -1) {
                    int lastSelectedIndex = stocksTableView.getLastSelectedIndex();

                    if (firstSelectedIndex == lastSelectedIndex) {
                        List<StockQuote> tableData = (List<StockQuote>)stocksTableView.getTableData();
                        stockQuote = tableData.get(firstSelectedIndex);
                    } else {
                        stockQuote = new StockQuote();
                    }
                } else {
                    stockQuote = new StockQuote();
                }

                detailPane.load(new BeanAdapter(stockQuote));
            }
            
        

Note that the load() method is actually called on the detail pane itself rather than on the parent container of the detail labels (an instance of Form). This is because the application also needs to bind to the label that contains the company name, which is not a child of the Form:

            
            <Label textKey="companyName" styles="{font:{size:12, bold:true}}"/>
            
        

Bind Mappings

Also note the use of the "textBindMapping" attributes. Bind mappings allow a caller to modify the data during the bind process. Incoming data can be converted before it is assigned to a property, and outgoing data can be converted before it is stored in the bind context. For example, the ValueMapping class formats incoming data as a currency value in U.S. dollars:

            
            public class ValueMapping implements Label.TextBindMapping {
                private static final DecimalFormat FORMAT = new DecimalFormat("$0.00");

                @Override
                public String toString(Object value) {
                    return Float.isNaN((Float)value) ? null : FORMAT.format(value);
                }

                @Override
                public Object valueOf(String text) {
                    throw new UnsupportedOperationException();
                }
            }
            
        

The toString() method is called to perform the translation during a load() operation. The valueOf() method would be called during store(), but throws UnsupportedOperationException because store() is never called by the Stock Tracker application.

Next: Localization