19 February 2010

Notifying GWT when a Flex widget is initialized

Flex .swf files can take a while to load. When using Flex components from within GWT, this means that the GWT user interface tends to be ready while the Flex widgets are still loading. This can cause problems if we call methods on those Flex widgets before they are completely initialized. This tutorial shows how to notify GWT when a Flex widget is ready, and how to prevent the problem of calling an uninitialized Flex widget. It is the second 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, creating a GWT Wrapper for Flex components, 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 general idea is to listen for the application complete event in Flex, to forward this event to the GWT wrapper and to notify listeners on the GWT side. For this to work, the Flex widget must include it's swfID when forwarding the event to GWT, because we need to know which Flex widget should get notified on the GWT side if we have multiple Flex widgets. For some reason, the Flex application id does not seem to work for that purpose on all browsers, so we pass it in as a Flash var:

1. Add addFlashVar call for passing the swfid into the Flex component to the SampleFlexWrapperWidget constructor:

addFlashVar("swfid", getSwfId());

The whole constructor should look like this:

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

2. In the Flex component, add an accessor to that property:

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

Now we can use the swf id within Flex. The next step is to listen for the application complete event in Flex and to forward it to GWT.

3. Add the event listener for the application complete event in the init() method of our Flex widget. We use the application complete event, because we want to make sure that the widget is visible and ready (More information on the Flex Application startup event order).

addEventListener(FlexEvent.APPLICATION_COMPLETE, onApplicationComplete);

The whole init method should look like this:

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

4. Add the event listener method to the Flex widget. We use callLater here to make sure the widget is ready when the event is fired. The method calls the method with the javascript identifier _swf_application_complete, which we will implement soon.

import mx.events.FlexEvent;

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

The complete Flex widget should look like this by now:

<?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);
}

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 get swfID():String {
return Application.application.parameters.swfid;
}

]]>
</mx:Script>
<mx:Text id="textWidget" width="100%"/>
</mx:Application>

5. 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'.

That's it on the Flex side. On the GWT side, we need to keep track of the different widgets and their swf id's so we can forward the event to the right widget.

6. Create a static map that enables us to find the GWT wrapper for a given swf id:

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

7. Register the widget in the map when loaded and deregister it when unloaded:

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

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

Now we can add the methods that are called from Flex.

8. Create a class initializer that calls a JSNI method to register the callback methods. That way, the first time one of our wrapper widgets gets created (i.e. the class is loaded), our callback method is registered.

static {
registerCallbackMethods();
}

9. Create the JSNI method that links a Java method to the callback point '_swf_application_complete':

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

10. Add the method that routes the event to the correct wrapper and calls fireSWFWidgetReady on that wrapper:

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

We need a special event and handler on the GWT. So lets create those and the related methods in the wrapper.

11. Create SWFWidgetReadyEvent and SWFWidgetReadyHandler

package de.larsgrammel.blog.flexgwt.client;

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

public class SWFWidgetReadyEvent extends GwtEvent<SWFWidgetReadyHandler> {

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

private SampleFlexWrapperWidget swfWidget;

public SWFWidgetReadyEvent(SampleFlexWrapperWidget swfWidget) {
assert swfWidget != null;
this.swfWidget = swfWidget;
}

@Override
protected void dispatch(SWFWidgetReadyHandler handler) {
handler.onSWFWidgetReady(this);
}

@Override
public Type<SWFWidgetReadyHandler> getAssociatedType() {
return TYPE;
}

public SampleFlexWrapperWidget getSWFWidget() {
return swfWidget;
}

}


package de.larsgrammel.blog.flexgwt.client;

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

public interface SWFWidgetReadyHandler extends EventHandler {

void onSWFWidgetReady(SWFWidgetReadyEvent event);

}

12. Add a method to register a SWFWidgetReadyHandler in the wrapper class - make sure to return the HandlerRegistration so the
listener can be remove if needed:

public HandlerRegistration addSWFWidgetReadyHandler(
SWFWidgetReadyHandler handler) {

return addHandler(handler, SWFWidgetReadyEvent.TYPE);
}

13. Implement the fireSWFWidgetReady method:

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

The whole SampleFlexWrapperWidget should look similar to this by now:

package de.larsgrammel.blog.flexgwt.client;

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) {
swfWidgets.get(swfId).fireSWFWidgetReady();
}

private static native void registerCallbackMethods() /*-{
$wnd._swf_application_complete=
@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 void displayText(String text) {
_displayText(getSwfId(), text);
}

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

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

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

}

14. In the client code that uses the widget, we can now disable our control widgets by default and then enable them once the Flex widgets are ready:

nameField.setEnabled(false);
sendButton.setEnabled(false);

flexWidget.addSWFWidgetReadyHandler(new SWFWidgetReadyHandler() {
@Override
public void onSWFWidgetReady(SWFWidgetReadyEvent event) {
nameField.setEnabled(true);
sendButton.setEnabled(true);
}
});

I refactored the client code a bit. It now adds two similar Flex wrappers including controls by calling a addFlexWidgetAndGWTControls method. I also removed the automatic text focus - you would need to decide for one of those controls to get the focus for this to work properly. The client code looks like this:

package de.larsgrammel.blog.flexgwt.client;

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() {
addFlexWidgetAndGWTControls();
addFlexWidgetAndGWTControls();
}

private void addFlexWidgetAndGWTControls() {
final Button sendButton = new Button("Send");
final TextBox nameField = new TextBox();
nameField.setText("GWT User");

nameField.setEnabled(false);
sendButton.setEnabled(false);

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

RootPanel.get().add(nameField);
RootPanel.get().add(sendButton);
RootPanel.get().add(flexWidget);

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(nameField.getText());
}
}

// Add a handler to send the name to the server
MyHandler handler = new MyHandler();
sendButton.addClickHandler(handler);
nameField.addKeyUpHandler(handler);

flexWidget.addSWFWidgetReadyHandler(new SWFWidgetReadyHandler() {
@Override
public void onSWFWidgetReady(SWFWidgetReadyEvent event) {
nameField.setEnabled(true);
sendButton.setEnabled(true);
}
});
}
}

With this approach, your GWT controls should be disabled until the Flex widgets are completely loaded - and it should work with multiple Flex widgets. More on integrating Flex and GWT will be covered in future posts, so stay tuned.

09 February 2010

GWT Wrapper for Flex components

I am working on a project that integrates a Flex-based graph component into a GWT application. Using Flash / Flex and GWT in combination is tricky when you want to call both Flash/Flex from GWT and GWT from Flash/Flex, because there are two bridges involved: the GWT Java Script Native Interface (JSNI) and the Flex external interface.

This tutorial is the first 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]

I will explain how to create a GWT adapter around a Flex component that enables calling Flex from GWT. The code will use the library gwt2swf. This post will be pretty strait forward and matches the gwt2swf docs for most part, but will include sample GWT and Flex code. Essentially, I will create a subclass of SWFWidget (part of gwt2swf) that wraps all the Flex specifics and can be used from GWT. This approach enables the use of several swf widgets from GWT at the same time.

To create a sample Flex component, I used the following steps:

1. Create a new Flex Project (e.g. in Flex Builder)

2. Add a creationComplete handler (here: init) to the mxml header

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute"
creationComplete="init()">


3. Remove the padding from the Flex component & set the background color in the mxml header

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute" creationComplete="init()"
backgroundColor="0xffffff"
paddingBottom="0" paddingLeft="0"
paddingRight="0" paddingTop="0">


4. Add the init method and a addJsCallback method with code that I shamelessly stole from my colleague Chris Callendar, who helped me getting this to work and who has an awesome Flex blog, http://flexdevtips.blogspot.com/.

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);
}
}


5. Add a text field to the flex component as an example element. We will change the contents of the text field from GWT. The width is set to 100%, so that the widget resizes based on the surrounding HTML element. For other widgets, e.g. a table, you might want to set the height to 100% as well.

<mx:Text id="textWidget" width="100%"/>


6. Add a displayText function and register it as a callback for JavaScript access under the name "displayText" in the init() method.

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

private function init():void {
addJSCallback("displayText", displayText);
}


7. The complete .mxml file should look similar to this by now:

<?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[
public function displayText(text:String):void {
textWidget.text = text;
}

private function init():void {
addJSCallback("displayText", displayText);
}

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);
}
}
]]>
</mx:Script>
<mx:Text id="textWidget" width="100%"/>
</mx:Application>


8. 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'

That's it on the Flex side. Here is the list of steps I followed on the GWT side:

1. Download gwt2swf from http://sourceforge.net/projects/gwt2swf/ (I use version 0.6.0 in the example presented here)

2. Extract the gwt2swf.jar and add it project's buildpath

3. Add gwt2swf to your project's .gwt.xml file:

<inherits name='pl.rmalinowski.gwt2swf.GWT2SWF' />


4. Create a subclass of SWFWidget (here: SampleFlexWrapperWidget) that sets swf file by specifying the swf source path in the constructor call. The first part of the swf source path is the module name, the rest the path below the 'public' directory including the .swf name.

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


5. Add a displayText method to that class as a public interface on the Java side. This method calls a private static native method that resolves the SWF DOM element and calls the exposed Flex method:

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

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


6. The wrapper should look similar to this by now:

public class SampleFlexWrapperWidget extends SWFWidget {

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

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

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

}


7. The wrapper can now be used in GWT, e.g. in the entry point class:

public class FlexGWTIntegration implements EntryPoint {

public void onModuleLoad() {
final Button sendButton = new Button("Send");
final TextBox nameField = new TextBox();
nameField.setText("GWT User");

final SampleFlexWrapperWidget flexWidget1 = new SampleFlexWrapperWidget(100, 50);
final SampleFlexWrapperWidget flexWidget2 = new SampleFlexWrapperWidget(100, 50);

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

RootPanel.get().add(flexWidget1);
RootPanel.get().add(flexWidget2);

nameField.setFocus(true);
nameField.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() {
flexWidget1.displayText(nameField.getText());
flexWidget2.displayText(nameField.getText() + " 2nd version");
}
}

MyHandler handler = new MyHandler();
sendButton.addClickHandler(handler);
nameField.addKeyUpHandler(handler);
}
}


Using this approach, you can use the SWF wrapper inside GWT like any other GWT widget, except for a small loading delay. I will explain how to deal with delayed loading, events from Flex and others integration issues in future blog posts - stay tuned.