GWT Tutorial RequestFactory, Activities and Places, Editors, Cell Table/List, and Twitter Bootstrap - Part 3

In Part 2 of this tutorial we covered the RequestFactory, reusable view plumbing, activities and places, and working with cell widgets. In this final part we're going to dig into the GWT editor framework and validation, create an abstract reusable editor with built in validation styling and integrate it with the RequestFactory persistence API. We'll also cover CellTables and create a table driven editor for our invoices.

Source Code Available here on GitHub

We left off with wiring up the add customer button so let's start there, as you recall we added an implementation of the CustomerAddEventHandler interface to the AceevoBooksActivityMapper, the handler tells the place controller to go to the ViewCustomer.java place

AceevoBooksActivityMapper.java

...
eventBus.addHandler(CustomerAddEvent.TYPE, new CustomerAddEventHandler() {

            @Override
            public void onCustomerAdd(CustomerAddEvent customerAddEvent) {
                ViewCustomer viewCustomer = new ViewCustomer();
                placeController.goTo(viewCustomer);
            }
        });

Detailed Customer View and Editor


Our detailed customer view and editor has 3 main components, the left hand customer editor, and the right hand master/detail customer invoice table list and invoice editor.


Laying out the Customer View

Once again we'll use uiBinder to create our view and we're going to break the components into several difference panels that we'll reference in our CustomerViewImpl.ui.xml. Here's what our package structure looks like.

CustomerView.java, CustomerViewImpl.java and CustomerViewImpl.ui.xml

The CustomerView.java Interface is pretty simple it has 2 methods, setInput and getInvoiceTable. The setInput method is the starting point for driving the entire view (more on that later).

package com.aceevo.example.aceevobooks.client.customer.view;

import com.aceevo.example.aceevobooks.client.model.CustomerProxy;
import com.aceevo.example.aceevobooks.client.model.InvoiceProxy;
import com.aceevo.example.aceevobooks.client.view.AceevoBooksView;
import com.aceevo.example.aceevobooks.client.view.BreadCrumbView;
import com.google.gwt.place.shared.PlaceController;
import com.google.gwt.view.client.HasData;
import com.google.web.bindery.requestfactory.shared.RequestContext;

public interface CustomerView extends AceevoBooksView, BreadCrumbView {

    void setInput(CustomerProxy customerProxy, RequestContext requestContext,
            PlaceController placeController);

    HasData<InvoiceProxy> getInvoiceTable();
}
<strong>CustomerViewImpl.ui.xml</strong>

<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
    xmlns:g='urn:import:com.google.gwt.user.client.ui' xmlns:c="urn:import:com.google.gwt.user.cellview.client"
    xmlns:e="urn:import:com.google.gwt.editor.ui.client" xmlns:my="urn:import:com.aceevo.example.aceevobooks.client.view"
    xmlns:myEditor="urn:import:com.aceevo.example.aceevobooks.client.customer.view"
    xmlns:invEditor="urn:import:com.aceevo.example.aceevobooks.client.invoice.view">

    <ui:style>
        .cellTable {
            border-bottom: 1px solid #ccc;
            text-align: left;
            margin: 4px;
        }
        
        .cellTable th {
            text-shadow: none;
        }
    </ui:style>

    <g:HTMLPanel>
        <my:HeaderView ui:field="headerView"></my:HeaderView>
        <div class="mainWrapper">
            <div class="customerViewWrapper">
                <div class="h2Wrapper">
                    <h2>Contact Information</h2>
                    <g:Button ui:field="deleteCustomer" styleName="btn bt34">
                        Delete
                    </g:Button>
                    <g:Button ui:field="editCustomer" styleName="btn bt34">
                        Edit
                    </g:Button>
                    <div class="clearfix" />
                </div>
                <myEditor:CustomerEditor ui:field="customerEditor"></myEditor:CustomerEditor>
                <div class="clearfix" />
            </div>
            <div class="summaryWrapper">
                <div class="h2Wrapper">
                    <h2>All Invoices</h2>
                    <g:Button ui:field="addInvoice" styleName="btn">
                        New Invoice
                    </g:Button>
                    <div class="clearfix" />
                </div>
                <div style="float: right">
                    <g:Anchor ui:field="allInvoicesFilter">all</g:Anchor>
                    <g:Anchor ui:field="paidInvoicesFilter">| paid </g:Anchor>
                    <g:Anchor ui:field="outstandingInvoicesFilter">| outstanding</g:Anchor>
                    <div class="clearfix"></div>
                </div>
                <c:CellTable addStyleNames='{style.cellTable}' pageSize='15'
                    ui:field='invoiceTable' />
                <div class="clearfix" />

                <invEditor:InvoiceEditor ui:field="invoiceEditor"></invEditor:InvoiceEditor>

            </div>
            <div class="clearfix"></div>
        </div>

    </g:HTMLPanel>
</ui:UiBinder>

Understanding the CustomerView Implementation

Let's take a detailed look at the CustomerViewImpl.java. We begin by defining our @UiField's and Injecting our EventBus and PlaceController, we then instantiate our view using uiBinder. Next we set up the databinding betweeen our InvoiceProxy Object and the invoiceTable and define the columns.

public class CustomerViewImpl extends AbstractAceevoBooksPage implements CustomerView {

    @UiField
    CustomerEditor customerEditor;

    @Inject
    EventBus eventBus;

    @Inject
    PlaceController placeController;

    @UiField
    Button editCustomer;

    @UiField
    Button deleteCustomer;

    @UiField
    Button addInvoice;

    @UiField
    CellTable<InvoiceProxy> invoiceTable;

    @UiField
    Anchor paidInvoicesFilter;

    @UiField
    Anchor allInvoicesFilter;

    @UiField
    Anchor outstandingInvoicesFilter;

    @UiField
    InvoiceEditor invoiceEditor;

    private CustomerProxy customerProxy;

    interface CustomerViewUiBinder extends UiBinder<HTMLPanel, CustomerViewImpl> {

    }

    private static CustomerViewUiBinder customerViewUiBinder = GWT
            .create(CustomerViewUiBinder.class);

    public CustomerViewImpl() {
        initWidget(customerViewUiBinder.createAndBindUi(this));

        TextColumn<InvoiceProxy> invoiceStateColumn = new TextColumn<InvoiceProxy>() {
            @Override
            public String getValue(InvoiceProxy invoiceProxy) {
                return invoiceProxy.getInvoiceState().toString();
            }
        };

        TextColumn<InvoiceProxy> descriptionColumn = new TextColumn<InvoiceProxy>() {
            @Override
            public String getValue(InvoiceProxy invoiceProxy) {
                return invoiceProxy.getDescription();
            }
        };

        TextColumn<InvoiceProxy> totalColumn = new TextColumn<InvoiceProxy>() {
            @Override
            public String getValue(InvoiceProxy invoiceProxy) {
                return invoiceProxy.getInvoiceTotal().toString();
            }
        };

        TextColumn<InvoiceProxy> dateColumn = new TextColumn<InvoiceProxy>() {
            @SuppressWarnings("deprecation")
            @Override
            public String getValue(InvoiceProxy invoiceProxy) {
                return DateTimeFormat.getShortDateFormat().format(invoiceProxy.getDate());
            }
        };

        invoiceTable.addColumn(invoiceStateColumn, "State");
        invoiceTable.addColumn(dateColumn, "Date");
        invoiceTable.addColumn(descriptionColumn, "Description");
        invoiceTable.addColumn(totalColumn, "Total");

        invoiceTable.setWidth("100%", true);
        invoiceTable.setColumnWidth(dateColumn, "30%");

        invoiceTable.setEmptyTableWidget(new HTML("No Results"));

Invoice Master Detail View and Table Selection Model

We then define a selection model for our invoiceTable. Our invoice table is a component of our master/detail view which drives our invoice editor, when we select a row in our invoice table we want our invoice editor to update, we accomplish this by defining the selection model and then calling setInput and passing the selected InvoiceProxy object to our InvoiceEditor when a row is clicked.

final SingleSelectionModel<InvoiceProxy> selectionModel = new SingleSelectionModel<InvoiceProxy>();
        invoiceTable.setSelectionModel(selectionModel);
        selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
            public void onSelectionChange(SelectionChangeEvent event) {
                InvoiceProxy selected = selectionModel.getSelectedObject();
                if (selected != null) {
                    invoiceEditor.setEventBus(eventBus);
                    invoiceEditor.setVisible(true);
                    invoiceEditor.setInput(selected, getRequestFactory(eventBus).invoiceRequest(),
                            EditorMode.VIEW, placeController);
                } else {
                    invoiceEditor.setVisible(false);
                }
            }
        });

ViewCustomerActivity.java

As I previously mentioned our setInput method drives the CustomerView, setInput is called when we select a Customer from the dashboard page. If you recall when we select a customer we tell the PlaceController to go to the ViewCustomer place which then starts our ViewCustomerActivity. In the start method of ViewCustomerActivity we retrieve the entityId of the Customer from the place.getEntityId() method, if it's -1 we know we have a new customer, otherwise we retrieve the Customer entity via the CustomerRequest.findById() method and call setInput() on our CustomerView.

ViewCustomerActivity.java

package com.aceevo.example.aceevobooks.client.customer;

import java.util.List;

import com.aceevo.example.aceevobooks.client.AceevoBaseAbstractActivity;
import com.aceevo.example.aceevobooks.client.customer.view.CustomerView;
import com.aceevo.example.aceevobooks.client.model.AddressProxy;
import com.aceevo.example.aceevobooks.client.model.BreadCrumb;
import com.aceevo.example.aceevobooks.client.model.CustomerProxy;
import com.aceevo.example.aceevobooks.client.model.InvoiceProxy;
...

public class ViewCustomerActivity extends AceevoBaseAbstractActivity {

    private CustomerView customerView;
    private AsyncDataProvider<InvoiceProxy> cachedDataProvider;

    public ViewCustomerActivity(ViewCustomer place, CustomerView customerView,
            PlaceController placeController) {
        super(placeController, place);
        this.customerView = customerView;
    }

    @Override
    public void start(final AcceptsOneWidget panel, final EventBus eventBus) {

        panel.setWidget(customerView.asWidget());

        CustomerRequest customerRequest = getRequestFactory(eventBus).customerRequest();

        final AsyncDataProvider<InvoiceProxy> dataProvider = new AsyncDataProvider<InvoiceProxy>() {
            @Override
            protected void onRangeChanged(HasData<InvoiceProxy> display) {

                InvoiceRequest request = getRequestFactory(eventBus).invoiceRequest();
                request.findInvoicesByCustomerId(place.getEntityId(),
                        ((ViewCustomer) place).getInvoiceState().toString()).fire(
                        new Receiver<List<InvoiceProxy>>() {

                            @Override
                            public void onSuccess(List<InvoiceProxy> response) {
                                customerView.getInvoiceTable().setRowData(0, response);
                                customerView.getInvoiceTable().setRowCount(response.size());
                            }
                        });
            }
        };

        Range range = customerView.getInvoiceTable().getVisibleRange();
        customerView.getInvoiceTable().setVisibleRangeAndClearData(range, true);
        dataProvider.addDataDisplay(customerView.getInvoiceTable());
        cachedDataProvider = dataProvider;

        if (place.getEntityId().equals("-1")) {
            CustomerProxy customerProxy = customerRequest.create(CustomerProxy.class);
            AddressProxy addressProxy = customerRequest.create(AddressProxy.class);
            customerProxy.setAddress(addressProxy);
            customerView.setInput(customerProxy, customerRequest, placeController);
            updateBreadCrumb(null, eventBus);
        } else {
            customerRequest.findById(place.getEntityId()).with("address").fire(
                    new Receiver<CustomerProxy>() {

                        @Override
                        public void onSuccess(CustomerProxy response) {
                            updateBreadCrumb(response, eventBus);
                            customerView.setInput(response, getRequestFactory(eventBus)
                                    .customerRequest(), placeController);

                        }

                        @Override
                        public void onFailure(ServerFailure error) {
                            panel.setWidget(new HTML("unable to find customer"));
                        }
                    });

        }

    }
    
    private void updateBreadCrumb(CustomerProxy customerProxy, EventBus eventBus) {
        String moduleUrl = getModuleUrl();

        place.addBreadCrumb(new BreadCrumb("Dashboard", moduleUrl + "#DashboardPlace"));
        place.addBreadCrumb(new BreadCrumb("Customer", null));
        if (customerProxy == null)
            place.addBreadCrumb(new BreadCrumb("New Customer", null));
        else {
            String newUrl = moduleUrl + "#" + "ViewCustomer:" + place.getEntityId();

            place.addBreadCrumb(new BreadCrumb(customerProxy.getName(), newUrl));
        }

        eventBus.fireEvent(new BreadCrumbChangeEvent(customerView));

    }

    @Override
    public void onStop() {
        cachedDataProvider.removeDataDisplay(customerView.getInvoiceTable());
    }
}

CustomerViewImpl.java setInput and UiHandlers

Once we receive our customer object we can start to drive our editors and the rest of the view components in CustomerViewImpl. Mostly what we're doing here is just adjusting some state of the view but the interesting part is the call to setInput on the customerEditor. We're going to dig into the editors more here shortly.

CustomerViewImpl.java setInput Method

@Override
    public void setInput(CustomerProxy customerProxy, RequestContext requestContext,
            PlaceController placeController) {

        this.customerProxy = customerProxy;
        EditorMode editorMode = EditorMode.VIEW;
        editCustomer.setVisible(true);
        deleteCustomer.setVisible(true);
        addInvoice.setVisible(true);

        if (customerProxy.getId() == null) {
            editorMode = EditorMode.CREATE;
            editCustomer.setVisible(false);
            deleteCustomer.setVisible(false);
            addInvoice.setVisible(false);
            invoiceEditor.setVisible(false);
        }
        customerEditor.setInput(customerProxy, requestContext, editorMode, placeController);
    }

        @UiHandler("editCustomer")
    void handleEditClick(ClickEvent e) {
        customerEditor.setEditMode(EditorMode.EDIT);
    }

    @UiHandler("deleteCustomer")
    void handleDeleteClick(ClickEvent e) {
        eventBus.fireEvent(new CustomerDeleteEvent(customerProxy));
    }

    @UiHandler("paidInvoicesFilter")
    void handlePaidInvoicesFilterClick(ClickEvent e) {
        applyInvoiceFilter(InvoiceState.Paid);
    }

    @UiHandler("outstandingInvoicesFilter")
    void handleOutstandingInvoicesFilterClick(ClickEvent e) {
        applyInvoiceFilter(InvoiceState.Outstanding);
    }

    @UiHandler("allInvoicesFilter")
    void handleAllInvoicesFilterClick(ClickEvent e) {
        applyInvoiceFilter(InvoiceState.All);
    }

    @UiHandler("addInvoice")
    void handleAddInvoice(ClickEvent e) {
        invoiceEditor.setVisible(true);
        InvoiceRequest invoiceRequest = getRequestFactory(eventBus).invoiceRequest();
        InvoiceProxy invoiceProxy = invoiceRequest.create(InvoiceProxy.class);
        invoiceProxy.setCustomerId(customerProxy.getId());
        invoiceProxy.setDate(new Date());
        invoiceProxy.setHours(new Integer(8));
        invoiceProxy.setRate(new Integer(100));
        invoiceProxy.setInvoiceState(InvoiceState.Outstanding);
        invoiceEditor.setInput(invoiceProxy, invoiceRequest, EditorMode.EDIT, placeController);
    }

Creating an Abstract Editing Framework using GWT Editor

So the goal here is to abstract the reusable parts of an editor into a common abstract parent class that we can reuse across our application. We have 3 editors in our app, the CustomerEditor.java, AddressEditor.java and InvoiceEditor.java, they all extend AbstractAceevoBaseEditor.java

Here's the entire implementation, next we'll dig and take a closer look at the class explaining each major piece of functionality.

AbstractAceevoEditor.java

package com.aceevo.example.aceevobooks.client.editor;

import java.util.List;
import java.util.Map;
import java.util.Set;

import com.aceevo.example.aceevobooks.client.model.AbstractEntityProxy;
import com.aceevo.example.aceevobooks.client.view.AceevoBooksView;
import com.google.gwt.editor.client.Editor;
import com.google.gwt.editor.client.EditorError;
import com.google.gwt.editor.client.HasEditorErrors;
import com.google.gwt.place.shared.Place;
import com.google.gwt.place.shared.PlaceController;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Widget;
import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryEditorDriver;
import com.google.web.bindery.requestfactory.shared.BaseProxy;
import com.google.web.bindery.requestfactory.shared.InstanceRequest;
import com.google.web.bindery.requestfactory.shared.Receiver;
import com.google.web.bindery.requestfactory.shared.RequestContext;
import com.google.web.bindery.requestfactory.shared.Violation;

public abstract class AbstractAceevoEditor<T extends BaseProxy> extends Composite implements
        Editor<T>, AceevoBooksView, HasEditorErrors<T> {

    public enum EditorMode {
        VIEW, EDIT, CREATE
    };

    protected RequestFactoryEditorDriver<T, Editor<T>> editorDriver;
    protected T cachedObject;
    protected RequestContext cachedRequestContext;

    protected PlaceController placeController;

    protected Map<String, Widget> pathToFieldMap;

    protected Receiver<T> updateReceiver = new Receiver<T>() {
        @Override
        public void onSuccess(T response) {
            clearErrors();
            try {
                placeController.goTo(getSavePlace(response));
            } catch (Exception ex) {
                Window.alert("Error saving Editor: " + ex.getMessage());
            }
        }

        public void onViolation(Set<Violation> errors) {
            clearErrors();
            editorDriver.setViolations(errors);
            for (Violation violation : errors)
                setWidgetError(pathToFieldMap.get(violation.getPath()));
        }
    };

    public AbstractAceevoEditor() {

    }

    public abstract Map<String, Widget> getPathToFieldMap();

    public abstract InstanceRequest<T, T> getInstanceRequest() throws Exception;

    public abstract Place getSavePlace(T t) throws Exception;

    public void setEditorDriver(RequestFactoryEditorDriver<T, Editor<T>> editorDriver) {
        this.editorDriver = editorDriver;
    }

    public void setInput(T entityProxy, RequestContext requestContext, EditorMode editorMode,
            PlaceController placeController) {

        this.cachedObject = entityProxy;
        this.placeController = placeController;
        this.cachedRequestContext = requestContext;

        editorDriver.initialize(this);
        setEditMode(editorMode);
        setVisible(true);

        clearErrors();
    }

    public void setEditMode(EditorMode editorMode) {
        if (editorMode == EditorMode.EDIT || editorMode == EditorMode.CREATE) {
            editorDriver.edit(this.cachedObject, this.cachedRequestContext);
            removeStyleName("readOnly");
        } else if (editorMode == EditorMode.VIEW) {
            editorDriver.display(this.cachedObject);
            addStyleName("readOnly");
        }

        notifyEditMode(editorMode);
    }

    public void notifyEditMode(EditorMode editorMode) {
        // no op
    }

    public void clearErrors() {
        if (pathToFieldMap == null)
            pathToFieldMap = getPathToFieldMap();

        for (Widget w : pathToFieldMap.values())
            w.removeStyleName("errorWidget");
        notifyErrorsCleared();
    }

    public void save() throws Exception {
        if (cachedObject instanceof AbstractEntityProxy) {
            editorDriver.flush();
            getInstanceRequest().using(cachedObject).fire(updateReceiver);
        }
    }

    @Override
    public void showErrors(List<EditorError> errors) {
        if (pathToFieldMap == null)
            pathToFieldMap = getPathToFieldMap();

        for (EditorError error : errors)
            setWidgetError(pathToFieldMap.get(error.getPath()));
    }

    public void setWidgetError(Widget w) {
        if (w != null)
            w.addStyleName("errorWidget");
    }

    public void notifyErrorsCleared() {
    }

}

A Closer Look at AbstractAceevoEditor

We know that we'll be editing Proxy objects from the RequestFactory and these objects extend BaseProxy so we type the AbstractAceevoEditor with a generic T that extends BaseProxy. We implement AceevoBooksView and HasEditorErrors as well as the GWT Editor interface and then declare an enum EditorMode that defines the different modes of the editor.

AbstractAceevoEditor.java

public abstract class AbstractAceevoEditor<T extends BaseProxy> extends Composite implements
        Editor<T>, AceevoBooksView, HasEditorErrors<T> {

    public enum EditorMode {
        VIEW, EDIT, CREATE
    };

The GWT Editor requires an editor driver so we've defined a generic editor for our type T and we also retain a reference to the current object we're editing (cachedObject) as well as the RequestContext for editing this object (cachedRequestContent).

Finally the pathToFieldMap retains key/value mapping of Fields in the UI to attributes in the model and we use this later for validation and setting fields red on error.

protected RequestFactoryEditorDriver<T, Editor<T>> editorDriver;
    protected T cachedObject;
    protected RequestContext cachedRequestContext;

    protected PlaceController placeController;

    protected Map<String, Widget> pathToFieldMap;

Next we create a generic instance of Receiver called updateReceiver, this object is later called from our generic save method. In onSuccess we retrieve the place from getSavePlace() and notify the PlaceController, onViolation we update our view by finding the path of a violation and retrieving the widget from the pathToFieldMap.

protected Receiver<T> updateReceiver = new Receiver<T>() {
        @Override
        public void onSuccess(T response) {
            clearErrors();
            try {
                placeController.goTo(getSavePlace(response));
            } catch (Exception ex) {
                Window.alert("Error saving Editor: " + ex.getMessage());
            }
        }

        public void onViolation(Set<Violation> errors) {
            clearErrors();
            editorDriver.setViolations(errors);
            for (Violation violation : errors)
                setWidgetError(pathToFieldMap.get(violation.getPath()));
        }
    };

AbstractAceevoEditor abstract methods

We define 3 abstract methods that any class that extends AbstractAceevoEditor must implement they are.

public abstract Map<String, Widget> getPathToFieldMap();

    public abstract InstanceRequest<T, T> getInstanceRequest() throws Exception;

    public abstract Place getSavePlace(T t) throws Exception;

The rest of our abstract implementation of AbstractAceevoEditor

The setInput method receives the proxied entity to be edited and resets the state of the editor, preparing it for editing, validation and calls to the InstanceRequest for persistance.

public void setInput(T entityProxy, RequestContext requestContext, EditorMode editorMode,
            PlaceController placeController) {

        this.cachedObject = entityProxy;
        this.placeController = placeController;
        this.cachedRequestContext = requestContext;

        editorDriver.initialize(this);
        setEditMode(editorMode);
        setVisible(true);

        clearErrors();
    }

The setEditMode method changes the state of the editor, the Editor Driver and applies some css styling pretty self explanatory.

public void setEditMode(EditorMode editorMode) {
        if (editorMode == EditorMode.EDIT || editorMode == EditorMode.CREATE) {
            editorDriver.edit(this.cachedObject, this.cachedRequestContext);
            removeStyleName("readOnly");
        } else if (editorMode == EditorMode.VIEW) {
            editorDriver.display(this.cachedObject);
            addStyleName("readOnly");
        }

        notifyEditMode(editorMode);
    }

clearErrors(), showErrors(), and setWidgetErrors() are all used in validating and updating the view when attempting to persist an entity.

public void clearErrors() {
        if (pathToFieldMap == null)
            pathToFieldMap = getPathToFieldMap();

        for (Widget w : pathToFieldMap.values())
            w.removeStyleName("errorWidget");
        notifyErrorsCleared();
    }

        @Override
    public void showErrors(List<EditorError> errors) {
        if (pathToFieldMap == null)
            pathToFieldMap = getPathToFieldMap();

        for (EditorError error : errors)
            setWidgetError(pathToFieldMap.get(error.getPath()));
    }

    public void setWidgetError(Widget w) {
        if (w != null)
            w.addStyleName("errorWidget");
    }

Finally our save() flushes our editorDriver and retrieve the InstanceRequest for our current object being edited and fires a persist request off to the InstanceRequest and RequestFactory handing our updateReceiver for dealing with the results.

public void save() throws Exception {
        if (cachedObject instanceof AbstractEntityProxy) {
            editorDriver.flush();
            getInstanceRequest().using(cachedObject).fire(updateReceiver);
        }
    }

Using our Abstract Editor Framework

So as previously mentioned we have 3 editors that extend our AbstractAceevoEditor, let's take a look at the AddressEditor, it's a component used in our CustomerEditor, I abstracted it out to it's own component because it's common to have to edit an address in more than one place in an application and we have a separate Address object in our Customer.java domain object.

Therefore you'lll notice that we throw UnsupportedOperationExceptions in the AddressEditor's getSavePlace() and getInstanceRequest() methods as our Address object is an Embedded value object on the backend.

To use the AbstractAceevoEdtitor we simply extend and provide a type for our T generic and then implement the required abstract methods.

@Embedded private Address address;

AddressEditor.java

package com.aceevo.example.aceevobooks.client.customer.view;

import java.util.HashMap;
import java.util.Map;

import com.aceevo.example.aceevobooks.client.editor.AbstractAceevoEditor;
import com.aceevo.example.aceevobooks.client.model.AddressProxy;
import com.aceevo.example.aceevobooks.client.view.AceevoBooksView;
import com.google.gwt.core.client.GWT;
import com.google.gwt.place.shared.Place;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;
import com.google.web.bindery.requestfactory.shared.InstanceRequest;

public class AddressEditor extends AbstractAceevoEditor<AddressProxy> implements AceevoBooksView {

    @UiField
    TextBox address;

    @UiField
    TextBox city;

    @UiField
    TextBox state;

    @UiField
    TextBox zip;

    @UiField
    TextBox country;

    interface AddressEditorUiBinder extends UiBinder<HTMLPanel, AddressEditor> {
    }

    private Map<String, Widget> pathToFieldMap;

    AddressEditorUiBinder addressEditorUiBinder = GWT.create(AddressEditorUiBinder.class);

    public AddressEditor() {
        initWidget(addressEditorUiBinder.createAndBindUi(this));
    }

    @Override
    public void notifyEditMode(
            com.aceevo.example.aceevobooks.client.editor.AbstractAceevoEditor.EditorMode editorMode) {
        address.setReadOnly(editorMode.equals(EditorMode.VIEW));
        city.setReadOnly(editorMode.equals(EditorMode.VIEW));
        state.setReadOnly(editorMode.equals(EditorMode.VIEW));
        zip.setReadOnly(editorMode.equals(EditorMode.VIEW));
        country.setReadOnly(editorMode.equals(EditorMode.VIEW));
    }

    @Override
    public Map<String, Widget> getPathToFieldMap() {
        if (pathToFieldMap == null) {
            pathToFieldMap = new HashMap<String, Widget>();
            pathToFieldMap.put("address", address);
            pathToFieldMap.put("city", city);
            pathToFieldMap.put("state", state);
            pathToFieldMap.put("zip", zip);
            pathToFieldMap.put("country", country);
        }
        return pathToFieldMap;

    }

    @Override
    public Place getSavePlace(AddressProxy addressProxy) throws Exception {
        throw new UnsupportedOperationException("getSavePlace not supported for AddressEditor");
    }

    @Override
    public InstanceRequest<AddressProxy, AddressProxy> getInstanceRequest() throws Exception {
        throw new UnsupportedOperationException(
                "getInstanceRequest not supported for AddressEditor");
    }

}

AddressEditor.ui.xml

<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
    xmlns:g='urn:import:com.google.gwt.user.client.ui' xmlns:c="urn:import:com.google.gwt.user.cellview.client"
    xmlns:my="urn:import:com.aceevo.example.aceevobooks.client.view">

    <g:HTMLPanel>
        <div class="formLabel">Address</div>
        <g:TextBox ui:field="address"></g:TextBox>
        <div class="formLabel">City</div>
        <g:TextBox ui:field="city"></g:TextBox>
        <div class="formLabel">State</div>
        <g:TextBox ui:field="state"></g:TextBox>
        <div class="formLabel">Zip</div>
        <g:TextBox ui:field="zip"></g:TextBox>
        <div class="formLabel">Country</div>
        <g:TextBox ui:field="country"></g:TextBox>
    </g:HTMLPanel>

</ui:UiBinder>

CustomerEditor.java

The CustomerEditor is slightly more complex than the AddressEditor, but follows the same principals. Some things to note include.

CustomerEditor.java

package com.aceevo.example.aceevobooks.client.customer.view;

import java.util.HashMap;
import java.util.Map;

import com.aceevo.example.aceevobooks.client.customer.ViewCustomer;
import com.aceevo.example.aceevobooks.client.editor.AbstractAceevoEditor;
import com.aceevo.example.aceevobooks.client.editor.BaseProxyEditorDecorator;
import com.aceevo.example.aceevobooks.client.model.CustomerProxy;
import com.aceevo.example.aceevobooks.client.requests.CustomerRequest;
import com.google.gwt.core.client.GWT;
import com.google.gwt.editor.client.EditorDelegate;
import com.google.gwt.place.shared.Place;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;
import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryEditorDriver;
import com.google.web.bindery.requestfactory.shared.InstanceRequest;

public class CustomerEditor extends AbstractAceevoEditor<CustomerProxy> {

    @UiField
    TextBox name;

    @UiField
    AddressEditor addressEditor;

    @UiField
    HTMLPanel editor;

    @UiField
    BaseProxyEditorDecorator baseProxyEditorDecorator;

    interface CustomerEditorUiBinder extends UiBinder<HTMLPanel, CustomerEditor> {
    }

    interface EditorDriver extends RequestFactoryEditorDriver<CustomerProxy, CustomerEditor> {
    }

    private final EditorDriver editorDriver = GWT.create(EditorDriver.class);

    CustomerEditorUiBinder customerEditorUiBinder = GWT.create(CustomerEditorUiBinder.class);

    @SuppressWarnings("unchecked")
    public CustomerEditor() {
        initWidget(customerEditorUiBinder.createAndBindUi(this));
        setEditorDriver((RequestFactoryEditorDriver) editorDriver);
        baseProxyEditorDecorator.setEditor((AbstractAceevoEditor) this);
    }

    @Override
    public void notifyEditMode(
            com.aceevo.example.aceevobooks.client.editor.AbstractAceevoEditor.EditorMode editorMode) {
        baseProxyEditorDecorator.setVisible(!editorMode.equals(EditorMode.VIEW));
        name.setReadOnly(editorMode.equals(EditorMode.VIEW));
        addressEditor.notifyEditMode(editorMode);
    }

    public InstanceRequest<CustomerProxy, CustomerProxy> getInstanceRequest() {
        CustomerRequest requestContext = (CustomerRequest) editorDriver.flush();
        return requestContext.persist();
    }

    @Override
    public Place getSavePlace(CustomerProxy customerProxy) {
        ViewCustomer viewCustomer = new ViewCustomer();
        viewCustomer.setEntityId(customerProxy.getId());
        return viewCustomer;
    }

    @Override
    public void notifyErrorsCleared() {
        addressEditor.clearErrors();
    }
    
    @Override
    public Map<String, Widget> getPathToFieldMap() {
        HashMap<String, Widget> map = new HashMap<String, Widget>();
        map.put("name", name);
        
        for(String key: addressEditor.getPathToFieldMap().keySet()) {
            map.put(key, addressEditor.getPathToFieldMap().get(key));
        }
        
        return map;
    }

}

CustomerEditor.ui.xml

<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
    xmlns:g='urn:import:com.google.gwt.user.client.ui' xmlns:c="urn:import:com.google.gwt.user.cellview.client"
    xmlns:my="urn:import:com.aceevo.example.aceevobooks.client.view"
    xmlns:myEditor="urn:import:com.aceevo.example.aceevobooks.client.customer.view"
    xmlns:editorDecorator="urn:import:com.aceevo.example.aceevobooks.client.editor">

    <g:HTMLPanel visible="false" styleName="customerEditorWrapper"
        ui:field="editor">
        <div class="formLabel">Organization Name</div>
        <g:TextBox ui:field="name"></g:TextBox>
        <myEditor:AddressEditor ui:field="addressEditor"></myEditor:AddressEditor>
        <editorDecorator:BaseProxyEditorDecorator
            ui:field="baseProxyEditorDecorator"></editorDecorator:BaseProxyEditorDecorator>
    </g:HTMLPanel>

</ui:UiBinder>

InvoiceEdtitor.java

package com.aceevo.example.aceevobooks.client.invoice.view;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import com.aceevo.example.aceevobooks.client.customer.ViewCustomer;
import com.aceevo.example.aceevobooks.client.customer.event.InvoiceDeleteEvent;
import com.aceevo.example.aceevobooks.client.editor.AbstractAceevoEditor;
import com.aceevo.example.aceevobooks.client.editor.BaseProxyEditorDecorator;
import com.aceevo.example.aceevobooks.client.model.InvoiceProxy;
import com.aceevo.example.aceevobooks.client.requests.InvoiceRequest;
import com.aceevo.example.aceevobooks.shared.InvoiceState;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.place.shared.Place;
import com.google.gwt.text.shared.Renderer;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.IntegerBox;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.ValueListBox;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.datepicker.client.DateBox;
import com.google.web.bindery.requestfactory.gwt.client.RequestFactoryEditorDriver;
import com.google.web.bindery.requestfactory.shared.InstanceRequest;

public class InvoiceEditor extends AbstractAceevoEditor<InvoiceProxy> {

    @UiField
    DateBox date;

    @UiField
    IntegerBox rate;

    @UiField
    IntegerBox hours;

    @UiField
    TextBox description;

    @UiField
    Button editInvoice;

    @UiField
    Button deleteInvoice;

    private EventBus eventBus;

    @UiField(provided = true)
    ValueListBox<InvoiceState> invoiceState = new ValueListBox<InvoiceState>(
            new Renderer<InvoiceState>() {

                @Override
                public String render(InvoiceState object) {
                    if (object != null)
                        return object.name();
                    return "";
                }

                @Override
                public void render(InvoiceState object, Appendable appendable) throws IOException {
                    render(object);
                }

            });

    @UiField
    HTMLPanel editor;

    @UiField
    BaseProxyEditorDecorator baseProxyEditorDecorator;

    interface InvoiceEditorUiBinder extends UiBinder<HTMLPanel, InvoiceEditor> {
    }

    interface EditorDriver extends RequestFactoryEditorDriver<InvoiceProxy, InvoiceEditor> {
    }

    private final EditorDriver editorDriver = GWT.create(EditorDriver.class);

    InvoiceEditorUiBinder invoiceEditorUiBinder = GWT.create(InvoiceEditorUiBinder.class);

    @SuppressWarnings("unchecked")
    public InvoiceEditor() {
        initWidget(invoiceEditorUiBinder.createAndBindUi(this));
        setEditorDriver((RequestFactoryEditorDriver) editorDriver);
        baseProxyEditorDecorator.setEditor((AbstractAceevoEditor) this);

        invoiceState.setAcceptableValues(Arrays.asList(new InvoiceState[] {
                InvoiceState.Outstanding, InvoiceState.Paid }));

    }

    public void notifyEditMode(EditorMode editorMode) {
        date.setEnabled(editorMode.equals(EditorMode.EDIT));
        rate.setReadOnly(editorMode.equals(EditorMode.VIEW));
        hours.setReadOnly(editorMode.equals(EditorMode.VIEW));
        description.setReadOnly(editorMode.equals(EditorMode.VIEW));
        DOM.setElementPropertyBoolean(invoiceState.getElement(), "disabled", editorMode
                .equals(EditorMode.VIEW));

        baseProxyEditorDecorator.setVisible(!editorMode.equals(EditorMode.VIEW));
    }

    public InstanceRequest<InvoiceProxy, InvoiceProxy> getInstanceRequest() {
        InvoiceRequest requestContext = (InvoiceRequest) editorDriver.flush();
        return requestContext.persist();
    }

    @Override
    public Place getSavePlace(InvoiceProxy customerProxy) {
        ViewCustomer viewCustomer = new ViewCustomer();
        viewCustomer.setEntityId(customerProxy.getCustomerId());
        return viewCustomer;
    }

    @Override
    public Map<String, Widget> getPathToFieldMap() {
        HashMap<String, Widget> map = new HashMap<String, Widget>();
        map.put("date", date);
        map.put("rate", rate);
        map.put("hours", hours);
        map.put("Description", description);
        map.put("InvoiceState", invoiceState);
        return map;
    }

    @UiHandler("deleteInvoice")
    void handleDeleteClick(ClickEvent e) {
        eventBus.fireEvent(new InvoiceDeleteEvent((InvoiceProxy) cachedObject));
    }

    @UiHandler("editInvoice")
    void handleEditClick(ClickEvent e) {
        setEditMode(EditorMode.EDIT);
    }

    public void setEventBus(EventBus eventBus) {
        this.eventBus = eventBus;
    }

}

InvoiceEdtitor.ui.xml

<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
    xmlns:g='urn:import:com.google.gwt.user.client.ui' xmlns:c="urn:import:com.google.gwt.user.cellview.client"
    xmlns:d="urn:import:com.google.gwt.user.datepicker.client"
    xmlns:my="urn:import:com.aceevo.example.aceevobooks.client.view"
    xmlns:myEditor="urn:import:com.aceevo.example.aceevobooks.client.customer.view"
    xmlns:editorDecorator="urn:import:com.aceevo.example.aceevobooks.client.editor">

    <g:HTMLPanel visible="false" ui:field="editor" styleName="invoiceEditorWrapper">
        <div class="h2Wrapper">
            <h2>Invoice Editor</h2>
            <g:Button ui:field="deleteInvoice" styleName="btn btn34   ">
                Delete
            </g:Button>
            <g:Button ui:field="editInvoice" styleName="btn btn34">
                Edit
            </g:Button>
            <div class="clearfix" />
        </div>
        <table style="clear:left" class="invoiceEditor">
        <tr>
            <td width="50"><div class="formLabel">Date</div></td>
            <td><d:DateBox styleName="gwt-TextBox" ui:field="date"></d:DateBox></td>
        </tr>  
        <!-- <g:TextBox ui:field="invoiceNumber"></g:TextBox>  -->
        <tr>
            <td><div class="formLabel">Rate</div></td>
            <td><g:IntegerBox ui:field="rate" styleName="gwt-TextBox"></g:IntegerBox></td>
        </tr>
        <tr>
            <td><div class="formLabel">Hours</div></td>
            <td><g:IntegerBox ui:field="hours" styleName="gwt-TextBox"></g:IntegerBox></td>
        </tr>
        <tr>
            <td><div class="formLabel">Description</div></td>
            <td><g:TextBox ui:field="description"></g:TextBox></td>
        </tr>
        <tr>
            <td><div class="formLabel">State</div></td>
            <td><g:ValueListBox ui:field="invoiceState"/></td>
        </tr>
        </table>
        <editorDecorator:BaseProxyEditorDecorator
            ui:field="baseProxyEditorDecorator"></editorDecorator:BaseProxyEditorDecorator>
    </g:HTMLPanel>

</ui:UiBinder>
comments powered by Disqus