Using gwt-dispatch without GWT RPC
I watched the
Best Practices For Architecting Your GWT App video, and followed the excellent
GWT MVP Example tutorial.
And it gave my some ideas on how to implement the command pattern in my GWT application.
The pattern is nice in a GWT app:
- We have a centralized component to handle cross-cutting concerns like caching, logging, handling network errors, etc.
- The components are no longer aware of the implementation details of the client / server communication, so:
- Better unit testing: Handlers can be easily mocked
- Handlers implementation could be swapped with minimal impact to:
- change the client / server communication: For example change JSON to GWT RPC
- or why not while the application is running: For example to implement an offline mode, the JSON handlers would be swapped to Gears handlers, without the components noticing.
I wanted to use the
gwt-dispatch lib, which implements this pattern in GWT, problem is: At the time of this writing, in gwt-dispatch, handlers could only be implemented on the server and with GWT RPC.
And in my application the client / server communication is done with SOAP.
So I hacked a couple of classes to handle the commands on the client side:
- RequestBuilderDispatcher, a implementation of DispatchAsync, capable of executing RequestBuilderActionHandlers, 2 public methods:
- addHandler : To add a RequestBuilderHandler for a specific Action
- execute: To execute an Action with a RequestBuilderActionHandler.
- RequestBuilderActionHandler : All handlers must implements this interface. 2 methods are defined:
- getRequestBuilder: Prepare and configure a RequestBuilder with the content of the Action: Setup the url, post method, request parameters, ...
- extractResult: Extract a Result from the server Response.
RequestBuilderActionHandler.java
package fr.hrgwt.client.command;
import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.Response;
public interface RequestBuilderActionHandler<A extends Action<R>, R extends Result> {
/**
* Prepare a RequestBuilder.
*/
RequestBuilder getRequestBuilder(A action);
/**
* Extract the result from the response.
*/
R extractResult(Response response);
Class<A> getActionType();
}
Here an example of implementation:
FindAllCheckListXmlActionHandler.java
package fr.hrgwt.client.command.impl;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.Response;
import fr.hrgwt.client.command.RequestBuilderActionHandler;
public class FindAllCheckListXmlActionHandler
implements
RequestBuilderActionHandler<FindAllCheckListAction, FindAllCheckListActionResult> {
@Override
public Class<FindAllCheckListAction> getActionType() {
return FindAllCheckListAction.class;
}
@Override
public RequestBuilder getRequestBuilder(FindAllCheckListAction action) {
return new RequestBuilder(RequestBuilder.GET, "/myurl");
}
@Override
public FindAllCheckListActionResult extractResult(Response response) {
// TODO extract the xml from the response here
return null;
}
}
Code for
RequestBuilderDispatcher:
RequestBuilderDispatcher.java
package fr.hrgwt.client.command;
import java.util.HashMap;
import java.util.Map;
import net.customware.gwt.dispatch.client.DispatchAsync;
import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.user.client.rpc.AsyncCallback;
/**
* Implements DispatchAsync with a RequestBuilder.
*/
public class RequestBuilderDispatcher implements DispatchAsync {
@SuppressWarnings("unchecked")
Map<Class<Action>, RequestBuilderActionHandler> handlersMap = new HashMap<Class<Action>, RequestBuilderActionHandler>();
@SuppressWarnings("unchecked")
public <A extends Action<R>, R extends Result> void execute(A action,
final AsyncCallback<R> asyncCallback) {
Class<? extends Action> actionType = action.getClass();
final RequestBuilderActionHandler<A, R> handler = handlersMap
.get(actionType);
if (handler == null) {
throw new IllegalArgumentException("unregistered actionType:"
+ actionType);
}
RequestBuilder requestBuilder = handler.getRequestBuilder(action);
requestBuilder
.setCallback(createRequestCallback(asyncCallback, handler));
try {
requestBuilder.send();
}
catch (RequestException e) {
throw new RequestRuntimeException(e);
}
}
// visibility package for tests
<A extends Action<R>, R extends Result> RequestCallback createRequestCallback(
final AsyncCallback<R> asynchCallback,
final RequestBuilderActionHandler<A, R> handler) {
return new RequestCallback() {
@Override
public void onResponseReceived(Request request, Response response) {
asynchCallback.onSuccess(handler.extractResult(response));
}
@Override
public void onError(Request request, Throwable exception) {
throw new RuntimeException("not implemented yet");
}
};
}
@SuppressWarnings("unchecked")
public <A extends Action<R>, R extends Result> void addHandler(
RequestBuilderActionHandler<A, R> handler) {
handlersMap.put((Class<Action>) handler.getActionType(), handler);
}
}
Now in the initialization of the app:
- Create a dispatcher that will be injected in the components that needs it (manually or better with GIN)
- Register an ActionHandler
RequestBuilderDispatcher dispatcher = new RequestBuilderDispatcher();
dispatcher.addHandler(new FindAllCheckListXmlActionHandler());
Nothing special when using the dispatcher (the client code won't know anything about the implementation, it could be GWT RPC) :
@Override
protected void onBind() {
dispatcher.execute(new FindAllCheckListAction(),
new AsyncCallback<FindAllCheckListActionResult>() {
@Override
public void onSuccess(FindAllCheckListActionResult result) {
display.setCheckLists(result.getCheckLists());
}
@Override
public void onFailure(Throwable caught) {
throw new RuntimeException("not implemented yet");
}
});
Conclusion
The good
- Quick implementation of an ActionHandler on the client side.
The bad
- Rollback is not handled
- No GIN integration
Remarks
- My build is dependant on the gwt-dispatch library, this is not really necessary as I am only using 3 interfaces from it (Action, Result and AsyncDispatch). It would be interessting to see if there's an impact on the size of the compiled Javascript modules.