TurboManage

David Chandler's Journal of Java Web and Mobile Development

  • David M. Chandler


    Web app developer since 1994 and former Developer Advocate with Google now residing in Colorado. Besides tech, I enjoy landscape photography and share my work at ColoradoPhoto.gallery.

  • Subscribe

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 233 other followers

  • Sleepless Nights…

    March 2019
    S M T W T F S
    « Nov    
     12
    3456789
    10111213141516
    17181920212223
    24252627282930
    31  
  • Blog Stats

    • 981,457 hits

Archive for the ‘Model-View-Presenter’ Category

Correction to previous post

Posted by David Chandler on October 9, 2009

I made a serious mistake in my previous post and spent all evening looking for it šŸ˜¦

A side effect of the if block in the previous post is that the PlaceManager doesn’t get loaded if the URL is empty. I’ve now corrected it to always load the PlaceManager, as gwt-presenter doesn’t work at all without it.

Advertisements

Posted in Google Web Toolkit, Model-View-Presenter | Leave a Comment »

Loading a default view on startup with gwt-presenter

Posted by David Chandler on October 9, 2009

As shown in Chris Lowe’s GWT MVP example, I’ve been using getPlaceManager().fireCurrentPlace() in my EntryPoint class to show the initial view after all presenters are loaded. This works fine if there is a history token (placeId) in the URL; however, on initial load, my URL is empty, so I want to navigate to the default view and mark the Place in History (I chuckle every time I use that phrase). The simplest way to do this is to fire a PlaceRequestEvent, so my GWT EntryPoint class now concludes with this:

		// Load PlaceManager so it can start listening
		PlaceManager placeManager = injector.getPlaceManager();
		
		String currentPlace = History.getToken();
		if ("".equals(currentPlace))
		{
			// Nothing in URL, load default page
			injector.getEventBus().fireEvent(
					new PlaceRequestEvent(new PlaceRequest(
							ManageListsPresenter.PLACE)));
		}
		else
		{
			// Load requested page
			placeManager.fireCurrentPlace();
		}

My default presenter is contained within a WidgetContainerPresenter, and gwt-presenter calls the appropriate methods to reveal my default view, set it as the current presenter in the container, and update the URL in the address bar. Slick.

Posted in Google Web Toolkit, Model-View-Presenter | Leave a Comment »

gwt-presenter Hyperlink gotcha: URL in address bar getting wiped out

Posted by David Chandler on October 8, 2009

Just a quick note in the Headsmack department related to navigation between views with gwt-presenter: if you attach a ClickHandler that fires a PlaceRequestEvent to a Hyperlink, but forget to supply the history token, the browser will correctly navigate to the requested Place; however, the placeId and any parameters on the URL will immediately be wiped out in the browser address bar. The reason is that after the ClickHandler fires, GWT replaces the URL in the address bar with the history token from the Hyperlink, which, in the example below, is empty because it’s never set.

		// Don't do this
		Hyperlink hyperlink = new Hyperlink();
		hyperlink.setText("click here");
		hyperlink.addClickHandler(new ClickHandler()
		{
			@Override
			public void onClick(ClickEvent event)
			{
                eventBus.fireEvent(new PlaceRequestEvent(placeRequest));
			}
		});

This is a case of making it too hard (or more likely, accidentally leaving the ClickHandler in place when you decide to try a Hyperlink instead of a Button). Thanks to the gwt-presenter PlaceManager, which listens for History ValueChangeEvents, all you need is this:

		Hyperlink hyperlink = new Hyperlink("click here", placeRequest.toString());

That’s so clean it makes me want to use Hyperlinks everywhere.

See also a previous post on this topic.

Posted in Google Web Toolkit, Headsmack, Model-View-Presenter | Leave a Comment »

Calling AppEngine (securely) from GWT with gwt-dispatch

Posted by David Chandler on October 7, 2009

When using an AJAX framework like GWT, it is necessary to authenticate each service call as well as the front end or else your services are open to the world. With AppEngine, there are several approaches we could take, such as wrapping all service servlets with a servlet filter or Spring Security or calling a checkLoggedIn() method as the first statement in each service method.

The mechanics of authenticating an AppEngine on the server side are simple enough. We just call the AppEngine UserService. The browser automatically passes the AppEngine cookie with each service request, so we don’t have to modify our service interfaces just to authenticate the user. This example shows a simple checkLoggedIn() method that could be called from each service method or wired into a servlet filter:

package com.turbomanage.server.util;

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.turbomanage.exception.NotLoggedInException;

public class ServiceUtil
{
	public static User checkLoggedIn() throws NotLoggedInException
	{
		User user = getUser();
		if (user == null)
		{
			throw new NotLoggedInException("Not logged in.");
		}
		return user;
	}

	private static User getUser()
	{
		UserService userService = UserServiceFactory.getUserService();
		return userService.getCurrentUser();
	}
}

In addition to authenticating the user, it’s a good idea to protect our services against CSRF/XSRF attacks. Google recommends (see section on XSRF and GWT) that each service request be accompanied by a token that is available to the server and the client application, but not to JavaScript code in an email link or running on otherĀ  pages. The simplest scheme is simply to pass the sessionId cookie as an argument to each service method. With standard GWT-RPC, however, this is a pain because we have to add an argument to every method of every service:

public interface MyService extends RemoteService {
  public boolean doSomething(String cookieValue);
  public void doAnotherThing(String cookieValue, String arg);
}

This is where gwt-dispatch comes to the rescue and makes you thankful for people who turn verbs into nouns. Because gwt-dispatch uses the Command pattern, all standard GWT-RPC services are replaced with a single DispatchService that accepts an Action and Result argument. Each method in a standard GWT-RPC service is now represented by a corresponding Action and Result class. Because there is now only one real GWT-RPC service, the DispatchService, and because its execute method gets called for every request, we now have only place we need to add a session token argument. The gwt-dispatcher SecureDispatchService does exactly this:

package net.customware.gwt.dispatch.client.secure;

import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;

@RemoteServiceRelativePath("dispatch")
public interface SecureDispatchService extends RemoteService {
    Result execute( String sessionId, Action<?> action ) throws Exception;
}

By implementing this service on the server, we now have one place where we can verify the user is logged in as well as verify that the session ID passed as an argument with the request matches the session ID cookie sent automatically by the browser.

So here is my take on an AppEngineDispatchService that builds on gwt-presenter’s SecureDispatchService to achieve both user authentication and a measure of CSRF/XSRF protection. Because gwt-presenter’s SecureDispatchAsync class has a dependency on its own SessionUtil class, I had to replace it with AppEngineDispatchAsync; however, this could easily be factored out as an interface in a future release of gwt-presenter (and indeed has been already on the server side).

One last thing to mention: AppEngine uses a cookie named “ACSID”, so that’s what we’re passing with each request.

Our first class is just a stripped-down version of gwt-presenter’s SecureDispatchServiceAsync which passes the AppEngine session ID as an argument with each request.

package com.turbomanage.gwt.client.dispatch;

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.core.client.GWT;
import com.google.gwt.user.client.Cookies;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.turbomanage.gwt.client.service.AppEngineDispatchService;
import com.turbomanage.gwt.client.service.AppEngineDispatchServiceAsync;

/**
 * An AppEngine-aware implementation of {@link DispatchAsync}
 *
 * (c) 2009 David M. Chandler
 * Licensed under the Apache License, Version 2.0
 */
public class AppEngineDispatchAsync implements DispatchAsync {

    private static final AppEngineDispatchServiceAsync realService = GWT.create( AppEngineDispatchService.class );

    public AppEngineDispatchAsync() {
    }

    /* (non-Javadoc)
	 * @see com.turbomanage.gwt.client.dispatch.AppEngineSecureDispatchServiceAsync#execute(A, com.google.gwt.user.client.rpc.AsyncCallback)
	 */
    public <A extends Action<R>, R extends Result> void execute( final A action, final AsyncCallback<R> callback ) {

    	// Get AppEngine session ID
        String sessionId = Cookies.getCookie("ACSID");

        realService.execute( sessionId, action, new AsyncCallback<Result>() {
            public void onFailure( Throwable caught ) {
                callback.onFailure( caught );
            }

            public void onSuccess( Result result ) {
                callback.onSuccess( ( R ) result );
            }
        } );
    }

}

To use this class instead of gwt-presenter’s Standard or SecureDispatchAsync, simply modify the binding in your GIN module:

    @Override
    protected void configure() {
    ...
        bind( DispatchAsync.class ).to( AppEngineDispatchAsync.class ).in( Singleton.class );
    ...
    }

Now for the service interfaces. These are just renamed from gwt-presenter’s SecureDispatchService and SecureDispatchServiceAsync, which was necessary because I could not use gwt-presenter’s SecureDispatchServiceAsync on account of the above-mentioned dependency on SessionUtil and GWT seems to require that a service and its corresponding Async class exist in the same package.

package com.turbomanage.gwt.client.service;

import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;

@RemoteServiceRelativePath("dispatch")
public interface AppEngineDispatchService extends RemoteService {
    Result execute( String sessionId, Action<?> action ) throws Exception;
}

And the corresponding Async interface, likewise just renamed from gwt-presenter’s SecureDispatchServiceAsync:

package com.turbomanage.gwt.client.service;

import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;

import com.google.gwt.user.client.rpc.AsyncCallback;

public interface AppEngineDispatchServiceAsync
{
	void execute(String sessionId, Action<?> action,
			AsyncCallback<Result> callback);
}

Finally, our server implementation of the DispatchService:

package com.turbomanage.gwt.server.service;

import java.util.logging.Logger;

import javax.servlet.ServletContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import net.customware.gwt.dispatch.client.secure.SecureDispatchService;
import net.customware.gwt.dispatch.server.Dispatch;
import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;
import net.customware.gwt.dispatch.shared.secure.InvalidSessionException;

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.turbomanage.gwt.client.service.AppEngineDispatchService;

/**
 * A servlet implementation of the {@link SecureDispatchService}. This verifies
 * that an AppEngine user is logged in and inspects the passed AppEngine
 * session ID to prevent CSRF/XSRF attacks
 *
 * (c) 2009 David M. Chandler
 * Licensed under the Apache License, Version 2.0
 */
@Singleton
public class AppEngineDispatchServiceServlet extends RemoteServiceServlet
		implements AppEngineDispatchService
{
	private static final long serialVersionUID = -1456388230348266500L;
	private static final Logger LOG = Logger.getLogger(AppEngineDispatchServiceServlet.class
			.getName());

	private final Dispatch dispatch;
	private Provider<HttpServletRequest> req;

	@Inject
	public AppEngineDispatchServiceServlet(Dispatch dispatch, Provider<HttpServletRequest> req, Provider<ServletContext> context)
	{
		this.dispatch = dispatch;
		this.req = req;
	}

	public Result execute(String clientSessionId, Action<?> action) throws Exception
	{
		try
		{
			String serverName = req.get().getServerName();
			UserService userService = UserServiceFactory.getUserService();
			User user = userService.getCurrentUser();
			if (user != null)
			{
				// User is logged in, now try to match session tokens
				// to prevent CSRF
				String sessionId = "";
				Cookie[] cookies = req.get().getCookies();
				for (Cookie cookie : cookies)
				{
					if (cookie.getName().equals("ACSID"))
					{
						sessionId = cookie.getValue();
						break;
					}
				}
				// Skip check on localhost so we can test in AppEngine local dev env
				if (("localhost".equals(serverName)) || (sessionId.equals(clientSessionId)))
				{
					return dispatch.execute(action);
				}
			}
			throw new InvalidSessionException();
		}
		catch (RuntimeException e)
		{
			LOG.warning("Exception while executing " + action.getClass().getName()
					+ ": " + e.getMessage());
			throw e;
		}
	}
}

For each request, we’re simply calling the AppEngine UserService to verify the user is logged in and attempting to match the AppEngine cookie passed as an argument with the one passed as a cookie. In order to get access to the cookie on the server, we utilize a Guide Provider that lets us get to the ServletRequest. Likewise, we obtain the ServletContext in order to get the server name and skip the CSRF check on localhost, since AppEngine doesn’t set the ACSID cookie when running in the local development environment.

Lastly, we replace the gwt-presenter Standard or SecureDispatchServiceServlet with our AppEngine equivalent in the server’s Guice module:

	@Override
	public void configureServlets()
	{
		// NOTE: change the servlet context for your app
		serve("/your_app/dispatch").with(AppEngineDispatchServiceServlet.class);
	}

Thanks to gwt-presenter and the Command pattern, every AppEngine service request in our application now checks for a logged in user and provides a measure of CSRF protection.

Posted in AppEngine, GIN / Guice, Google Web Toolkit, Model-View-Presenter, Web App Security | 23 Comments »

GWT PropertyChangeListeners come full circle with MVP

Posted by David Chandler on October 6, 2009

I initially stumbled on the excellent Model-View-Presenter pattern for GWT while wading through discussion threads on the various library options for PropertyChangeSupport (since GWT does not offer it natively). In the process, I’ve found that I’m unable to completely avoid the need to store a few things in a client-side model, and the model turns out to be the most convenient place to fire an event indicating that the model has been updated, which looks an awful lot like…a PropertyChangeEvent/Listener.

	void setPrayerLists(List<PrayerList> prayerLists)
	{
		this.prayerLists = prayerLists;
		// Hah, we're back to PropertyChangeListeners, after all
		eventBus.fireEvent(new PrayerListsModifiedEvent(prayerLists));
	}

Of course, the event now being fired is now my own, not a PropertyChangeEvent, so I no longer need a PropertyChangeSupport emulation library and no longer have to worry about the accompanying idiosyncracies involving collections, etc. And thanks to GWT’s HandlerManager and related classes, I can fire events for any purpose, not just PropertyChangeEvents, and the event can carry a payload such as accompanying data in its constructor. I just mean to say that firing events off model changes still turns out to be useful in some cases. And I somehow find that humorous.

Posted in Google Web Toolkit, Model-View-Presenter | Leave a Comment »

Factoring out common MVP services into a Service Facade

Posted by David Chandler on October 5, 2009

Shortly after adopting the Model-View-Presenter pattern, I found myself copying the same service call into multiple presenters, as both a navigation panel and a main content area can sometimes display the same information. I decided to factor out the repeated service calls into a Service Facade (for lack of a better name) and inject the facade into each presenter that’s needed.

	@Inject
	public AddPrayerListPresenter(Display display, EventBus eventBus, PrayerListServiceFacade prayerListServiceFacade)
	{
		super(display, eventBus);
		this.prayerListServiceFacade = prayerListServiceFacade;
		bind();
	}

Inside the service facade, I use constructor injection to get the event bus and dispatcher the same as in a presenter.

	@Inject
	public PrayerListServiceFacade(final EventBus eventBus,
			final DispatchAsync dispatch, RoaModel roaModel)
	{
		this.dispatch = dispatch;
		this.eventBus = eventBus;
		this.model = roaModel;
	}

	public void refreshPrayerLists()
	{
		GWT.log("Calling actionFindLists", null);
		// Fire event that marks start of the service call so listeners
		// can show the AJAX wait thingy if desired
		eventBus.fireEvent(new RefreshingPrayerListsEvent());
		dispatch.execute(new FindPrayerListsAction(),
				new AsyncCallback<FindPrayerListsResult>()
				{
					@Override
					public void onFailure(Throwable e)
					{
						GWT.log(e.getMessage(), e);
						Window.alert(e.getLocalizedMessage());
					}

					@Override
					public void onSuccess(FindPrayerListsResult result)
					{
						eventBus.fireEvent(new PrayerListsModifiedEvent(result.getPrayerLists()));
					}
				});
	}

The refreshPrayerLists() method contains the code that had been common to multiple presenters. There is one difference, however. When a presenter calls a service, it typically passes a DisplayCallback object, which gwt-presenter uses to call the view’s startProcessing() and stopProcessing() methods so you can show an Ajax wait thingy (er, progress indicator?) I could add a Display argument to the service method and create a DisplayCallback as usual; however, in this case, I want the refresh method to be called only once in conjunction with the event that necessitates a refresh (say, a new prayer list is added). And I want multiple widgets to be notified when the service call begins, and when it finishes. To accomplish this, I pass only a regular AsyncCallback object and fire an event both before and after the service call, to which any interested widget can listen.

In summary, the service facade provides a way to group all related service calls in one place and to factor out common calls from multiple presenters. Now all I need is a better name for it, as it sounds a little scary for my taste…

Posted in Google Web Toolkit, Model-View-Presenter | 7 Comments »

gwt-dispatch and com.google.gwt.user.client.rpc.SerializationException

Posted by David Chandler on October 2, 2009

I’d written a new gwt-dispatch ActionHandler with corresponding Action and Result classes and was getting the above exception. The cause turned out be very simple: Action and Result classes implement java.io.Serializable and GWT-RPC therefore requires that they have an empty constructor. So when you get the yellow squiggly in Eclipse that prompts you to generate a serial ID, add an empty constructor, too, and save yourself some grief.

Posted in Google Web Toolkit, Headsmack, Model-View-Presenter | Leave a Comment »

I *like* code like this: easiest possible navigation with gwt-presenter

Posted by David Chandler on October 1, 2009

Here’s a super easy way to trigger a view transition and pass a parameter using gwt-presenter:

...
import net.customware.gwt.presenter.client.place.PlaceRequest;
...
		PlaceRequest placeRequest = new PlaceRequest(ManageListsPresenter.PLACE)
			.with(ManageListsPresenter.PARAM_TEST, "34")
			.with(ManageListsPresenter.PARAM_ANOTHER, "hello");
		Hyperlink my_lists = new Hyperlink("My Lists", placeRequest.toString());

That’s it! No ClickHandlers needed, and gwt-presenter takes care of all the rest.

The PlaceRequest.with() method is a nice touch to gwt-presenter. It all ends up on the URL, which is important for GWT’s History mechanism, and it’s a clean way to generate the URL. Of course, there are corresponding getParameter methods to extract passed parameters from a PlaceRequest, which you would typically do in the presenter’s onPlaceRequest() method.

So what happens when the user clicks the link? The view associated with the requested presenter is shown, the place is marked in GWT’s History mechanism, and the presenter’s onPlaceRequest() method is invoked. No ClickHandlers on the Hyperlink are required to make this happen. When the user clicks a Hyperlink, GWT fires a ValueChangedEvent on History, which is picked up by gwt-presenter and sets off the chain reaction. Even if the requested presenter belongs to a container (WidgetContainerPresenter) that is not currently shown, gwt-presenter calls revealDisplay() on the presenter and its container if needed to bring it to the front.

If you want to use a Button, Label, etc., instead of Hyperlink, you can simply add a ClickHandler and fire the same PlaceRequestEvent that gwt-presenter uses under the covers in response to a Hyperlink:

		display.getAddButton().addClickHandler(new ClickHandler()
		{
			@Override
			public void onClick(ClickEvent event)
			{
				PlaceRequest placeRequest = new PlaceRequest(AddPrayerListPresenter.PLACE);
				eventBus.fireEvent(new PlaceRequestEvent(placeRequest));
			}
		});

Note: the browser only updates the URL in the address bar in response to a Hyperlink. If you use a Button or other ClickHandler and want to update History and the URL in the address bar, you must also fire a PlaceChangedEvent(), typically in the onPlaceRequest() method that gets called by the original PlaceRequestEvent(). This results in a call to GWT’s History.newItem().

	@Override
	protected void onPlaceRequest(PlaceRequest request)
	{
		// necessary when we've fired PRE from code vs Hyperlink
		eventBus.fireEvent(new PlaceChangedEvent(request));
	}

Long-time Web developers will of course recognize that the basic Hyperlink as created above is the same as we’ve been doing for 15 years now. The beauty of gwt-presenter is that with the same simple URL construction, you can easily target one section of the page for dynamic update, and do it in a way that works with browser history and bookmarks to boot. There might be something to this GWT thing.

Posted in Google Web Toolkit, Model-View-Presenter | 11 Comments »

Navigating to a new view with gwt-presenter, part 2

Posted by David Chandler on October 1, 2009

In the previous post, we considered how to switch between views using a DeckPanel. Depending on the situation, one possible disadvantage to a DeckPanel is that it retains each member view widget in memory even when not displayed. If instead you want to remove a view widget from its container when transitioning to another view, you can use gwt-presenter’s WidgetContainerPresenter and WidgetContainerDisplay classes. Here is the same example shown with these classes in which we use a VerticalPanel as a container and navigate from the ManageListsPresenter/View to the AddPrayerListPresenter/View within the VerticalPanel. Note that the ContentContainerPresenter constructor calls showPresenter(). This is very important, as our view’s implementation of showWidget() calls clear() and add() to replace the existing contents of the VerticalPanel with the new widget. If you don’t call showPresenter() in the container constructor, then the default behavior of WidgetContainerPresenter is to add all the member presenters to the view.

package com.roa.client.presenter;

import net.customware.gwt.presenter.client.EventBus;
import net.customware.gwt.presenter.client.place.Place;
import net.customware.gwt.presenter.client.place.PlaceRequest;
import net.customware.gwt.presenter.client.widget.WidgetContainerDisplay;
import net.customware.gwt.presenter.client.widget.WidgetContainerPresenter;
import net.customware.gwt.presenter.client.widget.WidgetPresenter;

public class ContentContainerPresenter extends WidgetContainerPresenter<ContentContainerPresenter.Display>
{

	private static final Place PLACE = new Place("content");

	public interface Display extends WidgetContainerDisplay
	{
	}

	public ContentContainerPresenter(Display display, EventBus eventBus,
			WidgetPresenter<?>[] presenters)
	{
		super(display, eventBus, presenters);
		bind();
		// Show first presenter as default
		showPresenter(presenters[0]);
	}

	@Override
	public Place getPlace()
	{
		return PLACE;
	}

	@Override
	protected void onPlaceRequest(PlaceRequest request)
	{
		// TODO Auto-generated method stub
	}

}

And here is the matching view that uses a VerticalPanel as the container widget:

package com.roa.client.ui.web.content;

import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.roa.client.presenter.ContentContainerPresenter.Display;

public class ContentContainerView implements Display
{

	private final VerticalPanel contentPanel = new VerticalPanel();

	public ContentContainerView()
	{
	}

	@Override
	public void addWidget(Widget widget)
	{
		contentPanel.add(widget);
	}

	@Override
	public void removeWidget(Widget widget)
	{
		contentPanel.remove(widget);
	}

	@Override
	public void showWidget(Widget widget)
	{
		contentPanel.clear();
		contentPanel.add(widget);
	}

	@Override
	public Widget asWidget()
	{
		return contentPanel;
	}

	@Override
	public void startProcessing()
	{
		// TODO Auto-generated method stub
	}

	@Override
	public void stopProcessing()
	{
		// TODO Auto-generated method stub
	}

}

These are wired up in AppPresenter and the view change triggered with a PlaceRequestEvent just as shown in the previous post.

Posted in Google Web Toolkit, Model-View-Presenter | 13 Comments »

Navigating to a new view with gwt-presenter

Posted by David Chandler on October 1, 2009

How can you move to a new “view” with gwt-presenter? In GWT, you wouldn’t ordinarily replace the entire page as in a server-side Web app. Suppose you have one area of the screen, say, a VerticalPanel, in which you want to show any number of functional views (i.e, manage lists, edit list properties, etc.). One way to do this is using a DeckPanel, which is the base class behind GWT’s TabPanel widget. DeckPanel has no visible UI controls, but lets you programatically swap out one widget for another just as if you were clicking tabs. Gwt-presenter provides DeckPresenter and DeckDisplay to help with just that. Here’s a sample DeckPresenter class.

package com.roa.client.presenter;

import net.customware.gwt.presenter.client.EventBus;
import net.customware.gwt.presenter.client.place.Place;
import net.customware.gwt.presenter.client.place.PlaceRequest;
import net.customware.gwt.presenter.client.widget.DeckDisplay;
import net.customware.gwt.presenter.client.widget.DeckPresenter;
import net.customware.gwt.presenter.client.widget.WidgetContainerDisplay;
import net.customware.gwt.presenter.client.widget.WidgetPresenter;

public class ContentContainerPresenter extends DeckPresenter
{

	private static final Place PLACE = new Place("content");

	public interface Display extends WidgetContainerDisplay
	{
	}

	public ContentContainerPresenter(DeckDisplay display, EventBus eventBus,
			WidgetPresenter<?>[] presenters)
	{
		super(display, eventBus, presenters);
		bind();
		// One way to show default presenter--see also AppPresenter below for another way
		// showPresenter(presenters[0]);
	}

	@Override
	public Place getPlace()
	{
		return PLACE;
	}

	@Override
	protected void onPlaceRequest(PlaceRequest request)
	{
		// TODO Auto-generated method stub
	}

}

And here’s the corresponding view:

package com.roa.client.ui.web.content;

import net.customware.gwt.presenter.client.widget.DeckDisplay;

import com.google.gwt.user.client.ui.Widget;

public class ContentContainerView extends DeckDisplay
{

	public ContentContainerView()
	{
	}

	@Override
	public void addWidget(Widget widget)
	{
		super.add(widget);
	}

	@Override
	public void removeWidget(Widget widget)
	{
		super.remove(widget);
	}

	@Override
	public void showWidget(Widget widget)
	{
		// Refer up to DeckPanel
		super.showWidget(widget);
	}

	@Override
	public Widget asWidget()
	{
		return this;
	}

	@Override
	public void startProcessing()
	{
		// TODO Auto-generated method stub

	}

	@Override
	public void stopProcessing()
	{
		// TODO Auto-generated method stub

	}

}

Gwt-presenter’s DeckDisplay, unlike the other gwt-presenter interfaces named “Display”, is actually a concrete class that extends GWT’s DeckPanel. As such, we can use it as our view implementation directly. Now let’s wire them up. Because DeckPresenter’s constructor takes an array of presenters as an argument, there’s no way (that I know of, at least) to instantiate it with GIN using @Inject. So in our AppPresenter, we manually construct the needed classes like this:

	@Inject
	private EventBus eventBus;
	@Inject
	private ManageListsPresenter listsPresenter;
	@Inject
	private AddPrayerListPresenter addPresenter;
...
		// Instantiate container presenter manually as its constructor
		// prevent GIN binding
		WidgetPresenter<?>[] presenters = new WidgetPresenter<?>[]
		{ listsPresenter, addPresenter };
		ContentContainerView contentContainerView = new ContentContainerView();
		ContentContainerPresenter contentContainerPresenter = new ContentContainerPresenter(
				contentContainerView, eventBus, presenters);
		// Fire event to reveal only default presenter
		eventBus.fireEvent(new PresenterRevealedEvent(listsPresenter));

In order to show the default presenter (in this case, Manage Lists), we fire a PresenterRevealedEvent after constructing the ContentContainerPresenter. Without it, however, the default behavior of GWT presenter is to show the widgets associated with all presenters that have been added to the container.

Finally, we need to fire an event to transition from one view to another. We could use a PresenterRevealedEvent; however, the PlaceRequestEvent does much more. Gwt-presenter’s BasicPresenter class (from which WidgetContainerPresenter and DeckPresenter are derived) listens for PlaceRequestEvents, marks the Place in History, calls onPlaceRequest() for the requested presenter, and finally calls the requested presenter’s revealDisplay() method, which in turn fires a PresenterRevealedEvent.

The code below will trigger a transition from the ManageListsPresenter to the AddPrayerListPresenter when the add button is clicked. The easiest way to get a reference to the presenter we’re going to is simply to make its PLACE constant public so we can use it in PlaceRequests from other classes.

public class ManageListsPresenter extends
		WidgetPresenter<ManageListsPresenter.Display>
{
		display.getAddButton().addClickHandler(new ClickHandler()
		{
			@Override
			public void onClick(ClickEvent event)
			{
				PlaceRequest placeRequest = new PlaceRequest(AddPrayerListPresenter.PLACE);
				eventBus.fireEvent(new PlaceRequestEvent(placeRequest));
			}
		});
...
}

Posted in Google Web Toolkit, Model-View-Presenter | 3 Comments »

 
%d bloggers like this: