Introduction
In a previous post I showed how to create a custom GWT component based on GWT 1.7. In this post I'll show you how to perform the same thing using GWT 2's new event system. In order to make clear how custom GWT events are built, I'll be doing things slightly differently in that we'll be building it in a Model-Binder-View style.
The Model
The model is where we're going to concentrate on the majority of our effort as this is the component that will be generating the custom events. There's nothing special in the basic model class, it looks like this :
public class ListModel { final private List<ListItem> items = new ArrayList<ListItem>(); public void addItem(ListItem item) { items.add(item); } public void removeItem(ListItem item) { items.remove(item); } public void removeAll() { items.clear(); } public ListgetItems() { return Collections.unmodifiableList(items); } }
Java Tip:Notice here that when we return the list of items, we wrap it in an UnmodifiableList. This makes the list immutable and thus stops any clients modifying the model state outside of our component.
The ListItem class looks like this:
public class ListItem { private final String text; public ListItem(String text) { this.text = text; } public String getText() { return text; } // Equals and HashCode hidden }
In this example, ListItem is simply a wrapper around some text. We could use a string for the ListItem, but this method allows a user of the model to extend ListItem to include any extra required information (for example, adding a map of values).
Adding the HandlerManager
At this point our model isn't very reactive, a client would have to poll the model in order to see any changes. What we'll do now is fire an event when an item is added. In GWT 1.x this had to be done using listeners, however GWT 2 provides out of the box support for handling event registration and notification in the form of the com.google.gwt.event.shared.HandlerManager.
Adding the HandlerManager to our ListModel is straight forward, here's the HandlerManager added to the top of our class:
public class ListModel { final private HandlerManager handlerManager = new HandlerManager(this); final private Listitems = new ArrayList (); // Other code hidden }
Notice that HandlerManager takes an object as it's constructor. The Object passed in is used as the source of all events this HandlerManager will fire.
The ItemAdded event
Events in GWT 2 come in two parts, a class that extends GWTEvent (the event object) and an interface that extends EventHandler (the handler interface). The event handler interface is the easiest to understand as EventHandler is simply a marker interface. The EventHandler for ItemAdded looks like so:
public interface ItemAddedHandler extends EventHandler { void onItemAdded(ItemAddedEvent event); }
The ItemAddedEvent class looks like so:
public class ItemAddedEvent extends GwtEvent<ItemAddedHandler> { private static final TypeTYPE = new Type<ItemAddedHandler>(); private final ListItem listItem; public ItemAddedEvent(ListItem listItem) { this.listItem = listItem; } public static Type getType() { return TYPE; } /** @returns The item added to the model */ public ListItem getListItem() { return listItem; } @Override protected void dispatch(ItemAddedHandler handler) { handler.onItemAdded(this); } @Override public com.google.gwt.event.shared.GwtEvent.Type getAssociatedType() { return TYPE; } }
The event object is slightly confusing because it fulfills two roles. On the one hand it's the object which the ListItem so that the handler can retrieve it when it's notified. On the other hand the event object also acts as the dispatcher for the given event.
Firing the event
Using these two new classes, when an item is added to our model, we can now fire the event. The following code shows the amended addItem method:
public void addItem(ListItem item) { items.add(item); handlerManager.fireEvent(new ItemAddedEvent(item)); }
When fireEvent is called the HandlerManager first calls setSource on the event object and sets it to the value passed into the constructor of the HandlerManager. It will then look for handlers registered against the given event and call them in the order they were registered in.
Registering an EventHandler
At the moment there's no way for a client to listen for the ItemAdded event. In order for this to happen we have to add an addItemAddedHandler to our ListModel class. This looks as follows:
public void addItemAddedHandler(ItemAddedHandler handler) { handlerManager.addHandler(ItemAddedEvent.getType(),handler); }
The Complete Model
Here's the complete ListModel code:
public class ListModel { private final HandlerManager handlerManager = new HandlerManager(this); private final Listitems = new ArrayList<ListItem>(); public void addItem(ListItem item) { items.add(item); handlerManager.fireEvent(new ItemAddedEvent(item)); } public void removeItem(ListItem item) { items.remove(item); handlerManager.fireEvent(new ItemRemovedEvent(item)); } public void removeAll() { for (ListItem item : items) { handlerManager.fireEvent(new ItemRemovedEvent(item)); } items.clear(); } public List getItems() { return Collections.unmodifiableList(items); } public void addItemAddedHandler(ItemAddedHandler handler) { handlerManager.addHandler(ItemAddedEvent.getType(),handler); } public void addItemRemovedHandler(ItemRemovedHandler handler) { handlerManager.addHandler(ItemRemovedEvent.getType(),handler); } }
From this you can see we've added events when an item is removed, and also fire the remove event when all the items are cleared. For completeness, the following show the ItemRemovedHandler and ItemEvent classes:
public interface ItemRemovedHandler extends EventHandler { void onItemRemoved(ItemRemovedEvent event); }
public class ItemRemovedEvent extends GwtEvent<ItemRemovedHandler> { private static final Type<ItemRemovedHandler> TYPE = new Type<ItemRemovedHandler>(); private final ListItem listItem; public ItemRemovedEvent(ListItem listItem) { this.listItem = listItem; } public static Type<ItemRemovedHandler> getType() { return TYPE; } public ListItem getListItem() { return listItem; } @Override protected void dispatch(ItemRemovedHandler handler) { handler.onItemRemoved(this); } @Override public com.google.gwt.event.shared.GwtEvent.Type<ItemRemovedHandler> getAssociatedType() { return TYPE; } }
The List Component
The list component is very much simplified from the previous article, simply being a wrapper around either an LI or OL element:
public class ListComponent extends Widget { private static final String IDENTIFIER_PROPERTY = "IDENTIFIER"; public enum ListType { ORDERED { protected Element createElement() { return Document.get().createOLElement(); } }, UNORDERED { protected Element createElement() { return Document.get().createULElement(); } }; protected abstract Element createElement(); } public ListComponent(ListType type) { setElement(type.createElement()); } public void addItem(String label, Object identifier) { LIElement liElement = Document.get().createLIElement(); liElement.setInnerText(label); liElement.setPropertyObject(IDENTIFIER_PROPERTY, identifier); getElement().appendChild(liElement); } public void removeItem(Object identifier) { Element childNode = getElement().getFirstChildElement(); while (childNode != null) { if (identifier.equals(childNode.getPropertyObject(IDENTIFIER_PROPERTY))) { childNode.removeFromParent(); break; } childNode = childNode.getNextSiblingElement(); } } }
As you can see here, an item consists of a label and an object identifier. The identifier is stored as an Object property in the new LI element so that during remove we can find the given element for a particular indentifier.
Binding the Model to the UI component
With the ListModel finished, we now need something that will register for events and modify a UI component when something is added. For this we'll create a binder which will update a UI list. It looks like this :
public class ListBinder { public ListBinder(final ListComponent list, ListModel listModel) { listModel.addItemAddedHandler(new ItemAddedHandler() { public void onItemAdded(ItemAddedEvent event) { ListItem listItem = event.getListItem(); list.addItem(listItem.getLabel(), listItem); } }); listModel.addItemRemovedHandler(new ItemRemovedHandler() { public void onItemRemoved(ItemRemovedEvent event) { list.removeItem(event.getListItem()); } }); } }
The binder is very simple, in that it converts the events triggered by the model into UI actions. It's useful as it keeps a nice seperation of concerns.
A Quick Test
Finally we come to the fun part, actually testing this. For this I created a simple test client that adds a list item every second:
public class TestClient implements EntryPoint { public void onModuleLoad() { final ListModel model = new ListModel(); final ListComponent list = new ListComponent(ListType.ORDERED); ListBinder binder = new ListBinder(list, model); Timer t = new Timer() { private int counter = 0; public void run() { model.addItem(new ListItem("New Item "+counter++)); } }; t.scheduleRepeating(1000); RootPanel.get().add(list); } }
Conclusion
And there we have it, a simple introduction to creating and using a custom GWT 2 event. I could have gone on and extended the ListComponent to fire events when one of the elements are clicked, but that would have distracted from the main aim of this post.
An Eclipse project with all the code for this post can be found here.
8 comments:
someNode instanceof Element will always be true, as both extends JavaScriptObject. You need to use Element.is(someNode) instead; and/or you could walk the tree using getFirstChildElement/getNextSiblingElement.
BTW, any reason you don't use the generics syntax?
Thanks for your comment, I've amended the removeItem method so that it better iterates through the child element.
As far as I'm aware, Generics are being used everywhere they can be, do you have a specific example?
Eh, the issue is with rendering: the angle brackets aren't escaped and therefore parsed as HTML (unknown) elements.
Ah! Excellent, thanks, I've corrected that now.
Hello David,
it is a nice article - simple and clean example.
I have just found one possible improvement: the addItem*Handler methods should return the HandlerRegistration object returned by the underlying handlerManager. This object provides the possibility to remove the registered handler.
How to register an interested in the event handler from outside of the class containing HandlerManager?
Hey David,
It is a nice, simple, and indeed useful post. Keep posting :)
-Ratul
Hi, this is a very helpful article but I found a bug on it.
Like Gabriel Forro said, you must return a HandlerRegistration when you create an addHandler function.
This is really important because you can't use the @UIHandler annotation with your custom event if you don't do it.
More information on this issue: Issue 4899
Thanks
Post a Comment