Thursday, July 22, 2010

Converting to the MVP architecture

If you see one thing referenced again and again in writings about GWT development, it's Ray Ryan's Google IO 2009 talk on GWT Architecture Best Practices. In the talk he describes using the MVP architecture to develop GWT applications that allow easier unit testing and clean logic separation. There's also a couple of articles about large scale application development and MVP also at Google Code. And there's a million other things you can read with a quick search.

I've used the MVC architecture for other projects, and it's treated me pretty well, but you can still end up with some code that's difficult to follow from beginning to end. This is generally because of the View cutting the Controller out of the loop in going to the Model. MVP looks like it could help resolve that, so I wanted to give it a shot while learning GWT.

Finding a framework


I also didn't want to do all of the work involved in developing my own framework to support MVP, so after looking around to see what open source projects are available, I came across gwt-presenter. Several blogs and examples talk about this framework, and early on I had decided that I'd use it for this project. But now that I'm at the stage where I want to implement it, I looked a little closer.

It doesn't seem to be in active development. The last release was in August 2009, and GWT 2.0 was not yet released. There are two branches, and the GAE/GWT development blog talks about using the "replace" branch, but I'm not sure what stage it's at.

Though this may be due to my own prejudices, I preferred something that's currently being hacked, and I found that in mvp4g. It also looks to have some more features without being bogged down, have some more complete documentation, uses JUnit with pretty high coverage, and a discussion group where the developer regularly responds to questions. So I'm giving it a shot, at least the 1.2.0 snapshot (found in the examples zip), since it supports Gin.

Moving to mvp4g


The code for this stage is available under the "add-mvp4g" tag on github, or in a zip download.

I'm going to convert the login page that we currently have working (well, working is probably a bit too strong) to using this framework. The mvp4g FAQ has some good, quick explanations of their architecture, including a class diagram that I didn't find terribly useful until I got into implementing it.

The first step, as usual, is to include the jar in your classpath, as well as commons-lang and commons-configuration. We also need to inherit it in our .gwt.xml module file.

com/lisedex/volinfoman/Volinfoman.gwt.xml
<!-- Mvp4g configuration -->
       <inherits name='com.mvp4g.Mvp4gModule'/>

We fire it up in our entry point class.

com/lisedex/volinfoman/client/Volinfoman.java
// code to initialize mvp4g, which handles setting up our
      // Ginjection
      Mvp4gModule module = (Mvp4gModule)GWT.create( Mvp4gModule.class );
      module.createAndStartModule();
      RootPanel.get().add( (Widget)module.getStartView() );

If you prefer to use a layout panel for your application, the last line can use RootLayoutPanel instead of the RootPanel.

Event bus


Most of the initial configuration is done through annotations in our event bus class, so we'll set that up next; apparently you can also set mvp4g up through an XML file, but I didn't play with that.

com/lisedex/volinfoman/client/VolinfomanEventBus.java
@Events(startView = LoginView.class, historyOnStart = true,
        ginModule=VolinfomanModule.class)
@Debug( logLevel = LogLevel.DETAILED, logger = 
        Mvp4gLoggerToGwtLogAdapter.class )
public interface VolinfomanEventBus extends EventBus {
        @Event(handlers = LoginPresenter.class)
        public void login();
}

The @Events annotation declares what View our application will start in, whether it will parse any history information embedded in the URL that starts the application (we currently don't use this, but I have it enabled anyways), and what Gin module, if any, we want it to use to configure a Ginjector. Previously we defined the Ginjector in the onModuleLoad2() method in our entry point, but for mvp4g we pull it out and let the framework handle it.

I also turn on debugging using the @Debug annotation, and set the logger to one that I wrote that forwards mvp4g's logging calls to gwt-log instead of GWT.log. I wanted all of the logging in one place. It's a very simple class, so I'm going to skip it here.

Finally, we declare an interface that extends EventBus which defines all of the events that can be raised on the bus. The events should not return anything, and can have either one or zero parameters. We don't need a parameter for the login event, since the presenter that handles it will have access to the fields the user has filled out. The @Event annotation declares which classes, separated by a comma, are registered to be notified of the event.

Presenter


com/lisedex/volinfoman/client/LoginPresenter.java
@Presenter(view = LoginView.class)
public class LoginPresenter extends BasePresenter
        <LoginPresenter.LoginViewInterface, VolinfomanEventBus> {

The Presenter will be matched with a View, so we declare that association through the @Presenter annotation, which also lets the mvp4g compiler know that this class defines a Presenter. Our presenter extends BasePresenter, which is a generic that takes the class defining our View interactions (LoginViewInterface), and our Event Bus class (VolinfomanEventBus) as parameters.

The Presenter class handles the logic that will make the interface displayed to the user actually do something. In this case, we're working with a login page, so we need to define what we need for interactions with the View. I followed the mvp4g documentation, which defines it using an inner interface. I don't see why you couldn't break it out to a separate file, but it's a bit more clear having it bound to the Presenter class. Plus it's right there to reference when writing your Presenter.

public interface LoginViewInterface {
  public String getUsername();
  public String getPassword();
  public void setMessage(String msg);
  public HasClickHandlers getLoginButton();
  public void setLoginButtonEnabled(boolean enabled);
 }

We need a few things from the user, and we don't really care how the View gets them. All we care about is that it can provide the information we need to process the login. In this case, we want to be able to get the username and password they've entered, and we also want to be able to send them messages, in case of an error. We also need to find out when the user's told the interface they've completed their data entry, so we want to bind to something that throws Click events; we don't really care if it's a button or not. I also need to be able to control whether the user can request to log in; for instance, if they've just clicked, until the server responds, I don't want them to be able to keep clicking. If the View is not implementing a button, they can use this information in whatever way makes sense. In fact, writing this, it really shouldn't be called setLoginButtonEnabled, but something like setLoginEnabled instead.

The Presenter is going to perform the logic we had in our previous DefaultHomepage class, so we need our UserService.

private UserServiceAsync userService = null;

 @InjectService
 public void setService(UserServiceAsync service) {
  this.userService = service;
 }

mvp4g supports injecting these services (both RPC and non-RPC) with the @InjectService annotation. You define them as you normally would, with both Service and the ServiceAsync version. I didn't have to change the code at all.

The bind() method is called by the framework for us to do further set up of our interactions with the View. The BasePresenter we're extending provides both view and eventBus references so we don't need to handle that.

@Override
 public void bind() {
  super.bind();
  view.getLoginButton().addClickHandler(new ClickHandler() {
   public void onClick(ClickEvent event) {
    view.setLoginButtonEnabled(false);
    eventBus.login();
   }
  });
 }

All I want right now is to know when the login button (or whatever the View is using) is clicked, and when it is, I turn off the login button and fire a login event onto the Event Bus. This will allow some flexibility later, if I need to have another class know about logins.

Finally we define the method to handle the login event. All events are handled by methods that start with "on" followed by the event name. In this case, it will be onLogin.

public void onLogin() {
 if (userService == null) {
  Log.fatal("userService is null, not injected", 
    new NullPointerException("LoginPresenter.userService"));
  view.setMessage("Fatal application error.  \"userService not injected.\"");
  view.setLoginButtonEnabled(true);
  return;
 }
  
 userService.getUser(view.getUsername(), new AsyncCallback<User>() {
  @Override
  public void onFailure(Throwable caught) {
   view.setMessage("RPC FAILED");
   view.setLoginButtonEnabled(true);
  }

  @Override
  public void onSuccess(User result) {
   if (result == null) {
    view.setMessage("NO SUCH USER");
   } else {
    view.setMessage("SUCCESS: " + result.toString());
   }
   view.setLoginButtonEnabled(true);
  }
 });
}

This is pretty much the click handler that was in DefaultHomepage. We first do a quick sanity check for our UserService, and if it's not there, there's something really wrong. Then we call our service's getUser method, and when it returns we either tell the user the user doesn't exist, or spit out the user's information. Then we turn the login button back on.

View


The View just sets up visual aspect of the interface the user sees. In this case, we're still using a UiBinder, copied from the DefaultHomepage, and then adding the methods to flesh out the View interface we built in the Presenter.

com/lisedex/volinfoman/client/LoginView.java
public class LoginView extends Composite 
 implements LoginPresenter.LoginViewInterface {

 private static LoginViewUiBinder uiBinder = GWT
  .create(LoginViewUiBinder.class);

 interface LoginViewUiBinder extends UiBinder<Widget, LoginView> {
 }

The View is going to be a Composite widget, and it will implement the View interface. We also bind the View with its associated LoginView.ui.xml file, so we can use the fields declared there to provide information to the Presenter.

@UiField
 TextBox username;
 
 @UiField
 TextBox password;
 
 @UiField
 Button sendButton;
 
 @UiField
 HTML sendStatus;
 
 public LoginView() {
  initWidget(uiBinder.createAndBindUi(this));
  
  sendButton.setText("Login");
  
  sendStatus.setStyleName("serverResponseLabelError");
  
  DeferredCommand.addCommand(new Command() {
   public void execute() {
    username.setFocus(true);
   }
  });
  username.selectAll();
 }

This code is basically the same as what was in DefaultHomepage. We bind the XML defined user interface, then we set the text for our login button. We also set the style for the place we'll send messages to the user, so text shows up red, and then we set the focus on the username field. Setting focus did not work in previous versions of our code, so after a quick search I found this workaround described in GWT issue 1849, where they discuss making this the default implementation of setFocus().

The rest of the class is just implementing the LoginViewInterface.

@Override
 public String getUsername() {
  return username.getText();
 }

 @Override
 public String getPassword() {
  return password.getText();
 }

        .... etc, etc.

They're all just basic getters and setters, so I won't include them here, but you can take a peek at the complete code if you want to see the whole thing.

Some notes


I like the way mvp4g seems to work, and reading past posts on its discussion group was useful in figuring some of it out. There's other functionality I haven't even touched yet: code-splitting through using different modules for different parts of the interface, lazy loading, and, most importantly, history. I'll be adding history as soon as I have a second page or something where history makes sense. The others seem to be optimizations which we don't need yet.

I also may want to implement the command pattern later, to have more complete bits of information passed with their event on the event bus.

I've been avoiding it long enough, but now it's time to figure out a good authentication/session design, since I don't want to require the volunteers using this to have a Google account. If I did, it would pretty much be implemented for me in the App Engine Users API.

3 comments:

  1. Wow, very nice. As a GWT/mvp4g newbie I have been struggling through my first test apps and I found this blog to be VERY helpful!!!

    Thanks!!!

    ReplyDelete
  2. Hi Bob, I'm glad you found it useful. I'll continue to document the things I've struggled with myself, so feel free to check back and see if I've covered something else you're interested in.

    ReplyDelete
  3. Hi John, thank you for this post. It clarifies a lot. I'm about to start using mvp4g

    ReplyDelete