GWT is a true gem in the world of open-source slosh where most products are shiny gimmicks with little or no added value. GWT accomplishes this mind-boggling feat of compiling Java source including much of Java's core classes into JavaScript/HTML perfection. JavaScript/HTML being this world of icky sludge that many (like me) would rather avoid stepping into with our tidy feet, maintaining our elitist staticky Object-Oriented (OO) attitude to everything while building great Rich Internet Applications (RIAs) that can out-rich most other RIAs. We can now use our IDEs to navigate and refactor across our entire tidy codebase and actually maintain our Apps for the long-term across a large team. We can now apply a more uniform OO design across our entire application as sharing of Java source across tiers is now possible. Componentization of UI into reusable widgets is now just a matter of OO design.
Of course, Its not all peaches and cream with this hot and sexy GWT - waiting for GWT to finish can shrivel up most anybody's confidence. I outlined some of the issues with GWT in an earlier Article - the biggest being the relatively long compile waits - something you learn to compensate for in other ways. Also to the contrary, you still need to understand HTML and CSS pretty well to use GWT effectively.
Outlined are some of the key Concepts and Stereotypes we employ across our GWT and JEE Application. Code samples and Diagrams are shown after the outline.
Inversion Of Control (IoC)
IoC is a concept we employ across our JEE Application including within our GWT tier for maximum separation of concerns. There are many options for IoC available for Java in general but hardly any for GWT where everything is statically analyzed upfront in compilation and Java Reflection cannot be used. In fact, I would have said that none were available at the time of this writing, except I was corrected that Google GIN based on Google Guice was on its way to being released. Luckily we rely on a design pattern for IoC which worked perfectly for our GWT application needs as well.- Use the Context IoC Design Pattern.
- See Article: Context IoC Revisited.
- Important Constraints:
- Browser-side bindings must be distinct from Server-side bindings.
- They may share common Java source e.g. DomainBeans/Models.
- Browser-side bindings must be GWT compilable.
- Browser-side bindings must be scoped to a GWT Module.
- Server-side bindings must be scoped to a User's Session.
Event & EventManager
We initially introduced an EventManager to fire and listen to Events and elegantly manage the Asynchronous nature of Browser-side work. Later We extended this to also process Events Synchronously as well which could then be leveraged Server-Side. Finally, tired of the tediousness of GWT-RPC, We decided to just connect the two sides by introducing the RemoteEvent Framework which transparently delivered RemoteRequestEvents sub-types fired locally Browser-side to the Server-Side and returned the response back to the Browser and fired for asynchronous listeners of the response. This greatly enhanced and simplified our RPC - Panels now merely fired and listened to Events for Server-side processing. We also now had a unified and universal Event Management approach across all tiers. Finally, We also introduced a ViewEvent that could be sub-typed for easy tokenization and browser history management when fired.
- Events capture interesting Actions or Requests or Responses within your System.
- Events can be fired or listened to for Asynchronous processing using a local EventManager obtained via IoC.
- Events of type RequestEvent can be processed Synchronously using a local EventManager obtained via IoC.
- Synchronous processing of an Event provides an immediate ResponseEvent sub-type returned to the caller.
- Events fired of type RemoteRequestEvent will be remotely processed Asynchronously via the RemoteEvent Framework.
- The correct sub-type of RemoteResponseEvent is returned and fired locally upon completion of Server-side processing.
- See section: RemoteControl, RemoteEvent & RemoteEvent Framework.
- Events can capture UI Actions (e.g. Resize, ViewCart), User Actions (e.g Submit) or Usecase Actions (e.g. PurchaseItems).
- Can be triggered via a User's action or internally fired as part of Application flow.
- Events define their Listener as an inner-interface unique to it, to be implemented by interested listeners.
- The Listener's onEvent() signature must accept the exact type of Event.
- RequestEvent subtypes define their Processor as an inner-interface unique to it, to be implemented by interested Processors.
- The Processor's process() signature must accept the exact type of RemoteResponseEvent.
- Events should NOT access state static or otherwise external to themselves or their composition.
- Events should NOT use IoC or reference other Stereotypes and should be constructable.
- Events should NOT fire, listen or process other Events or access the EventManager via IoC.
Panel & Widget
- Panels and Widgets live solely on the Client-side (Browser-side).
- All Panels concerned with Application flow should be CustomPanels (extends CustomPanel).
- CustomPanels render rich visual user application flow - built using GWT's Panels and Widgets.
- CustomPanels can maintain presentation state on behalf of the user, but should refrain from holding domain state.
- CustomPanels access domain state via Models obtained via IoC. They may also update these Models.
- CustomPanels may access various Formatters and similar types of utilities via IoC.
- CustomPanels may access the EventManager via IoC and fire | listen Events.
- CustomPanel base class provides convenience methods to fire | listen Events.
- CustomPanels should avoid accessing other Stereotypes via IoC that are not mentioned in this section.
- CustomPanels should explicitly destroy children that it no longer requires.
- Rule of thumb: If re-assigning a member field then destroy() it.
- CustomPanel base class provides convenience methods to destroy(Widget) including children.
- Proper destruction ensures that all CustomPanels attached to the EventManager are de-registered.
- A CustomPanel can optionally choose to destroy() itself and then re-initalize itself.
- Note: CustomPanel can re-register for Events it was listening to before its destruction.
- Utility components not concerned with Application specific flow may directly extend GWT's Panel, Composite or Widget.
- Utility components should NOT access the EventManager or other Stereotypes via IoC.
- CustomPanel designers should define appropriate ViewEvent subtypes to activate their Panels.
- To activate their Panel listen for their specific ViewEvents and fire an appropriately configured BorderUpdateEvent.
- ViewEvent base class provides facilities to tokenize an event for Browser History management.
Model
- Models capture state as a closed composition of DomainBeans, their associations, and operations that apply solely on itself and its children.
- Models may be composed of other Models and may operate on them as well.
- Models should NOT access state static or otherwise external to themselves or their composition.
- Models should NOT use IoC to reference other Stereotypes.
- Models should NOT fire, listen or process Events nor access the EventManager via IoC.
- Models should always be transportable and valid/equal on all tiers including Client-tier.
- Models should be constructable on any tier including Client-side with a life-span determined by its scope.
- Models may be referenced by other Stereotypes via IoC if it is a shared instance of user session scope.
RemoteControl, RemoteEvent & RemoteEvent Framework
- RemoteControls live solely on the Server-side and replaces the former stereotype Control.
- RemoteControls (like the former Control) process domain actions and maintain domain state on behalf of the User.
- RemoteControls (like the former Control) are Stateful and hold domain state on behalf of the User.
- Either internally or via one or more shared Models.
- RemoteControls can be accessed by other Server-Side Stereotypes via IoC.
- RemoteControls can process RemoteRequestEvents fired Client-side and delivered Server-side via the RemoteEvent Framework.
- They must register as a Processor for the exact sub-type of a RemoteRequestEvent with their local EventManager.
- Events extending RemoteRequestEvent when fired Client-side will automatically be serialized and delivered to the registered Processor Server-side.
- RemoteControls produce RemoteReponseEvents sub-types to be delivered via the RemoteEvent Framework back to Listeners Client-side.
- Events extending RemoteReponseEvent will be automatically serialized and delivered to the registered Listeners Client-side.
- CustomPanels interested in responses to fired events should register themselves as listeners with their local EventManager.
Service, Store, DomainBean
- Services and Stores live solely on the Server-side.
- Services are always Stateless and implement transaction-level Usecases.
- Stores are always Stateless and implement CRUD operations for DomainBeans residing in a Repository.
- DomainBeans should always be transportable and valid/equal on all tiers including Client-side.
- DomainBeans should be constructable on any tier including Client-side with a life-span determined by its scope.
Diagrams & Code Samples
Disclaimer: Diagrams & Code Samples are a mashup of actual content slashed up and modified to use the mocked sample of Shopping. If it doesn't all make sense, I apologize, the parts are there to convey the approach even if the sample as a whole doesn't always come together.
Local Events & Panels
Remote Events & RemoteControls
Code Samples:
Disclaimer: Diagrams & Code Samples are a mashup of actual content slashed up and modified to use the mocked sample of Shopping. If it doesn't all make sense, I apologize, the parts are there to convey the approach even if the sample as a whole doesn't always come together.
/** * Sample ViewEvent * * Fired and used Browser-side only. * Note: Extend Event.class directly for non-history managed events. */ public class ViewProductSearchEvent extends ViewEvent { /** * Define a Listener for your event. * Always cookie-cutter, just change event class name. */ public interface Listener extends Event.Listener { public void onEvent(ViewProductSearchEvent e); } /** * Define a method to delegate to your Listener by exact type. * Always cookie-cutter, just change event class name. */ @Override public void fire(Event.Listener listener) { ((ViewProductSearchEvent.Listener)listener).onEvent(this); } //optionally override methods set/getTokenProps() //for additional history token state. } |
/** * Sample RemoteRequestEvent. * * Fired Browser-side and serialized to Server-side. */ public class FindProductsEvent extends RemoteRequestEvent { /** * Define a Listener for your event. * Always cookie-cutter, just change event class name. */ public interface Listener extends Event.Listener { public void onEvent(FindProductsEvent e); } /** * Define a method to delegate to your Listener by exact type. * Always cookie-cutter, just change event class name. */ @Override public void fire(Event.Listener listener) { ((FindProductsEvent.Listener)listener).onEvent(this); } /** * Define a Processor for your event (for synchronous processing). * Specify exact type of your request and response events. * Always cookie-cutter, just change event class names. */ public interface Processor extends RequestEvent.Processor { public FindProductsDoneEvent process(FindProductsEvent e); } /** * Define a method to delegate to your Processor by exact type * and return the exact type of your response. * Note: Mediator is a simple interface to access the Processor of an Event type. * Your Local EventManager implements this Mediator interface. * Always cookie-cutter, just change event class names. */ @Override public FindProductsDoneEvent process(RequestEvent.Mediator mediator) { return ((FindProductsEvent.Processor)(mediator.getProcessor(this))).process(this); } private String searchText = null; /** * Not shown: GWT Serialization also requires a default constructor. */ public FindProductsEvent(String searchText) { this.searchText = searchText; } public String getSearchText() { return searchText; } /** * RequestEvents must implement, used for cache-able responses. */ @Override public String getKey() { return new ToStringBuilder(getClass()) .append("searchText", searchText) .toString(); } } |
/** * Sample RemoteResponseEvent. * * Serialized after Server-side processing back to Browser and fired. */ public class FindProductsDoneEvent extends RemoteResponseEvent { /** * Define a Listener for your event. * Always cookie-cutter, just change event class name. */ public interface Listener extends Event.Listener { public void onEvent(FindProductsDoneEvent e); } /** * Define a method to delegate to your Listener by exact type. * Always cookie-cutter, just change event class name. */ @Override public void fire(Event.Listener listener) { ((FindProductsDoneEvent.Listener)listener).onEvent(this); } private List<Products> result = null; private ProductSearchException ex = null; /** * Success with result. * Not shown: GWT Serialization also requires a default constructor. */ public FindProductsDoneEvent(String requestKey, List<Product> result) { super(requestKey, Status.Success); this.results = results; } /** * Failed with exception. */ public FindProductsDoneEvent(String requestKey, ProductSearchException x) { super(requestKey, Status.Fail); this.ex = x; } public List<Product> getResult() { return results; } public ProductSearchException getException() { return ex; } } |
/** * Sample Panel for shopping. * * Base CustomPanel class provides following shortcuts: * listen(eventClass) = * cxt.getEventManager().addListener(eventClass, this); * fire(event) = * cxt.getEventManager().fire(event); */ public class ShoppingPanel extends CustomPanel implements ViewShoppingEvent.Listener, ViewProductSearchEvent.Listener LoginDoneEvent.Listener, FindProductsDoneEvent.Listener, AddToCartDoneEvent.Listener, PurchaseDoneEvent.Listener { /** * Define my IoC dependencies. * Note CustomPanel.Context already specifies a dependency to EventManager. */ public interface Context extends CustomPanel.Context { public ShoppingModel getShoppingModel(); } private final Context cxt; private Widgets widgets = null; /** * Construct with my IoC Context defined above. */ public ShoppingPanel(Context cxt) { super(cxt); this.cxt = cxt; } /** * Build my panel. */ @Override public CustomPanel build() { if (isBuilt()) { return this; } //use base class shortcuts to register as listeners of events. listen(ViewShoppingEvent.class); listen(ViewProductSearchEvent.class); listen(LoginDoneEvent.class); listen(FindProductsDoneEvent.class); listen(AddToCartDoneEvent.class); listen(PurchaseDoneEvent.class); //I've put my widgets into an inner class for easy manage. widgets = new Widgets(); //Will be built later as product results are added widgets.productResults.table.pad(5).space(0).width("100%").css("shoppingTable"); //x(),y() are fluent api for Horizontal/Vertical layout (= x/y axis). widgets.main.width("100%").css("shoppingMain") .x(widgets.breadcrumb).id("breadCrumb").width("100%").q() .y().width("100%").css("shoppingSearch") .x().stretch().align(-1,0).space(5) .put(widgets.productSearch.searchText) .put(widgets.productSearch.search) .q() .x().stretch().align(1,0).space(5) .put(widgets.newSearch) .put(widgets.print) .q() .q() .x(horizontalLine()).width("100%").style("margin", "1em 0").q() .x(widgets.productResults.table).width("100%").q() .x(widgets.productResults.viewAlternatives).width("100%").align(1,0).space(6).q() ;//end add(new CSSPanel().css("shoppingDecor").wrap(widgets.main)); //Sample fire of RemoteRequestEvent for Server-side processing. widgets.productSearch.search.addClickHandler(new ClickHandler() { public void onClick(ClickEvent e) { fire(new FindProductsEvent(widgets.productSearch.searchText.getText()); fire(new ShowWorkingPopupEvent("Searching Products")); } }); return this; } /** * Sample: handle Event to display myself. */ public void onEvent(ViewShoppingEvent e) { fire(new BorderUpdateEvent(this)); } /** * Sample: handle Event to display self * with product search. */ public void onEvent(ViewProductSearchEvent e) { if (e.getSearchText() != null) { fire(new FindProductsEvent(e.getSearchText())); fire(new ShowWorkingPopupEvent("Searching Products")); } fire(new BorderUpdateEvent(this)); } /** * Sample: handle server response to product search. */ public void onEvent(FindProductsDoneEvent e) { fire(new HideWorkingPopupEvent()); if (e.isFail()) { errorAlert(e); return; } cxt.getShoppingModel().addProductsSearched(e.getSearchText(), e.getResult()); updateProductResultsView(); } /** * Sample: handle server response to cart add. */ public void onEvent(AddToCartDoneEvent e) { if (e.isFail()) { errorAlert(e); return; } cxt.getShoppingModel().addToCart(e.getResult()); updateCartView(); } /** * Sample: handle server response to purchasing. */ public void onEvent(PurchaseDoneEvent e) { if (e.isFail()) { errorAlert(e); return; } cxt.getShoppingModel().setOrder(e.getOrder()); cxt.getShoppingModel().clearCart(); updateOrderView(); } /** * Sample: handle server response to login. */ public void onEvent(LoginDoneEvent e) { if (e.isFail()) { errorAlert(e); return; } cxt.getShoppingModel().resetCart(); } // //Collecting my widgets into inner classes for easy create/destroy/manage. //These widgets are spread across a giant table due to biz requirements. //Refactor into independent panels if can standalone. // private class Widgets { BreadCrumbPanel breadcrumb = new BreadCrumbPanel(); Button newSearch = new Button("NEW SEARCH"); Button print = new Button("PRINT"); ProductSearchWidgets productSearch = new ProductSearchWidgets(); List<ProductResultWidgets> productResults = new ProductResultWidgets(); AxisPanel main = new AxisPanel(); } private class ProductSearchWidgets { TextBox searchText = new TextBox(); Button search = new Button(); } private class ProductResultWidgets { ProductWidgets mainProduct = new ProductWidgets(); ProductWidgets similarProduct = new ProductWidgets(); Button viewAlternatives = new Button("VIEW ALTERNATIVES"); AxisTable table = new AxisTable(); } private class ProductWidgets { ProductDescWidgets desc = new ProductDescWidgets(); ProductCostsWidgets pricing = new ProductPricingWidgets(); } private class ProductDescWidgets { Label name = new Label("", false); Label brand = new Label("", false); void setDesc(Product product) { name.setText(product.getName()); brand.setText(product.getBrand()); } } private class ProductCostsWidgets { CSSPanel productCost = new CSSPanel(); CSSPanel shippingCost = new CSSPanel(); CSSPanel savings = new CSSPanel(); void setCosts(Product product, Pricing pricing) { if (pricing != null) { productCost.put(new Label("$" + pricing.getProductCost())); shippingCost.put(new Label("$" + pricing.getShippingCost())); savings.put(new Label("$" + pricing.getSavings())); } } } // //More code..more widgets, updating widgets etc. // } |
/** * A Sample RemoteControl. * * Base RemoteControl class provides the following shortcuts: * process(eventClass) = * cxt.getEventManager().addProcessor(eventClass, this); * listen(eventClass) = * cxt.getEventManager().addListener(eventClass, this); * fire(event) = * cxt.getEventManager().fire(event); */ public class ShoppingRemoteControlImpl extends RemoteControl implements ShoppingRemoteControl, FindProductsEvent.Processor, AddToCartEvent.Processor, PurchaseEvent.Processor, LoginDoneEvent.Listener { /** * Define my IoC dependencies. * Note RemoteControl.Context already specifies a dependency to EventManager. */ public interface Context extends RemoteControl.Context { public UserRemoteControl getUserRemoteControl(); public ProductSearchService getProductSearchService(); public OrderService getOrderService(); public CartService getCartService(); } private final Context cxt; private Cart cart = null; /** * Construct with the IoC context defined above. */ public ShoppingRemoteControlImpl(Context cxt) { super(ctx); this.cxt = cxt; this.cart = new Cart(); //register as processor of events using base class shortcuts. process(FindProductsEvent.class); process(AddToCartEvent.class); process(PurchaseEvent.class); //register as listener of events using base class shortcuts. listen(LoginDoneEvent.class); } /** * process request arriving remotely. */ public AddToCartDoneEvent process(AddToCartEvent e) { this.addToCart(e.getProduct(), e.getQuantity()); return new AddToCartDoneEvent(e.getKey()); } /** * process request arriving remotely. */ public FindProductsDoneEvent process(FindProductsEvent e) { List<Products> products = this.findProducts(e.getSearchText()); return new FindProductsDoneEvent(e.getKey(), products); } /** * process request arriving remotely. */ public PurchaseDoneEvent process(PurchaseEvent e) { Order order = this.purchaseCart(); return new PurchaseDoneEvent(e.getKey(), order); } /** * listen (not process) a request arriving remotely. */ public void onEvent(LoginDoneEvent e) { UserKey userKey = cxt.getUserRemoteControl().getUser().getKey(); this.cart = cxt.getCartService().getLastSavedCart(userKey); } /** * impl of biz interface. */ public List<Products> findProducts(String text) { UserKey userKey = cxt.getUserRemoteControl().getUser().getKey(); List<Products> results = cxt.getProductSearchService().findProducts(userKey, text); return results; } /** * impl of biz interface. */ public Order purchaseCart() { UserKey userKey = cxt.getUserRemoteControl().getUser().getKey(); Order order = cxt.getOrderService().purchase(userKey, this.cart); cxt.getCartService().clearSavedCart(userKey); this.cart = new Cart(); return order; } /** * impl of biz interface. */ public void addToCart(Product product, BigDecimal quantity) { UserKey userKey = cxt.getUserRemoteControl().getUser().getKey(); cxt.getCartService().addToSavedCart(userKey, product, quantity); this.cart.add(product,quantity); } /** * impl of biz interface. */ public void removeFromCart(Product p) { UserKey userKey = cxt.getUserRemoteControl().getUser().getKey(); cxt.getCartService().removeFromSavedCart(userKey, product); this.cart.remove(product); } } |
Conclusion:
Draw your own conclusions!!