18 March 2010

Handling Flex Events in GWT

Tightly integrating Flex components in GWT applications requires that GWT is notified on events triggered in Flex. For example, when the user performs an action in the Flex component, e.g. double-clicking a node in a graph widget, we might want to perform some operations on the GWT side, e.g. updating a dependent view to show information about the double-clicked node. This tutorial shows how UI event from Flex can be forward to and handled by GWT code. The example contains a GWT text box and a Flex text box and buttons for copying the text from one to the other. This tutorial is the third part of a series of blog posts about using Flex components in GWT with the library gwt2swf:

1. GWT Wrapper for Flex components
2. Notifying GWT when a Flex widget is loaded [Demo, Source Code]
3. Handling Flex events in GWT [Demo, Source Code]

This tutorial is based on the previous one, notifying GWT when a Flex widget is loaded, and assumes that you have a similar project setup and that you have created the classes and files with the source code from that tutorial. The main ideas are similar those from the previous tutorial, but we will explicitly register our event handler bridge as a callback in Flex. That way, the Flex component can expose operations that might not be used by our GWT application, but in other applications, e.g. separate JavaScript projects, without requiring the GWT wrapper to provide hooks for those operations.

The Flex component is first extended by adding a 'send' button and changing the text to a text input. Then a method for adding a JavaScript method as event listener is implemented and exposed. When calling registered event listeners, the Flex components adds the content of the text field, so we can use it on the GWT side. Alternatively, one could implement and expose a method for getting the content of the Flex text field from JavaScript.

Text.mxml
<mx:Button id="sendButton" x="200" width="80" label="Send"/>
<mx:TextInput id="textWidget" x="0" width="200"/>

...

public function addSendListener(jsFunctionName:String):void {
sendButton.addEventListener(MouseEvent.CLICK, function(event:MouseEvent):void {
ExternalInterface.call(jsFunctionName, swfID, textWidget.text);
});
}

private function init():void {
...
addJSCallback("addSendListener", addSendListener);
}
The complete Flex component should look like this:

Text.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute" creationComplete="init()"
backgroundColor="0xffffff" paddingBottom="0" paddingLeft="0"
paddingRight="0" paddingTop="0">

<mx:Script>
<![CDATA[

import mx.events.FlexEvent;

public function displayText(text:String):void {
textWidget.text = text;
}

private function init():void {
addEventListener(FlexEvent.APPLICATION_COMPLETE, onApplicationComplete);
addJSCallback("displayText", displayText);
addJSCallback("addSendListener", addSendListener);
}

private function onApplicationComplete(event:FlexEvent):void {
callLater(function():void {
ExternalInterface.call("_swf_application_complete", swfID);
});
}

public static function addJSCallback(jsFunctionName:String, flexFunction:Function):void {
try {
if (ExternalInterface.available) {
ExternalInterface.addCallback(jsFunctionName, flexFunction);
}
} catch (error:SecurityError) {
trace("Couldn't add javascript callback: " + error);
}
}

public function addSendListener(jsFunctionName:String):void {
sendButton.addEventListener(MouseEvent.CLICK, function(event:MouseEvent):void {
ExternalInterface.call(jsFunctionName, swfID, textWidget.text);
});
}

public function get swfID():String {
return Application.application.parameters.swfid;
}

]]>
</mx:Script>

<mx:Button id="sendButton" x="200" width="80" label="Send"/>
<mx:TextInput id="textWidget" x="0" width="200"/>

</mx:Application>
Now we can build the Flex project and copy the generated .swf file into the GWT project. It should be under the 'public' folder below the folder that contains the .gwt.xml file, e.g. in my case as 'src/de/larsgrammel/blog/flexgwt/public/Test.swf'.

The GWT wrapper keeps track of event handler that are interested in this send event. For that purpose, we add custom event and event handler classes:

TextSentEvent.java
import com.google.gwt.event.shared.GwtEvent;

public class TextSentEvent extends GwtEvent<TextSentEventHandler> {

public static final Type<TextSentEventHandler> TYPE = new Type<TextSentEventHandler>();

private SampleFlexWrapperWidget swfWidget;

private String newText;

public TextSentEvent(SampleFlexWrapperWidget swfWidget, String newText) {
assert swfWidget != null;
this.newText = newText;
this.swfWidget = swfWidget;
}

protected void dispatch(TextSentEventHandler handler) {
handler.onTextSent(this);
}

public String getNewText() {
return newText;
}

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

public SampleFlexWrapperWidget getSWFWidget() {
return swfWidget;
}
}
TextSentEventHandler.java
public interface TextSentEventHandler extends EventHandler {

void onTextSent(TextSentEvent event);

}
Now, we add a method for registering TextSentEventHandlers to our SampleFlexWrapperWidget:

SampleFlexWrapperWidget.java
...

public HandlerRegistration addTextSentEventHandler(
TextSentEventHandler handler) {

return addHandler(handler, TextSentEvent.TYPE);
}
The next two methods forward the Flex events to the registered event handlers:

SampleFlexWrapperWidget.java
...

public static void _onTextSent(String swfId, String text) {
swfWidgets.get(swfId).onTextSent(text);
}

private void onTextSent(String text) {
fireEvent(new TextSentEvent(this, text));
}
Now we register the _onTextSent method as listener on the Flex component by exposing it as a JavaScript method. The GWT guide recommends that Java methods should be wrapped by $entry() when exposing them as JavaScript callback points. We thus register our wrapped _onTextSent method as JavaScript handle once the Flex component finished loading.

SampleFlexWrapperWidget.java
...

public static void onSwfApplicationComplete(String swfId) {
_registerSwfListeners(swfId);
...
}

private static native void _registerSwfListeners(String swfID) /*-{
var swfWidget = $doc.getElementById(swfID);
swfWidget.addSendListener("_swf_on_text_sent");
}-*/;

private static native void registerCallbackMethods() /*-{
$wnd._swf_on_text_sent=
$entry(@de.larsgrammel.blog.flexgwt.client.SampleFlexWrapperWidget::_onTextSent(Ljava/lang/String;Ljava/lang/String;));
...
}-*/;
Finally, we change our main code to use the new functionality of our wrapper by updating the GWT text field when the Flex component sents over new text:

FlexGWTIntegration.java
...
flexWidget.addTextSentEventHandler(new TextSentEventHandler() {
public void onTextSent(TextSentEvent event) {
textField.setText(event.getNewText());
}
});

The complete FlexGWTIntegration and SampleFlexWrapperWidget should look like this now:

SampleFlexWrapperWidget.java
import java.util.HashMap;
import java.util.Map;

import pl.rmalinowski.gwt2swf.client.ui.SWFWidget;

import com.google.gwt.event.shared.HandlerRegistration;

public class SampleFlexWrapperWidget extends SWFWidget {

private static Map<String, SampleFlexWrapperWidget> swfWidgets = new HashMap<String, SampleFlexWrapperWidget>();

static {
registerCallbackMethods();
}

private static native void _displayText(String swfID, String text) /*-{
$doc.getElementById(swfID).displayText(text);
}-*/;

public static void onSwfApplicationComplete(String swfId) {
_registerSwfListeners(swfId);
swfWidgets.get(swfId).fireSWFWidgetReady();
}

private static native void _registerSwfListeners(String swfID) /*-{
var swfWidget = $doc.getElementById(swfID);
swfWidget.addSendListener("_swf_on_text_sent");
}-*/;

private static native void registerCallbackMethods() /*-{
$wnd._swf_on_text_sent=
$entry(@de.larsgrammel.blog.flexgwt.client.SampleFlexWrapperWidget::_onTextSent(Ljava/lang/String;Ljava/lang/String;));
$wnd._swf_application_complete=
$entry(@de.larsgrammel.blog.flexgwt.client.SampleFlexWrapperWidget::onSwfApplicationComplete(Ljava/lang/String;));
}-*/;

public SampleFlexWrapperWidget(int width, int height) {
super("flexgwtintegration/Test.swf", width, height);
addFlashVar("swfid", getSwfId());
}

public HandlerRegistration addSWFWidgetReadyHandler(
SWFWidgetReadyHandler handler) {

return addHandler(handler, SWFWidgetReadyEvent.TYPE);
}

public HandlerRegistration addTextSentEventHandler(
TextSentEventHandler handler) {

return addHandler(handler, TextSentEvent.TYPE);
}

public static void _onTextSent(String swfId, String text) {
swfWidgets.get(swfId).onTextSent(text);
}

private void onTextSent(String text) {
fireEvent(new TextSentEvent(this, text));
}

public void displayText(String text) {
_displayText(getSwfId(), text);
}

private void fireSWFWidgetReady() {
fireEvent(new SWFWidgetReadyEvent(this));
}

protected void onLoad() {
super.onLoad();
SampleFlexWrapperWidget.swfWidgets.put(getSwfId(), this);
}

protected void onUnload() {
SampleFlexWrapperWidget.swfWidgets.remove(getSwfId());
super.onUnload();
}
}

FlexGWTIntegration.java
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;

public class FlexGWTIntegration implements EntryPoint {

public void onModuleLoad() {
final Button sendButton = new Button("Send to Flex");
final TextBox textField = new TextBox();
textField.setText("from GWT");

final SampleFlexWrapperWidget flexWidget =
new SampleFlexWrapperWidget(300, 50);

RootPanel.get().add(textField);
RootPanel.get().add(sendButton);

RootPanel.get().add(flexWidget);

textField.setFocus(true);
textField.selectAll();

class MyHandler implements ClickHandler, KeyUpHandler {
public void onClick(ClickEvent event) {
displayTextInFlex();
}

public void onKeyUp(KeyUpEvent event) {
if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
displayTextInFlex();
}
}

private void displayTextInFlex() {
flexWidget.displayText(textField.getText());
}
}

MyHandler handler = new MyHandler();
sendButton.addClickHandler(handler);
textField.addKeyUpHandler(handler);

flexWidget.addTextSentEventHandler(new TextSentEventHandler() {
public void onTextSent(TextSentEvent event) {
textField.setText(event.getNewText());
}
});
}
}
This example code allows you to send text from GWT to Flex and from Flex to GWT (demo). Together with the first two parts of this tutorial series, this should enable you to use a Flex component from GWT in a way that resembles the GWT API. There are a couple more details that I will cover in future posts.

9 comments:

Uli said...

Great tutorial - can't wait for your next posts to come out!

FYI: the overly long lines in this post and the previous one are cut off in Firefox.

Have you considered using Flex-Ajax Bridge and gwt-fabridge for Flex-Ajax communication?

lgrammel said...

@Uli Thanks - I am glad you liked the tutorial.

I have looked at gwt-fabridge, but I found that there was not much support for that project on google code (e.g. old issue tracker items without developer response, source code not in repository), so I did not look deeper into it.

I agree with you that using the Flex-Ajax Bridge is a good idea that would remove a lot of the boilerplate code - thanks for the suggestion (I actually did not consider it further after looking at gwt-fabridge, but I might look at it again) - but the general concepts of integrating Flex & GWT would still be the same :-)

lgrammel said...

I just took a look at integrating Flash & GWT using gwt-fabridge again and realized that I forgot to mention the main reason why I did not use it: it is licensed under GPL and I plan to release the project I am developing under the Apache License - which would not be possible if it contains a GPL component.

Mike said...

Hey Lars,

Why are you not using the $entry() function when registering the callback methods? This is mentioned in the JSNI Reference page at http://code.google.com/webtoolkit/doc/latest/DevGuideCodingBasicsJSNI.html#calling

thanks,
Dennis

Neil said...

Hey Lars,

Question regarding multiple widgets within a GWT program; I'm having a problem with one widget, when rendered, somehow interfering with the communication between an already existing flex widget and gwt (eg once the 2nd widget is instantiated, the 1st still works, but does not communicated back to GWT).

lgrammel said...

@Dennis / Mike

Thanks for pointing this out! I updated the blog post and the code on GitHub.

lgrammel said...

@Neil

I added another FlexWrapperWidget and it works for me, at least on Firefox 3.5.13 / developer mode. Here is the code that I added to FlexGWTIntegration.java (below similar passages for flexWidget):

final SampleFlexWrapperWidget flexWidget2 = new SampleFlexWrapperWidget(
300, 50);

RootPanel.get().add(flexWidget2);

flexWidget2.addTextSentEventHandler(new TextSentEventHandler() {
@Override
public void onTextSent(TextSentEvent event) {
textField.setText(event.getNewText());
}
});

Sandip said...

Hi,

I am new to GWT. I am excited with this gwtflex. Your blog really helps me to understand this.

I have a doubt. I am trying to send an alert to flex in this listener,
same as display text. But thatz throwing object/property not supported. Can you help me? how I can implement this?

flexWidget.addTextSentEventHandler(new TextSentEventHandler() {
@Override
public void onTextSent(TextSentEvent event) {
textField.setText(event.getNewText());
//send alert to flex interface
}
});

Regards

lgrammel said...

@Sandip You would need to expose an alert method in you Flex widget that you would call. See http://lgrammel.blogspot.com/2010/02/i-am-working-on-project-that-integrates.html for more information on how to expose Flex methods to GWT.