Wednesday, July 14, 2010

Apache Click 2.2.0: DataProvider

In this three part series I'll blog about some of the new features added in 2.2.0, namely:

  1. DataProviders (Japanese version)
  2. Explicit binding (Japanese version)
  3. Dynamic Form Validation ( Japanese version)
    In this first installment I'll cover DataProviders and how they simplify Click Page implementations.

    To understand the benefits of DataProviders, we need to look at the problem they are trying to solve. To start off with let's do a quick recap of the major life cycle events of a Click Page. Below we list the events in the order they are executed:
    • <<init>>: page is created
    • onInit: page controls are created and added to the page
    • onProcess: control values are bound to request parameters, control values are validated and action listeners are fired
    • onRender: database intensive operations are performed here
    Click allows certain events to be skipped depending on the outcome of a previous event. onRender for example, will be skipped if a Control action returns false. This together with the fact that onRender is the last event exposed by Click makes onRender ideal for database intensive operations. If a control action listener decides to redirect to a different page, it can return false to bypass the slow onRender method.

    For example:
    public class CustomerPage extends BorderPage {
    
        private Table table = new Table("table");
        private ActionLink editLink = new EditLink("edit");
    
        @Override
        public void onInit() {
            super.onInit();
    
            table.add(new Column("firstname"));
    
            ...
    
            editLink.setActionListener(new ActionListener() {
                public boolean onAction(Control source) {
                    Map params = Collections.singletonMap("id", editLink.getValue());
    
                   // Redirect to edit customer page and pass the selected customer ID
                   setRedirect(EditCustomerPage.class, params);
                   return false;
                }
            });
        }
    
        @Override
        public void onRender() {
            // Database intensive operation: retrieving all customers from the database
            List<Customer> customers = getCustomerService.getCustomers();
            table.setRows(customers);
        }
    }
    
    In the example above you can see that we don't want to hit the database and retrieve all the customers only to be redirected to another page that does not render the customers we retrieved. In other words, because the customers won't be rendered by the page we redirect to, we don't want to pay for the database hit. Since the onRender event is skipped if an action listener returns false, it makes sense to place database intensive code there.

    Inconsistent

    Unfortunately this pattern cannot be applied to all controls. Some controls need their values populated before the onProcess event, either for validation or parameter binding purposes. For example, the Select control validation depends on its values to determines whether or not a valid selection was made. FormTable needs to have it's rows set  in order to update its entity values against incoming request parameters.

    Having some of the controls populated in onInit and others in onRender is inconsistent and a common pitfall for new users as they have to figure out which controls should be populated in which event.

    DataProvider

    The solution used in Click was to add a DataProvider interface to enable on demand data loading. A DataProvider has a single method, "public List getData()", that the control can invoke when it needs it's data. For example, a Table will invoke getData when it is rendered, a Select will invoke getData when it is validated and a FormTable will invoke getData when it is processed.

    DataProviders leads to a more consistent Page implementation where all control creation logic can be encapsulated in the onInit event or even the page constructor. For example:

    public class CustomerPage extends BorderPage {
    
        private Table table = new Table("table");
        private Select select = new Select("markets");
    
        @Override
        public void onInit() {
            super.onInit();
    
            table.add(new Column("firstname"));
    
            ...
    
            table.setDataProvider(new DataProvider() {
                public List<Customer> getData() {
                    return getCustomerService().getCustomers();
                }
            });
    
            select.setDataProvider(new DataProvider() {
                public List<Option> getData() {
                    List options = new ArrayList();
                    for (Market market : getMarketService().getMarkets()) {
                        options.add(new Option(market.getId(), market.getName());
                    }
                    return options;
                }
            });
        }
    }
    
    In the example above we use DataProviders for both the Table and Select control. The Table data is only retrieved when the Table is rendered, so in the event of a redirect, the database operation is skipped. The Page is now consistent as all control setup logic is placed inside the onInit event.

    Is onRender still necessary?

    Absolutely. While DataProviders allow control creation logic to be encapsulated within the onInit event, not all data needs to be represented with controls. The Page template (Velocity) for example can be used to render markup for the customers passed from the page. onRender could still be used to retrieve the customers and make it available to the template. For example:

    public class CustomerPage extends BorderPage {
    
        public void onRender() {
            addModel("customers", getCustomerService().getCustomers());
        }
    } 

    <table>
    ...
    
    #foreach($customer in $customers)
      <tr>
        <td>
          $customer.name
        <td>
        <td>
          $customer.holdings
        <td>
      </tr>
    #end
    </table> 

    If an action listener returns false, onRender is skipped and the database operation won't be performed, which is exactly the behavior we want.

    Hope this overview was helpful. In the next installment I'll cover Explicit binding and Dynamic Form Validation, which both simplifies dynamic form creation.

    No comments: