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

In Part 1 of this tutorial we covered the server side domain model, morphia setup and basic GWT bootstrapping for the project. So let's continue by setting up our RequestFactory.

Source Code Available here on GitHub

RequestFactory

The RequestFactory is described on the GWT website, it says...

RequestFactory is an alternative to GWT-RPC for creating data-oriented services. RequestFactory and its related interfaces (RequestContext and EntityProxy) make it easy to build data-oriented (CRUD) apps with an ORM-like interface on the client. It is designed to be used with an ORM layer like JDO or JPA on the server, although this is not required.

This article here on stackoverflow contains a good explanation of the differences between GWT-RPC and RequestFactory, but what it boils down to is GWT-RPC is "RPC-by-concrete-type" while RequestFactory is "RPC-by-interface".

One of the obvious and immediate benefits of using the RequestFactory is we now eliminate the need for using DTO's and can use proxy interfaces.

Request Contexts

Figure 1: Entity and Value Proxies and Request Contexts

As with GWT-RPC, you define the interface between your client and server code by extending an interface. You define one RequestFactory interface for your application, and it consists of methods that return service stubs. Here's our RequestFactory interface.

AceevoBooksRequestFactory.java

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

import com.google.web.bindery.requestfactory.shared.RequestFactory;

public interface AceevoBooksRequestFactory extends RequestFactory {

    CustomerRequest customerRequest();

    InvoiceRequest invoiceRequest();

}

For our two entity types (Customer and Invoice) we will create RequestContext interfaces, as noted in the GWT documentation,

The RequestFactory service stubs must extend RequestContext and use the @Service or @ServiceName annotation to name the associated service implementation class on the server. The methods in a service stub do not return entities directly, but rather return subclasses of com.google.web.bindery.requestfactory.shared.Request. This allows the methods on the interface to be invoked asynchronously with Request.fire() similar to passing an AsyncCallback object to each service method in GWT-RPC.

CustomerRequest.java

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

import java.util.List;

import com.aceevo.example.aceevobooks.client.model.CustomerProxy;
import com.aceevo.example.aceevobooks.server.model.Customer;
....

@Service(Customer.class)
public interface CustomerRequest extends RequestContext {

    Request<List<CustomerProxy>> findAllCustomers();

    Request<CustomerProxy> findById(String id);

    InstanceRequest<CustomerProxy, CustomerProxy> persist();

    InstanceRequest<CustomerProxy, Void> remove();

}

InvoiceRequest.java

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

import java.util.List;

import com.aceevo.example.aceevobooks.client.model.CustomerProxy;
import com.aceevo.example.aceevobooks.client.model.InvoiceProxy;
...
@Service(Invoice.class)
public interface InvoiceRequest extends RequestContext {

    Request<List<InvoiceProxy>> findAllInvoices();

    Request<List<InvoiceProxy>&gt findInvoicesByCustomerId(String customerId, String invoiceState);

    Request<InvoiceProxy> findById(String id);

    InstanceRequest<InvoiceProxy, InvoiceProxy> persist();

    Request<Double> findOutstandingInvoiceTotal();

    Request<Double> findPaidInvoiceTotal();

    InstanceRequest<InvoiceProxy, Void> remove();

}

Locator and ServiceLocator

If we wanted to get fancy and put our persistence logic in a DAO we could use a ServiceLocator to and put our business and persistence logic in a separate class and annotate our RequestContexts like this...

Locator Example

@Service(value = CustomerDao.class, locator = AceevoBooksServiceLocator.class)
interface CustomerRequest extends RequestContext

but we're going to keep it real simple and leave our persistence and business logic on the entities themselves for this tutorial.

Client domain model (Proxies)

Now for our client side domain model we will create Proxy Interfaces for Customer, Invoice, and Address, I've also created a base AbstractEntityProxy class for our Entity types that share the requirement for the getId() and getVersion() methods.

Make sure that you annotate your client proxy interface with the @ProxyFor annotation corresponding to the appropriate server side class.

Figure 2: Client Domain Model

AbstractEntityProxy.java

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

import com.google.web.bindery.requestfactory.shared.EntityProxy;

public interface AbstractEntityProxy extends EntityProxy {

    public Long getVersion();

    public String getId();

}

InvoiceProxy.java

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

import java.util.Date;

import com.aceevo.example.aceevobooks.server.model.Invoice;
import com.aceevo.example.aceevobooks.shared.InvoiceState;
import com.google.web.bindery.requestfactory.shared.ProxyFor;

@ProxyFor(Invoice.class)
public interface InvoiceProxy extends AbstractEntityProxy {

    String getCustomerId();
    
    void setCustomerId(String customerId);

    Integer getInvoiceNumber();
    
    void setInvoiceNumber(Integer invoiceNumber);

    Date getDate();
    
    void setDate(Date date);

    String getDescription();
    
    void setDescription(String description);

    Integer getRate();
    
    void setRate(Integer rate);

    Integer getHours();
    
    void setHours(Integer hours);

    Double getInvoiceTotal();
    
    InvoiceState getInvoiceState();
    
    void setInvoiceState(InvoiceState invoiceState);
    
}

CustomerProxy.java

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

import com.aceevo.example.aceevobooks.server.model.Customer;
import com.google.web.bindery.requestfactory.shared.ProxyFor;

@ProxyFor(Customer.class)
public interface CustomerProxy extends AbstractEntityProxy {

    String getName();
    void setName(String name);
    AddressProxy getAddress();
    void setAddress(AddressProxy addressProxy);
}

AddressProxy.java

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

import com.aceevo.example.aceevobooks.server.model.Address;
import com.google.web.bindery.requestfactory.shared.ProxyFor;
import com.google.web.bindery.requestfactory.shared.ValueProxy;

@ProxyFor(Address.class)
public interface AddressProxy extends ValueProxy {

    String getAddress();

    void setAddress(String address);

    String getCity();

    void setCity(String city);

    String getState();

    void setState(String state);

    String getCountry();

    void setCountry(String country);

    String getZip();

    void setZip(String zip);

}

Some View Plumbing

Taking a look at our Dashboard Page, it has 3 main components, the header and breadcrumb, the left hand customer "bubble" list and the right hand aggregate invoice statistics view. Actually both the Dashboard Page and the View Customer Page both a header (where perhaps we'd add a nav) and a breadcrumb, let's start by building some abstract view components for our header and breadcrumb that will be reused on each of our pages.

Figure 3: Dashboard Page Sections

AceevoBooks View plumbing

The com.aceevo.example.aceevobooks.view package contains our basic plumbing for our views and the HeaderView and BreadCrumbView.

For our views, whether they are pages or modal components we want to implement the asWidget method so we have a base interface AceevoBooksView.java

AceevoBooksView.java

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

import com.google.gwt.user.client.ui.IsWidget;

public interface AceevoBooksView {

    IsWidget asWidget();

}

For any view that is an independent page I've created the AbstractAceevoBooksPage class to handle the basic boilerplate to implement asWidget() and handle updating the breadcrumb

AbstractAceevoBooksPage.java

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

import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Widget;

public abstract class AbstractAceevoBooksPage extends Composite implements BreadCrumbView, AceevoBooksView {

    @UiField
    public HeaderView headerView;

    @Override
    public void setBreadCrumb(HTML html) {
        headerView.setBreadCrumb(html);
    }
    
    @Override
    public Widget asWidget() {
        return this;
    }

}

Now we can implement our HeaderView and accompanying ui.xml. The HeaderView will hold the reference to our Breadcrumb and exposes a setBreadCrumb(HTML html) method, that will be called by our BreadCrumbChangeEventHandler()

HeaderView.java

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

import com.google.gwt.core.client.GWT;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.Widget;

public class HeaderView extends Composite {

    interface HeaderViewUiBinder extends UiBinder<HTMLPanel, HeaderView> {
    }

    private static HeaderViewUiBinder headerViewUiBinder = GWT.create(HeaderViewUiBinder.class);

    @UiField
    FlowPanel breadCrumb = new FlowPanel();

    public HeaderView() {
        initWidget(headerViewUiBinder.createAndBindUi(this));
    }

    public void setBreadCrumb(HTML html) {
        breadCrumb.clear();
        breadCrumb.add(html);
    }

    @Override
    public Widget asWidget() {
        return this;
    }

}

HeaderView.ui.xml

<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
  xmlns:g='urn:import:com.google.gwt.user.client.ui'>


<g:HTMLPanel>
  <div id="header">
    <h1>AceevoBooks</h1>
  </div>
  <g:FlowPanel styleName="breadCrumb" ui:field="breadCrumb"/>
</g:HTMLPanel>

</ui:UiBinder>

Activities and Places, Implementing Abstract Places and Activities

We only have 2 pages in our example application but we'll need to implement activities and places for both. You can imagine though in a much larger application having to implement many activities and places and undoubtedly there will be logic that is shared amongst these classes. For this tutorial I've created the abstract class AceevoBooksPlace.java and AceevoBaseAbstractActivity.java to handle encapsulating the abstract logic that will be shared among our activity and place implementations.

AceevoBaseAbstractActivity takes a PlaceController and an AceevoBooksPlace as constructor arguments, the getRequestFactory method lazy initializes an AceevoBooksRequestFactory that is used by activities to retrieve data from the server. We also provide a setter method for unit testing and provide default implementations for mayStop() and goTo(Place place).

In addition we also provide the getModuleUrl() helper method used in activities to construct breadcrumbs.

AceevoBaseAbstractActivity.java

package com.aceevo.example.aceevobooks.client;

import com.aceevo.example.aceevobooks.client.place.AceevoBooksPlace;
import com.aceevo.example.aceevobooks.client.requests.AceevoBooksRequestFactory;
import com.google.gwt.activity.shared.AbstractActivity;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.place.shared.Place;
import com.google.gwt.place.shared.PlaceController;
import com.google.gwt.user.client.Window.Location;
import com.google.gwt.user.client.ui.AcceptsOneWidget;

public abstract class AceevoBaseAbstractActivity extends AbstractActivity {

    private AceevoBooksRequestFactory aceevoBooksRequestFactory;
    protected PlaceController placeController;
    protected AceevoBooksPlace place;

    public AceevoBaseAbstractActivity(PlaceController placeController, AceevoBooksPlace place) {
        this.placeController = placeController;
        this.place = place;
    }

    @Override
    public abstract void start(AcceptsOneWidget panel, EventBus eventBus);

    public AceevoBooksRequestFactory getRequestFactory(EventBus eventBus) {
        if (aceevoBooksRequestFactory == null) {
            aceevoBooksRequestFactory = GWT.create(AceevoBooksRequestFactory.class);
            aceevoBooksRequestFactory.initialize(eventBus);
        }
        return aceevoBooksRequestFactory;
    }

    public void setRequestFactory(AceevoBooksRequestFactory aceevoBooksRequestFactory) {
        this.aceevoBooksRequestFactory = aceevoBooksRequestFactory;
    }

    /**
     * Navigate to a new Place in the browser
     */
    public void goTo(Place place) {
        placeController.goTo(place);
    }

    /**
     * Ask user before stopping this activity
     */
    @Override
    public String mayStop() {
        return null;
    }
    
    protected String getModuleUrl() {
        String moduleUrl = Location.getHref();

        if (moduleUrl.indexOf("#") != -1) {
            moduleUrl = moduleUrl.substring(0, Location.getHref().indexOf("#"));
        }
        
        return moduleUrl;
    }

}

Our abstract place implementation called AceevoBooksPlace has a getEntityId and setEntityId method (as most of our places may consist of viewing or editing entities) and we also have the addBreadCrumb(BreadCrumb breadCrumb) and List<BreadCrumb> getBreadCrumbs() methods to so that we can abstractly retrieve and create the breadcrumb when we switch places.

AceevoBooksPlace.java

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

import java.util.ArrayList;
import java.util.List;

import com.aceevo.example.aceevobooks.client.model.BreadCrumb;
import com.google.gwt.place.shared.Place;

public abstract class AceevoBooksPlace extends Place {

    private String entityId = "-1";
    private List&lt;BreadCrumb&gt; breadCrumbs = new ArrayList&lt;BreadCrumb&gt;();

    public AceevoBooksPlace() {
        // TODO Auto-generated constructor stub
    }

    public void setEntityId(String entityId) {
        this.entityId = entityId;
    }

    public String getEntityId() {
        return entityId;
    }
    
    public List&lt;BreadCrumb&gt; getBreadCrumbs() {
        return breadCrumbs;
    }
    
    public void addBreadCrumb(BreadCrumb breadCrumb) {
        breadCrumbs.add(breadCrumb);
    }
}

Dashboard Page Place and Activity

Our dashboard place is really simple, there aren't any URL parameters that need tokenization so we just extend the AceevoBooksPlace abstract place and provide a basic implementation of the PlaceTokenizer

DashboardPlace.java

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

import com.aceevo.example.aceevobooks.client.place.AceevoBooksPlace;
import com.google.gwt.place.shared.PlaceTokenizer;

public class DashboardPlace extends AceevoBooksPlace {

    public DashboardPlace() {
        // TODO Auto-generated constructor stub
    }
    
    public static class Tokenizer implements PlaceTokenizer&lt;DashboardPlace&gt; {

        @Override
        public DashboardPlace getPlace(String token) {
            DashboardPlace dashboardPlace = new DashboardPlace();
            return dashboardPlace;
        }

        @Override
        public String getToken(DashboardPlace place) {
            return "";
        }
    }
}

The DashboardActivity extends the AceevoBaseAbstractActivity and is responsible for retrieving the required information for the dashboard page from the server and updating our dashboard view. The constructor takes Dashboard place, a reference to a DashboardView and a Place Controller.

DashboardActivity.java

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

import java.util.List;

import com.aceevo.example.aceevobooks.client.AceevoBaseAbstractActivity;
import com.aceevo.example.aceevobooks.client.dashboard.view.DashboardView;
import com.aceevo.example.aceevobooks.client.model.BreadCrumb;
import com.aceevo.example.aceevobooks.client.model.CustomerProxy;
import com.aceevo.example.aceevobooks.client.place.BreadCrumbChangeEvent;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.place.shared.PlaceController;
import com.google.gwt.user.client.ui.AcceptsOneWidget;
import com.google.gwt.view.client.AsyncDataProvider;
import com.google.gwt.view.client.HasData;
import com.google.gwt.view.client.Range;
import com.google.web.bindery.requestfactory.shared.Receiver;

public class DashboardActivity extends AceevoBaseAbstractActivity {

    private DashboardView dashboardView;

    public DashboardActivity(DashboardPlace place, DashboardView dashboardView,
            PlaceController placeController) {
        super(placeController, place);
        this.dashboardView = dashboardView;

    }

    @Override
    public void start(AcceptsOneWidget panel, final EventBus eventBus) {
        panel.setWidget(dashboardView.asWidget());

        final AsyncDataProvider&lt;CustomerProxy&gt; dataProvider = new AsyncDataProvider&lt;CustomerProxy&gt;() {

            @Override
            protected void onRangeChanged(HasData&lt;CustomerProxy&gt; display) {
                getRequestFactory(eventBus).customerRequest().findAllCustomers().with("address")
                        .fire(new Receiver&lt;List&lt;CustomerProxy&gt;&gt;() {
                            @Override
                            public void onSuccess(List&lt;CustomerProxy&gt; response) {
                                dashboardView.getCustomers().setRowData(0, response);
                            }
                        });
            }
        };

        Range range = dashboardView.getCustomers().getVisibleRange();
        dashboardView.getCustomers().setVisibleRangeAndClearData(range, true);
        dataProvider.addDataDisplay(dashboardView.getCustomers());

        getRequestFactory(eventBus).invoiceRequest().findOutstandingInvoiceTotal().fire(
                new Receiver&lt;Double&gt;() {

                    @Override
                    public void onSuccess(Double response) {
                        dashboardView.getOutstandingInvoiceTotal().setText(response.toString());
                    }
                });

        getRequestFactory(eventBus).invoiceRequest().findPaidInvoiceTotal().fire(
                new Receiver&lt;Double&gt;() {

                    @Override
                    public void onSuccess(Double response) {
                        dashboardView.getPaidInvoiceTotal().setText(response.toString());
                    }
                });
        
        String moduleUrl = getModuleUrl();

        place.getBreadCrumbs().clear();
        place.addBreadCrumb(new BreadCrumb("Aceevo Books", null));
        place.addBreadCrumb(new BreadCrumb("Dashboard", moduleUrl + "#DashboardPlace"));

        eventBus.fireEvent(new BreadCrumbChangeEvent(dashboardView));

    }

}

To populate our left hand customer list we create an AsyncDataProvider and override the onRangeChange method, we then use our getRequestFactory(EventBus eventBus) method to retrieve a request factory and the customer request stub and call the findAllCustomers() method.

Notice we call the findAllCustomers() method using the with("address") helper method that tell the request factory to retrieve and populate the Address value objects that are references contained within our server side customer object, without this the address references would be null. We then call fire and in our onSuccess(List response) method we retrieve the Customer CellList from the dashboard view and call setRowData() to populate it with the list of customers.

@Override
    public void start(AcceptsOneWidget panel, final EventBus eventBus) {
        panel.setWidget(dashboardView.asWidget());

        final AsyncDataProvider&lt;CustomerProxy&gt; dataProvider = new AsyncDataProvider&lt;CustomerProxy&gt;() {

            @Override
            protected void onRangeChanged(HasData&lt;CustomerProxy&gt; display) {
                getRequestFactory(eventBus).customerRequest().findAllCustomers().with("address")
                        .fire(new Receiver&lt;List&lt;CustomerProxy&gt;&gt;() {
                            @Override
                            public void onSuccess(List&lt;CustomerProxy&gt; response) {
                                dashboardView.getCustomers().setRowData(0, response);
                            }
                        });
            }
        };

        Range range = dashboardView.getCustomers().getVisibleRange();
        dashboardView.getCustomers().setVisibleRangeAndClearData(range, true);
        dataProvider.addDataDisplay(dashboardView.getCustomers());

We then set the visible range on the Customer Cell List and add the Cell List as a data display to the AsycDataProvider.

The other two calls to the RequestFactory populate the right hand side dashboard Outstanding and Paid Invoice totals via calls to the server side Customer object through the CustomerRequest object. Finally we clear out the breadcrumbs in our current place and construct a new BreadCrumb that reflects our current navigation with the application (the Dashboard Page) and fire a new BreadCrumbChangeEvent that will be handled by the BreadCrumbChangeEventHandler we implemented in the AceevoBooksActivityMapper which will generically update our breadcrumb for us.

getRequestFactory(eventBus).invoiceRequest().findOutstandingInvoiceTotal().fire(
                new Receiver&lt;Double&gt;() {

                    @Override
                    public void onSuccess(Double response) {
                        dashboardView.getOutstandingInvoiceTotal().setText(response.toString());
                    }
                });

        getRequestFactory(eventBus).invoiceRequest().findPaidInvoiceTotal().fire(
                new Receiver&lt;Double&gt;() {

                    @Override
                    public void onSuccess(Double response) {
                        dashboardView.getPaidInvoiceTotal().setText(response.toString());
                    }
                });
        
        String moduleUrl = getModuleUrl();

        place.getBreadCrumbs().clear();
        place.addBreadCrumb(new BreadCrumb("Aceevo Books", null));
        place.addBreadCrumb(new BreadCrumb("Dashboard", moduleUrl + "#DashboardPlace"));

        eventBus.fireEvent(new BreadCrumbChangeEvent(dashboardView));

    }

AceevoBooksActivityMapper.java

...

eventBus.addHandler(BreadCrumbChangeEvent.TYPE, new BreadCrumbChangeEventHandler() {

            @Override
            public void onBreadCrumbChange(BreadCrumbChangeEvent breadCrumbChangeEvent) {
                Place place = placeController.getWhere();
                if (place instanceof AceevoBooksPlace) {
                    AceevoBooksPlace aceevoBooksPlace = (AceevoBooksPlace) place;
                    SafeHtmlBuilder safeHtmlBuilder = new SafeHtmlBuilder();
                    List&lt;BreadCrumb&gt; breadCrumbs = aceevoBooksPlace.getBreadCrumbs();

                    for (int i = 0; i < breadCrumbs.size(); i++) {
                        BreadCrumb breadCrumb = breadCrumbs.get(i);
                        String key = breadCrumb.getKey();
                        String href = breadCrumb.getHref();
                        if (href == null) {
                            safeHtmlBuilder.appendEscaped(key);
                        } else {
                            safeHtmlBuilder.appendHtmlConstant("<a href=\"" + href + "\">" + key
                                    + "</a>");
                        }

                        if (i < breadCrumbs.size() - 1) {
                            safeHtmlBuilder.appendHtmlConstant(" > ");
                        }
                    }
                    breadCrumbChangeEvent.getBreadCrumbView().setBreadCrumb(
                            new HTML(safeHtmlBuilder.toSafeHtml().asString()));
                }
            }
        });

...

Implementing the Dashboard View and Styling CellLists

Using the MVP pattern we implement an interface called DashboardView.java where we extend AceevoBooksView and BreadCrumbView and define the getters for our view widgets. This way we can easily mock the interface for Unit testings.

DashboardView.java

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

import com.aceevo.example.aceevobooks.client.model.CustomerProxy;
import com.aceevo.example.aceevobooks.client.view.AceevoBooksView;
import com.aceevo.example.aceevobooks.client.view.BreadCrumbView;
import com.google.gwt.user.client.ui.HasText;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.view.client.HasData;

public interface DashboardView extends AceevoBooksView, BreadCrumbView {

    HasData&lt;CustomerProxy&gt; getCustomers();

    HasWidgets getInvoiceDetails();

    HasText getOutstandingInvoiceTotal();

    HasText getPaidInvoiceTotal();
}

We're using UiBinder so we provide a DashboardViewImpl.java and a DashboardViewImpl.ui.xml

DashboardViewImpl.java

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

import com.aceevo.example.aceevobooks.client.customer.event.CustomerAddEvent;
import com.aceevo.example.aceevobooks.client.model.CustomerProxy;
import com.aceevo.example.aceevobooks.client.view.AbstractAceevoBooksPage;
import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
...

public class DashboardViewImpl extends AbstractAceevoBooksPage implements DashboardView {

    interface DashboardViewUiBinder extends UiBinder&lt;HTMLPanel, DashboardViewImpl&gt; {
    }

    private static DashboardViewUiBinder dashboardViewUiBinder = GWT
            .create(DashboardViewUiBinder.class);

    @UiField
    CellList&lt;CustomerProxy&gt; customerList;

    @UiField
    Button addCustomer;

    @UiField
    VerticalPanel dashboardInvoiceDetail;

    @UiField
    Label outstandingInvoiceTotal;

    @UiField
    Label paidInvoiceTotal;

    @Inject
    EventBus eventBus;

    @Inject
    PlaceController placeController;

    @UiFactory
    CellList&lt;CustomerProxy&gt; makeCustomerList() {
        return new CellList&lt;CustomerProxy&gt;(new CustomerCell(), CustomerCellListResources.INSTANCE);
    }

    public DashboardViewImpl() {
        initWidget(dashboardViewUiBinder.createAndBindUi(this));
        setBreadCrumb(new HTML("Aceevo Books > <a href='#'> Dashboard</a>"));
    }


    @Override
    public HasData&lt;CustomerProxy&gt; getCustomers() {
        return customerList;
    }

    /**
     * The Cell used to render a {@link ContactInfo}.
     */
    static class CustomerCell extends AbstractCell&lt;CustomerProxy&gt; {

        /**
         * The html of the image used for contacts.
         */

        public CustomerCell() {

        }

        @Override
        public void render(Context context, CustomerProxy value, SafeHtmlBuilder sb) {
            // Value can be null, so do a null check..
            if (value == null) {
                return;
            }

            sb.appendHtmlConstant("<div class=\"customerDashboardWidget\">");
            sb.appendHtmlConstant("<div class=\"name\">");

            String moduleUrl;
            if (Location.getHref().indexOf("#") == -1) {
                moduleUrl = Location.getHref();
            } else {
                moduleUrl = Location.getHref().substring(0, Location.getHref().indexOf("#"));
            }
            String newUrl = moduleUrl + "#" + "ViewCustomer:" + value.getId() + "!All";

            sb.appendHtmlConstant("<a href=\"" + newUrl + "\">");
            sb.appendEscaped(value.getName());
            sb.appendHtmlConstant("</a>");
            sb.appendHtmlConstant("</div>");
            sb.appendHtmlConstant("<div class=\"address\">");
            sb.appendEscaped(value.getAddress().getAddress());
            sb.appendHtmlConstant("<br/>");
            sb.appendEscaped(value.getAddress().getCity() + ", " + value.getAddress().getState()
                    + " " + value.getAddress().getZip());
            sb.appendHtmlConstant("</div>");

            sb.appendHtmlConstant("</div>");
        }
    }

    @Override
    public HasWidgets getInvoiceDetails() {
        return dashboardInvoiceDetail;
    }

    @Override
    public HasText getOutstandingInvoiceTotal() {
        return outstandingInvoiceTotal;
    }

    @Override
    public HasText getPaidInvoiceTotal() {
        return paidInvoiceTotal;
    }

    @UiHandler("addCustomer")
    public void onAddCustomerClick(ClickEvent e) {
        eventBus.fireEvent(new CustomerAddEvent());
    }

}

(Note: Some of the pre formatting is wacky in the ui.xml files, make sure you get the real copy from github.)

DashboardViewImpl.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">

    <ui:style>
        .summaryLabel {
            font-weight: bold;
            margin: 5px 0px;
            font-size: 1.4em;
        }
        
        .outstandingTotal {
            font-weight: bold;
            color: red;
            font-size: 1.2em;
        }
        
        .paidTotal {
            font-weight: bold;
            color: green;
            font-size: 1.2em;
        }
        
        .outstandingTotal:before,.paidTotal:before {
            content: "$";
        }
    </ui:style>

    <g:HTMLPanel>
        <my:HeaderView ui:field="headerView"></my:HeaderView>
        <div class="mainWrapper">
            <div class="customerWrapper">
                <div class="h2Wrapper">
                    <h2 class="h2WithButtons">Customers</h2>
                    <g:Button ui:field="addCustomer" styleName="btn">Add Customer
                    </g:Button>
                    <div class="clearfix" />
                </div>
                <c:CellList ui:field="customerList"></c:CellList>
                <div class="clearfix"></div>
            </div>
            <div class="summaryWrapper">
                <div class="h2Wrapper">
                    <h2>Invoice Summary</h2>
                    <div class="clearfix" />
                </div>
                <g:VerticalPanel ui:field="dashboardInvoiceDetail">
                    <g:Label addStyleNames='{style.summaryLabel}'>Outstanding Invoices Total</g:Label>
                    <g:Label addStyleNames='{style.outstandingTotal}' ui:field="outstandingInvoiceTotal"></g:Label>
                    <g:Label addStyleNames='{style.summaryLabel}'>Paid Invoices Total</g:Label>
                    <g:Label addStyleNames='{style.paidTotal}' ui:field="paidInvoiceTotal"></g:Label>
                </g:VerticalPanel>
                <br />
                <div class="clearfix"></div>
            </div>
            <div class="clearfix"></div>
        </div>
    </g:HTMLPanel>
</ui:UiBinder>

CellList Styling

In order to customize our CellList we need to use the @UIFactory annotation and pass our customized AbstractCell implementation called CustomerCell and our CustomerCellListResources which extends CellList.Resources

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

import com.aceevo.example.aceevobooks.client.customer.event.CustomerAddEvent;
import com.aceevo.example.aceevobooks.client.model.CustomerProxy;


public class DashboardViewImpl extends AbstractAceevoBooksPage implements DashboardView {

...
    @UiField
    CellList&lt;CustomerProxy&gt; customerList;


    @UiFactory
    CellList&lt;CustomerProxy&gt; makeCustomerList() {
        return new CellList&lt;CustomerProxy&gt;(new CustomerCell(), CustomerCellListResources.INSTANCE);
    }

...

/**
     * The Cell used to render a {@link ContactInfo}.
     */
    static class CustomerCell extends AbstractCell&lt;CustomerProxy&gt; {

        /**
         * The html of the image used for contacts.
         */

        public CustomerCell() {

        }

        @Override
        public void render(Context context, CustomerProxy value, SafeHtmlBuilder sb) {
            // Value can be null, so do a null check..
            if (value == null) {
                return;
            }

            sb.appendHtmlConstant("<div class=\"customerDashboardWidget\">");
            sb.appendHtmlConstant("<div class=\"name\">");

            String moduleUrl;
            if (Location.getHref().indexOf("#") == -1) {
                moduleUrl = Location.getHref();
            } else {
                moduleUrl = Location.getHref().substring(0, Location.getHref().indexOf("#"));
            }
            String newUrl = moduleUrl + "#" + "ViewCustomer:" + value.getId() + "!All";

            sb.appendHtmlConstant("<a href=\"" + newUrl + "\">");
            sb.appendEscaped(value.getName());
            sb.appendHtmlConstant("</a>");
            sb.appendHtmlConstant("</div>");
            sb.appendHtmlConstant("<div class=\"address\">");
            sb.appendEscaped(value.getAddress().getAddress());
            sb.appendHtmlConstant("<br/>");
            sb.appendEscaped(value.getAddress().getCity() + ", " + value.getAddress().getState()
                    + " " + value.getAddress().getZip());
            sb.appendHtmlConstant("</div>");

            sb.appendHtmlConstant("</div>");
        }
    }

CustomerCellListResources and customerCellListStyle.css

We need to tell our CellList where to find it's styling and this is accomplished in the CustomerCellListResources interface, we create a new singleton instance using GWT create and then use the @Source annotation on the cellListStyle() method to direct our CellList to the customerCellListStyle.css file.

CustomerCellListResources.java

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

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.cellview.client.CellList;
import com.google.gwt.user.cellview.client.CellList.Style;

public interface CustomerCellListResources extends CellList.Resources {

    public static CustomerCellListResources INSTANCE = GWT.create(CustomerCellListResources.class);

    @Override
    @Source("customerCellListStyle.css")
    public Style cellListStyle();
}

customerCellListStyle.css

.cellListEvenItem {
    -moz-border-radius: 15px;
    border-radius: 15px;
    background-color: #EEE;
    margin: 5px 0px;
    padding: 10px;
    text-shadow: 0 1px 0 #FFFFFF;
}

.cellListOddItem {
    -moz-border-radius: 15px;
    border-radius: 15px;
    background-color: #CCC;
    margin: 5px 0px;
    padding: 10px;
    text-shadow: 0 1px 0 #FFFFFF;
}

.cellListWidget {
    margin-top:20px;   
}

.cellListSelectedItem {

}

.cellListKeyboardSelectedItem {
    
}

Adding a new Customer

We also need to wire up our Add Customer button we do that in the DashboardViewImpl.java by using the EventBus and the @UIHandler annotation to fire a CustomerAddEvent

DashboardViewImpl.java

...
@UiHandler("addCustomer")
    public void onAddCustomerClick(ClickEvent e) {
        eventBus.fireEvent(new CustomerAddEvent());
    }

and then in AceevoBooksHistoryMapper.java we provide a Handler for the CustomerAddEvent where we create an instance of the ViewCustomer place object and call placeController.goTo(viewCustomer)

AceevoBooksActivityMapper.java

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

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

In part 3 we will go into expand on the GWT editor framework and validation, create an abstract reusable editor with built in validation styling and integrate it with the RequestFactory persistence API's. We'll also cover CellTables and create a table driven editor for our invoices.

comments powered by Disqus