Not too long ago, I started using GWT and GWTP and found it pretty hard to understand its event system.
I still can’t find much recent information about it, so I am writing this post to hopefully help others. Disclaimer: There many ways to achieve what I do in this post, but this is the way I prefer.
All right! Let’s demonstrate how events and the event bus work by creating a CSI-Hacking Dashboard. You can get the code on GitHub.
We have ComputerPresenter
, a HackerPresenter
and a root Presenter that holds/creates other widgets. We also have a ComputerHackedEvent
and ComputerHackedHandler
. The event will be raised to signal that the computers got hacked, and the handler code will be executed after that.
Here’s the classic implementation of a GWT event. Our event will only carry the hacker’s name.
public class ComputerHackedEvent extends GwtEvent<ComputerHackedEvent.ComputerHackedHandler> { public interface ComputerHackedHandler extends EventHandler { void onSystemHacked(ComputerHackedEvent event); } public static final Type<ComputerHackedHandler> TYPE = new Type<>(); private final String hackerName; public ComputerHackedEvent(String hackerName) { this.hackerName = hackerName; } public static void fire(String hackerName, HasHandlers source) { source.fireEvent(new ComputerHackedEvent(hackerName)); } public String getHackerName() { return hackerName; } @Override public Type<ComputerHackedHandler> getAssociatedType() { return TYPE; } @Override protected void dispatch(ComputerHackedHandler handler) { handler.onSystemHacked(this); } }
I usually declare my handler as a nested interface in the event declaration. That’s only a matter of personal preference though.
Next we have to raise the event. That will be done by our HackerPresenter
. A really simple way of becoming a computer hacker, is by adding a button to the screen and pressing it, right CSI?
Here’s the code of the view and the presenter.
public class HackerView extends ViewWithUiHandlers<HackerUiHandlers> implements HackerPresenter.MyView { interface Binder extends UiBinder<Widget, HackerView> { } @UiField Button hackThePlanet; @Inject HackerView(Binder binder) { initWidget(binder.createAndBindUi(this)); } @UiHandler("hackThePlanet") public void initiateHacking(ClickEvent event) { getUiHandlers().onInitiateHacking(); } }
public class HackerPresenter extends PresenterWidget<MyView> implements HackerUiHandlers { public interface MyView extends View, HasUiHandlers<HackerUiHandlers> { } private final String hackerName; @Inject HackerPresenter( EventBus eventBus, MyView view, @Assisted String hackerName) { super(eventBus, view); this.hackerName = hackerName; getView().setUiHandlers(this); } @Override public void onInitiateHacking() { ComputerHackedEvent.fire(hackerName, this); } }
So we have a button, and when we click on it, it raises an event. There are a couple of ways to raise an event, but the one I usually use, is the static fire()
method on the event. (I’ll talk about the other ways of firing an event later on.)
Now we have to handle the event somewhere. We want to know when the computers get hacked, so we’ll represent the computers with the ComputerPresenter
. Its role will be to print in the console when it gets hacked, and by which hacker. Here’s the presenter code:
public class ComputerPresenter extends PresenterWidget<MyView> implements ComputerHackedEvent.ComputerHackedHandler { public interface MyView extends View { void setComputerName(String computerName); void displayStatus(String computerName, String hackerName); } private final String computerName; @Inject ComputerPresenter( EventBus eventBus, MyView view, @Assisted String computerName) { super(eventBus, view); this.computerName = computerName; view.setComputerName(computerName); } @Override protected void onBind() { super.onBind(); addRegisteredHandler(ComputerHackedEvent.TYPE, this); } @Override public void onSystemHacked(ComputerHackedEvent event) { getView().displayStatus(computerName, event.getHackerName()); } }
This way, when a hacker clicks on the “start hacking” button, all the computers that are listening to the event will print something. As you can see, the ComputerPresenter
registers itself as a handler for the ComputerHackedEvent
through the addRegisteredHandler
method.
This is a convenience method provided by GWTP. Using this method instead of registering directly on the EventBus will make the event registration part of GWTP’s lifecycle and unbind the event handler when the presenter is unbound. That means that if the presenter is unbound and rebound, you’ll have to re-register event handlers. This is why the onBind method is a good place to register handlers.
Here’s the code of the root presenter:
public class RootPresenter extends Presenter<RootPresenter.MyView, RootPresenter.MyProxy> { interface MyView extends View { } @ProxyStandard @NameToken(NameTokens.home) interface MyProxy extends ProxyPlace<RootPresenter> { } public static final Object SLOT_COMPUTERS = new Object(); public static final Object SLOT_HACKERS = new Object(); private final WidgetsFactory widgetsFactory; @Inject RootPresenter( EventBus eventBus, MyView view, MyProxy proxy, WidgetsFactory widgetsFactory) { super(eventBus, view, proxy, ApplicationPresenter.SLOT_SetMainContent); this.widgetsFactory = widgetsFactory; } @Override protected void onBind() { super.onBind(); HackerPresenter zeroCool = widgetsFactory.createHacker("Zer0C00L"); HackerPresenter acidBurn = widgetsFactory.createHacker("AcidBurn"); addToSlot(SLOT_HACKERS, zeroCool); addToSlot(SLOT_HACKERS, acidBurn); ComputerPresenter computerA = widgetsFactory.createComputer("A"); ComputerPresenter computerB = widgetsFactory.createComputer("B"); addToSlot(SLOT_COMPUTERS, computerA); addToSlot(SLOT_COMPUTERS, computerB); } }
We just create one hacker named Zer0C00L and 2 computers to hack. And now the view:
public class RootView extends ViewImpl implements RootPresenter.MyView { interface Binder extends UiBinder<Widget, RootView> { } @UiField HTMLPanel computers; @UiField HTMLPanel hackers; @Inject RootView(Binder uiBinder) { initWidget(uiBinder.createAndBindUi(this)); } @Override public void addToSlot(Object slot, IsWidget content) { super.addToSlot(slot, content); if (slot == RootPresenter.SLOT_COMPUTERS) { computers.add(content); } else if (slot == RootPresenter.SLOT_HACKERS) { hackers.add(content); } } @Override public void removeFromSlot(Object slot, IsWidget content) { super.removeFromSlot(slot, content); if (slot == RootPresenter.SLOT_COMPUTERS) { computers.remove(content); } else if (slot == RootPresenter.SLOT_HACKERS) { hackers.remove(content); } } }
Nice! When I click on “Hack the planet!” I see the following result in the Javascript console:
I got hacked. (A) by Zer0C00L I got hacked. (B) by Zer0C00L
I should start writing in l33tsp33k now.
Now what if you remove one of the ComputerWidgets from the DOM by calling removeFromSlot(SLOT_COMPUTERS, computerB);
and still try to hack the planet?
If you read the output of the console you will see:
I got hacked. (A) by Zer0C00L I got hacked. (B) by Zer0C00L
Wait… What? The handler for the computer B is still registered, the presenter wasn’t unbound, it was only removed from the DOM.
What if we want computer B to stop listening to the events when it’s not present in the DOM? Well that’s a job for addVisibleHandler
. So instead of registering the handler using addRegisteredHandler
we’ll use addVisibleHandler
that will handle this for us. This way, when a presenter is considered “not visible” in GWTP’s lifecycle perspective (read: not visible as in “visible in the DOM”), the event will not reach the handler. The new output should now be:
I got hacked. (A) by Zer0C00L
There’s still a problem though. What if there were too many computers for a single hacker? I think at some point we’ll have to add someone to the team. Let’s do it!
// in the RootPresenter's OnBind method HackerPresenter acidBurn = widgetsFactory.createHacker("AcidBurn"); addToSlot(SLOT_HACKERS, acidBurn);
You should see 2 buttons saying “Hack the planet!” and when you click them both, the output is:
I got hacked. (A) by Zer0C00L I got hacked. (B) by Zer0C00L I got hacked. (A) by AcidBurn I got hacked. (B) by AcidBurn
All computers are reacting to every hacker, which is not what we want. This is happening because of the way we registered the handlers earlier. What we want is for the computers to react a specific hacker’s ComputerHackedEvent
.
Since we can have a reference to the said hacker, that is pretty easy to accomplish. We have to delegate the handler registration to the concerned presenter. From the RootPresenter
we’ll delegate the task, but first let’s create an interface :
import com.google.web.bindery.event.shared.HandlerRegistration; public interface HasComputerHackedHandlers { HandlerRegistration addComputerHackedHandler(ComputerHackedEvent.ComputerHackedHandler handler, Object source); }
We can then let ComputerPresenter
implement it.
public HandlerRegistration addComputerHackedHandler(ComputerHackedEvent.ComputerHackedHandler handler, Object source) { HandlerRegistration hr = getEventBus().addHandlerToSource(ComputerHackedEvent.TYPE, source, handler); registerHandler(hr); return hr; }
Note that instead of registerHandler()
you can also use registerVisibleHandler()
.
And finally, when you click on both buttons, the output should be:
I got hacked. (A) by Zer0C00L I got hacked. (B) by AcidBurn
All right! We’re ready to hack the planet! Are we?
Remember when I said I would talk about the ways of firing events? If you are new to GWT and GWTP, you might have noticed that there are multiple methods available to fire events and register handlers.
// GWT eventBus.addHandler(eventType, handler) eventBus.addHandlerToSource(eventType, source, handler) eventBus.fireEvent(event) eventBus.fireEventFromSource(event, source) // GWTP presenterWidget.addHandler(eventType, handler) // deprecated presenterWidget.addRegisteredHandler(eventType, handler) presenterWidget.addVisibleHandler(eventType, handler) presenterWidget.registerHandler(handlerRegistration) presenterWidget.registerVisibleHandler(handlerRegistration) presenterWidget.fireEvent(event)
Confused yet? I can say I was after seeing this. If you dig down you can see that a presenter widget gets an event bus injected and delegates most of its job to it. The only difference is that GWTP manages the handler registrations with its lifecycle (i.e: when a presenter gets unbound, the registered handlers get cleared). Also, if you dig for the fireEvent
method, you’ll see GWTP delegates to eventBus.fireEventFromSource()
. You may want to call the original fireEvent()
if you want to match the following case (taken from the javadoc) “Fires the event from no source. Only unfiltered handlers will receive it”. Honestly, I’ve never faced that situation.
Here’s my cheat sheet of events in GWTP:
– Do I need to fire an event globally on the event bus? (i.e: everything registered to the event will handle it)
Y: presenterWidget.addRegisteredHandler()
+ SomeEvent.fire()
N: Go next
– Do I need to filter the handlers by the visibility of the handler?
Y: presenterWidget.addVisibleHandler()
+ SomeEvent.fire()
N: Go next
– Do I need specific handlers to handle events from a specific source?
Y: Create an interface called HasXXXHandlers
and make your handling presenter implement it. GWTP gives PresenterWidget
the capability to register handlers via registerHandler()
and registerVisibleHandler()
. Finally, fire the event with SomeEvent.fire()
N: That’s it. I usually don’t need more options, so the decision tree ends here. If you have another situation that doesn’t fit, let me know!
Great explanation. Thanks for this funny article!
Thanks! It was a lot of fun to write. 🙂