Monday, May 6, 2013

GWT Event-Bus in PUC


GWT Event-Bus in PUC


In 5.0 (SUGAR) we have finally made the upgrade from our homegrown listener system to using the GWT Event-Bus.  This was an easy thing to introduce, but it required a decent amount of work to migrate to once in place.  The problem with our existing listener system was that we had to write a new implementation each time we wanted to expose any kind of event from something.  Using the Event-Bus will allow us to cleanup a fair amount of code and greatly simplify how events are handled throughout the system.  We have previously had to write JSNI code in order to bridge any kind of event handling between GWT and JavaScript which was specific to each situation.

How GWT Event-Bus Works


An EventBus must be created, for example:

public static final EventBus EVENT_BUS = GWT.create(SimpleEventBus.class);

We use the same bus for the entire application, but we could actually create additional if needed.  Once we
have the EVENT_BUS available to us, we can fire events and add handlers to the bus:

EVENT_BUS.fireEvent(new MyEvent(x,y,z));

Anyone who has previously registered a handler with the EVENT_BUS for "MyEvent" will get called.  To add a handler to the bus:

EVENT_BUS.addHandler(MyEvent.TYPE, new MyEventHandler() {
  onMyEvent(MyEvent event) {
    // do something
  }
});

That's pretty much all there is to it.  If you are going to be creating your own event types, there is some additional work that you'll have to take care of.

Creating New Events


In PUC, we created about 10 new Event types, if you need to create any additional events, please put them in the org.pentaho.mantle.client.events package (for reasons explained later).  To create your own events, extend GwtEvent and implement two methods:  getAssociatedType and dispatch.  The following example is the event which is fired when the "recent" items list is changed.

public class RecentsChangedEvent extends GwtEvent<RecentsChangedEventHandler> {
  public static Type<RecentsChangedEventHandler> TYPE = new Type<RecentsChangedEventHandler>();
  public static final String TYPE_STR = "RecentsChangedEvent";

  public RecentsChangedEvent() {
  }

  public Type<RecentsChangedEventHandler> getAssociatedType() {
    return TYPE;
  }

  protected void dispatch(RecentsChangedEventHandler handler) {
    handler.onRecentsChanged(this);
  }
}

We have to create a handler interface as well (You can see the use of it in the Event itself).  The handler
interface for "recents" looks like this (these are general quite simple):

public interface RecentsChangedEventHandler extends EventHandler {
  void onRecentsChanged(RecentsChangedEvent event);
}

Bridging GWT + JavaScript


I created an EventBusUtil interface in the org.pentaho.mantle.client.events package for the purpose of bridging GWT and JavaScript.  It also has the actual EVENT_BUS we use throughout the application (PUC).  There interface looks like this:

public interface EventBusUtil {
  public static final EventBus EVENT_BUS = GWT.create(SimpleEventBus.class);
  public void addHandler(String eventType, JavaScriptObject handler);
  public void invokeEventBusJSO(JavaScriptObject handler, Object...params);
  public void fireEvent(String eventType);
}

At compile time, we use a generator to dynamically build an implementation for each of the methods on the interface.  This allows us to make available all of the events we know about to/from JavaScript from GWT.  The alternative would be to write and maintain a method which checks incoming JavaScript string types against known event names.  The result would be fragile, we could easily forget to add new events or break existing ones with trivial changes.  The generator guarantees that the system is always up-to-date and exposes everything we know about, which is every event in org.pentaho.mantle.client.events.  If your events are outside of this package, the generator is not going to pick them up and expose them through JSNI.  The generator itself is in the rebind package of PUC, called EventBusUtilGenerator.  It's fairly similiar to our CommandExecGenerator except that it is actually processing any fields which may exist on the Event itself (see x,y,z) in the "MyEvent" example above.  The fields are passed on down to the JSNI call for the handler.  Not everything is going to translate down very well, but we will pass everything we have.  As an example of this, here is a section of the generated Java source for the addHandler method in EventBusUtilImpl:

public void addHandler(final String eventType, final JavaScriptObject handler) {
..
else if(eventType.equals("UserSettingsLoadedEvent")){
 EVENT_BUS.addHandler(UserSettingsLoadedEvent.TYPE, new UserSettingsLoadedEventHandler() {
public void onUserSettingsLoaded(UserSettingsLoadedEvent event) {
 invokeEventBusJSO(handler, event.getSettings());
}
 });
    }
..
}

Notice event.getSettings() has been added to the call to invoke.  When addHandler is called with a String eventType (from JavaScript), we match it with the known events, such as "UserSettingsLoadedEvent." It is important to note that this is dynamically generated at compile time, we are not actually writing or maintaining code with hard-coded values like that.  We use reflection to grab class names, method names, signatures and return types.  When you add a JavaScript handler, it comes into GWT as a JavaScriptObject, in reality, these are just functions passed up from JavaScript.  When it comes time to invoke the handler, we dip back down into JSNI and call the handler with all of the parameters provided in JSON.

The actual JSNI call to invoke the handler is just two lines of code:

public native void invokeEventBusJSO(final JavaScriptObject jso, final String parameterJSON)
/*-{
  eval('var p = ' + parameterJSON);
  jso.call(this, p);
}-*/;

So these are the low-level details, to actually handle GWT events in JavaScript all you have to do is provide a handler:

mantle_addHandler("RecentsChangedEvent", function(paramJSON) {
 // do something 
});

Whenever GWT fires a RecentsChangedEvent, your handler function will get called.  If you would like to fire an event from JavaScript to GWT (such that GWT and any other JavaScript handlers will be made aware of) you would do it like this:

mantle_fireEvent("RecentsChangedEvent");

If you want to fire an event which has parameters, provide them as JSON (so that we can match the name of the parameter with the setter for the field on the event class itself).  For example:


mantle_fireEvent("GenericEvent", { eventSubType: "MDD", stringParam: "TestString" });

Events can now be shared between the two worlds of GWT & JavaScript with support for parameters.  We can only handle events we know about (at compile time), one way around this is to use the GenericEvent which has fields for each type of primitive you might want to set.  Use the eventSubType to distinguish the event from other types of events.  PUC does not have any uses of it, this would only be useful for any cross iframe situations or if an event bus or pub/sub model is not provided by whatever framework is being used.  An example handler in JavaScript might be something like this:

mantle_addHandler("GenericEvent", function(paramjson) {
  if (paramjson.eventSubType == "MDD") {
    // do something, this is the subType we care about
  }
});


CommandExec Revisted


While I was writing the EventBusUtilGenerator I revisited the CommandExecGenerator and added parameter support to it as well.  All of the commands that we used to ignore, which did not have a nullary constructor, are now available in JavaScript.  Pass parameters in the same manner as described above (as JSON).  We do not support complex types such as "FileItem" and going forward, discourage their use, try to find an alternative.  If you are passing a FileItem but only actually need the file's path, just use the path (as a String).  An example command execution with a parameter:

executeCommand("SwitchLocaleCommand", { locale : 'fr' });


No comments:

Post a Comment