zaterdag 6 december 2008

Where do GWT widget events come from??

(This post applies to GWT 1.5. In GWT 1.6 the event system has been changed.)

The Google Web Toolkit works with an event mechanism that implements the Observer pattern: If an event occurs within a certain object called the `observed' object, this triggers a method invocation on each object within a set of `observing' objects called the listeners.

Various event types exist. Most of them (or all of them -- i'm not sure) represent events happening on the user-interface, e.g. a mouse entering or leaving a widget, a mouse click on a widget, etcetera. It was unclear to me how this widget event model worked, and i did not find a good explanation in the docs, so that's why i wrote this post.

The `observed' widget acts as an event source, e.g., a Button acts as a source for click events. In GWT, if a widget acts as an event source it must implement a sourcesXXXEvents interface. There are 15 somewhat sourcesXXXEvents interfaces defined in the GWT API. SourcesClickEvents, SourcesChangeEvents, SourcesKeyboardEvents, etcetera. These interfaces require that event listeners can be added to and removed from the listener collection of the event source.

The `observing' objects -- the ones that are registered as listeners -- must implement methods to deal with events if they are notified. E.g., an object listening to a SourcesKeyboardEvents object must implement the KeyBoardListener interface, requiring it to implement the onKeyDown, onKeyPress, onKeyUp methods. As such, each listener interface requires its own methods to be implemented. This is all pretty standard and straightforward if you ever did some event programming in something like Swing or Windows.

So-far-so-good, but one question remains: what is the rock-bottom source of events in GWT? Is declaring the implementation of an SourcesXXXEvents interface enough to have the events actually generated?

For instance, the Button class does not implement the SourcesMouseEvents interface. (At least not in GWT 1.5.3.) The SourcesMouseEvents interface contains methods onMouseEnter() and onMouseLeave(). These could be used to e.g. change the widget's text when the mouse hovers over it. Suppose i subclass Button as follows:

public class MouseEventsButton extends Button implements SourcesMouseEvents {

MouseListenerCollection mouseListenerCollection;

public MouseEventsButton(String string) {
super(string);
}

public void addMouseListener(MouseListener listener) {
if (mouseListenerCollection == null) {
mouseListenerCollection = new MouseListenerCollection();
}
mouseListenerCollection.add(listener);
}

public void removeMouseListener(MouseListener listener) {
if (mouseListenerCollection != null) {
mouseListenerCollection.remove(listener);
}
}


}

Will my new button respond to a mouse entering and leaving it now?? Let's try.

public class DemoGwtEvents implements EntryPoint {

public void onModuleLoad() {

VerticalPanel vPanel = new VerticalPanel();
vPanel.setWidth("100%");
vPanel.setHorizontalAlignment(VerticalPanel.ALIGN_CENTER);
RootPanel.get().add(vPanel);

// create the new button
MouseEventsButton mouseEventsButton = new MouseEventsButton("Rub me!");

// add a listener to it that changes text on events
mouseEventsButton.addMouseListener(new MouseListenerAdapter() {

public void onMouseEnter(Widget sender) {
Button b = (Button) sender;
b.setHTML("thank you");
}

public void onMouseLeave(Widget sender) {
Button b = (Button) sender;
b.setHTML("rub me again!!");
}
});

vPanel.add(mouseEventsButton);

}
}
NOOOOO, that doesn't work... Nothing happens if i move my mouse over the MouseEventsButton.

The key is that i must make a connection between low-level DOM-events and high-level GWT events somehow. DOM-events are events that happen in the browser window. GWT events happen in the Java layer of a GWT app. Somehow, the DOM-events must be captured and re-launched into the Java layer. This is done in two steps:
  1. Tell the new widget (the button) to CAPTURE certain DOM-events by calling sinkEvents. In our case this is this.sinkEvents(Event.ONMOUSEOVER | Event.ONMOUSEOUT);
  2. Write an event handler to capture the sunk DOM events and re-launch them in the java world. This is done by overriding the onBrowserEvent method of the widget. In our case this is
     public void onBrowserEvent(Event event) {
    super.onBrowserEvent(event);
    // Handle events as a normal Button would
    int type = DOM.eventGetType(event);
    // Look at the type of event again
    switch (type) {
    case Event.ONMOUSEOVER:
    mouseListenerCollection.fireMouseEnter(this);
    break;
    case Event.ONMOUSEOUT:
    mouseListenerCollection.fireMouseLeave(this);
    }
    }
    Note that to throw the events into the java world, the MouseListenerCollection class provides utility methods as `fireMouseLeave' etc.
Putting it all together, we get the following code for MouseEventsButton:

public class MouseEventsButton extends Button implements SourcesMouseEvents {

MouseListenerCollection mouseListenerCollection;

public MouseEventsButton(String string) {
super(string);
this.sinkEvents(Event.ONMOUSEOVER | Event.ONMOUSEOUT);
}

public void addMouseListener(MouseListener listener) {
if (mouseListenerCollection == null) {
mouseListenerCollection = new MouseListenerCollection();
}
mouseListenerCollection.add(listener);
}

public void removeMouseListener(MouseListener listener) {
if (mouseListenerCollection != null) {
mouseListenerCollection.remove(listener);
}
}

public void onBrowserEvent(Event event) {
super.onBrowserEvent(event);
// Handle events as a normal Button would
int type = DOM.eventGetType(event);
// Look at the type of event again
switch (type) {
case Event.ONMOUSEOVER:
mouseListenerCollection.fireMouseEnter(this);
break;
case Event.ONMOUSEOUT:
mouseListenerCollection.fireMouseLeave(this);
}
}
}
So, in summary we may say that GWT widget events are created explicitly as 2-nd layer events after explicitly catching and handling low-level DOM events.

Looking at the current version of the source code (dec. 8, 2008) checked out from svn, many of the classes and methods involved in event handling have been deprecated, so it looks asif a major refactoring of the event mechanism is pending. Hopefully, this will be better documented than it was in the current version.

Events in GWT Composite Widgets

This blog post is on event handling in composite widgets in the Google Web Toolkit. For a general description on event handling in GWT, please see my earlier post: Where do GWT events come from?

I have been working with Google Web Toolkit (GWT) for a while, but i never created any custom widgets yet. Now, that i'm trying to build one, i'm kind of confused about the way composite widgets deal with events.

Now, if i create a composite widget in GWT, what happens with the events? A number of questions come up:
  • Can the composite widget be an event source?
  • Can the composite widget listen to external event sources?
  • Can the composite widget listen to internal event sources?
  • Can a wrapped widget within the composite listen to external events?
  • Can a wrapped widget within the composite source events to external listerers, bypassing the composite widget?
Below, i'll answer these questions one by one...

Q: Can the composite widget be an event source?

A: Yes. The Composite widget can pass on the events generated by a contained widget and as such, be an event source by itself. GWT actually provides special classes and methods to support this: This is done through the DelegatingXXXListener approach. As an example, consider the following Composite widget, that contains only a Button.

public class DelegatingClickWidgetDemo extends Composite implements
SourcesClickEvents {
Button button1;

DelegatingClickListenerCollection clickListeners;

public DelegatingClickWidgetDemo(String caption) {

VerticalPanel verticalPanel = new VerticalPanel();
button1 = new Button(caption);
verticalPanel.add(new HTML("Composite widget delegatingclicklisterer"));
verticalPanel.add(button1);

initWidget(verticalPanel);
}

public void addClickListener(ClickListener listener) {
if (clickListeners == null) {
clickListeners = new DelegatingClickListenerCollection(this,
button1);
}
clickListeners.add(listener);
}

public void removeClickListener(ClickListener listener) {
if (clickListeners != null)
clickListeners.remove(listener);
}

}
There are three things to notice about this code (marked in red in the listing):
  1. The class implements the SourcesClickEvents interface, thus is must implement addClickListener and removeClickListener.
  2. The class maintains a member of class DelegatingClickListenerCollection. This object delegates click listeners to another object, in this case the warpped Button.
  3. When DelegatingClickListenerCollection is instantiated, it is given the Button object it should delegate to as an argument.
So, the Composite widget maintains a Listener collection that actually listens to a contained widget!

(Sideways remark: It seems difficult to maintain separate Listener collections for multiple contained widgets at the Composite level, since there is only one addXXXListener method. But there may be a workaround.)

(Sideways remark 2: i don't know whether the composite can also be an event source detached from its contained widgets. Since the rock-bottom source of GWT events are DOM browser events, it seems that all GWT events must come from widgets that are associated with a browser object. So i guess the answer is no.)

If you want to use this class you must instantiate the composite widget and register an event listener:

public class DemoGwtComposite implements EntryPoint {

public void onModuleLoad() {
VerticalPanel vPanel = new VerticalPanel();
vPanel.setWidth("100%");
vPanel.setHorizontalAlignment(VerticalPanel.ALIGN_CENTER);
RootPanel.get().add(vPanel);

DelegatingClickWidgetDemo dcwd = new delegatingClickWidgetDemo("DCWD");
vPanel.add(dcwd);

// add a click listener to the COMPOSITE object, not to the button
// itself!
dcwd.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
sender
.setTitle("This title was set by a delegating click listener");
}
});
}
}
If you run this example you will see that the click-listener registered with the composite widget handles the button's clicks!

Q: Can the composite widget listen to external event sources?


A: Sure, why not? It must just implement the methods required to be an event listerer... E.g., for click events it must implement the onClick method.

Q: Can the composite widget listen to internal event sources?

Yes. There is no difference between external and contained widgets in this respect. A composite can register itself as a listener to events generated by wrapped widgets.

Q: Can a widget within the composite listen to external events?

Yes, as long as it implements the required interfaces and registers itself as a listener. I did not test this.

Q: Can a widget within the composite source events to external listerers, bypassing the composite widget?

I think that in principe, this should be possible. However, this against the intention of Composite widgets, since a composite widget shields the methods of its contained widgets from the outside world. Thus, it is impossible to register listeners with the contained widget directly. (Instead, one can use the DelegatingXXXListener approach outlines above.)

If, against the spirit of composite widgets, you want a contained widget to source events to the outside world, you must make its addXXXListener methods available to the outside world somehow. You could do that by creating helper methods on the Composite object, and/or by passing a reference to the contained object to the outside world.