Tuesday, June 8, 2010

The GWT Handlers, Events, and the Event Bus

We have setup our MVP framework, and made our first GWT-RPC service call. Now it is time to create some event wiring. After all, those buttons we painted on the ContactsView should actually do something. So how is this done?

It is very simple. The first thing you do is to add a handler to a particular widget. For example, you would add a ClickHandler to a Button. By adding this handler, you can choose to fire an event. You fire events in the eventBus (which is an instance of the HandlerManager we created in the EntryPoint).

Next, you can then add handlers to the eventBus that will listen for that specific event you fired, and perform whatever task you like.

In our MVP framework, all the wiring of handlers and events is done in the presenters. So the binding of the ClickHandler goes there, the firing of the event in the eventBus goes there, as well as the binding of the handler on the eventBus to pick up that fired event goes there (but most likely in a different presenter - more on that later).


The Add Contact Event

Let's start by adding an event to our Add Contact button. This should be relatively straightforward. What we want is for any click on that button to show a popup screen where you can provide a firstName, lastName, and email. This popup will also have a button for saving and canceling, but we will deal with that later.

First we need to create our event. To create the proper plumbing for an event, we need two parts: a class that extends GwtEvent and an interface that extends EventHandler. The GwtEvent is the event that you will create when something is clicked. The EventHandler is the one that performs an action when that event is fired. We will create both in a new package called gaej.example.contacts.client.events so that all event plumbing is nicely organized together.

First we add our EventHandler interface. Since it is an event that gets created when we click the Add Contact button, we will call this the AddContactEventHandler:
package gaej.example.contacts.client.event;

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

public interface AddContactEventHandler extends EventHandler {
 //TODO: add action method onAddContact(AddContactEvent)
}

Our current incarnation of the interface does not have much, but as we create the Event itself, we will start to add methods to it. Let's now create the second part of our event plumbing - the AddContactEvent:
package gaej.example.contacts.client.event;

import gaej.example.contacts.client.event.AddContactEventHandler;
import com.google.gwt.event.shared.GwtEvent;

public class AddContactEvent extends GwtEvent<AddContactEventHandler> {

 public static Type<AddContactEventHandler> TYPE = new Type<AddContactEventHandler>();
 
 @Override
 protected void dispatch(AddContactEventHandler handler) {
  // TODO Auto-generated method stub

 }

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

}

This one has more substance to it. Here we create an AddContactEvent that extends GwtEvent with handler type AddContactEventHandler. We have a public static TYPE property to easily determine the type of the event. The important method for us is the dispatch(AddContactEventHandler handler) method. Here we need to put the action method that we are going to add to the interface AddContactEventHandler now. Open up AddContactEventHandler.java and edit so that it looks like follows:
package gaej.example.contacts.client.event;

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

public interface AddContactEventHandler extends EventHandler {
 public void onAddContact(AddContactEvent event);
}

All we did was add an onAddContact(AddContactEvent event) method to the interface. Now we need to call this method from within the dispatch method of AddContactEvent:
package gaej.example.contacts.client.event;

import gaej.example.contacts.client.event.AddContactEventHandler;
import com.google.gwt.event.shared.GwtEvent;

public class AddContactEvent extends GwtEvent<AddContactEventHandler> {

 public static Type<AddContactEventHandler> TYPE = new Type<AddContactEventHandler>();
 
 @Override
 protected void dispatch(AddContactEventHandler handler) {
  handler.onAddContact(this);
 }

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

}

Alright, now that we have created our event, we can start binding it to the proper widget.

Binding the AddContactEvent

To bind our events to the widgets, we create a private void method called bind() in the ContactsPresenter. Go ahead and open ContactsPresenter.java and add the following method:
private void bind() {
  display.getAddButton().addClickHandler(new ClickHandler() {
   public void onClick(ClickEvent event){
    eventBus.fireEvent(new AddContactEvent());
   }
  });
 }

After adding this method, be sure to also add the appropriate imports at the top of the file.

So what happens here? Again we create a callback, but this time for an event. We add a new ClickHandler object to our Add button. This callback object has one method onClick(ClickEvent event) which does only one thing: it adds a new AddContactEvent object to the eventBus. Later on we will see how we will bind to the eventBus to pick up this particular event, but for now, let's edit the go(HasWeidgets container) method so that we call this new bind() method from there.
@Override
 public void go(HasWidgets container) {
  container.clear();
  container.add(display.asWidget());
  fetchLightWeightContacts();
  bind();
 }

Now that we have created the event firing mechanism, and bound it to the widget, its time to create the receiving end.

ContactPopupPresenter

Let's step back for a second. Remember that we want to show a popup where we can add our new contact his or her details the moment the Add button is clicked? In other words, we need to create a ContactPopupPresenter and a ContactPopupView which this event will trigger.

So to complete this event, we will first create the ContactPopupPresenter:
package gaej.example.contacts.client.presenter;

import gaej.example.contacts.client.ContactsServiceAsync;
import gaej.example.contacts.shared.Contact;

import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.Widget;

public class ContactPopupPresenter implements Presenter {

 public interface Display {
  HasClickHandlers getSaveButton();
  HasClickHandlers getCancelButton();
  HasValue<String> getFirstName();
  HasValue<String> getLastName();
  HasValue<String> getEmail();
  DialogBox getDialogBox();
  Widget asWidget();
 }
 
 private Contact contact;
 private final HandlerManager eventBus;
 private final ContactsServiceAsync rpcService;
 private final Display display;
 
 public ContactPopupPresenter(HandlerManager eventBus, ContactsServiceAsync rpcService, Display display) {
  this.eventBus = eventBus;
  this.rpcService = rpcService;
  this.display = display;
 }
 
 private void bind() {
  // TODO: add widget binding here
 }
 
 @Override
 public void go(HasWidgets container) {
  // Nothing to bind to - we are a popup!
 }

}

As you can see, it is very similar to our ContactsPresenter. We define a Display interface which our ContactPopupView will use (see later), we have a constructor that takes an eventBus, rpcService, and display instance, we have a bind() method that we will use later to add our event bindings to, and finally a go(HasWidgets container) method where we do absolutely nothing. The reason for this is because this is a popup, and it will not have its own page (more on this later once we discuss the browser history buttons).

ContactPopupView

Now we can flesh out the ContactPopupView:
package gaej.example.contacts.client.view;

import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;

import gaej.example.contacts.client.presenter.ContactPopupPresenter;

public class ContactPopupView extends DialogBox implements ContactPopupPresenter.Display {

 private final TextBox firstName;
 private final TextBox lastName;
 private final TextBox email;
 private final TextBox id;
 private final FlexTable detailsTable;
 private final Button saveButton;
 private final Button cancelButton;
 
 public ContactPopupView() {
  
  setText("Edit Contact");
  setAnimationEnabled(true);
  
  VerticalPanel contactDetailsPanel = new VerticalPanel();
  contactDetailsPanel.setWidth("100%");
  
  // Create the contacts list
  detailsTable = new FlexTable();
  detailsTable.setCellSpacing(0);
  detailsTable.setWidth("100%");
  detailsTable.setStyleName("contacts-ListContainer");
  detailsTable.getColumnFormatter().addStyleName(1, "add-contact-input");
  id = new TextBox();
  id.setReadOnly(true);
  firstName = new TextBox();
  lastName = new TextBox();
  email = new TextBox();
  initDetailsTable();
  contactDetailsPanel.add(detailsTable);
  
  // Create the menu
  HorizontalPanel menuPanel = new HorizontalPanel();
  saveButton = new Button("Save");
  cancelButton = new Button("Cancel");
  menuPanel.add(saveButton);
  menuPanel.add(cancelButton);
  contactDetailsPanel.add(menuPanel);
  
  setWidget(contactDetailsPanel);
  
 }
 
 private void initDetailsTable() {
  detailsTable.setWidget(0, 0, new Label("Firstname"));
  detailsTable.setWidget(0, 1, firstName);
  detailsTable.setWidget(1, 0, new Label("Lastname"));
  detailsTable.setWidget(1, 1, lastName);
  detailsTable.setWidget(2, 0, new Label("Email"));
  detailsTable.setWidget(2, 1, email);
  firstName.setFocus(true);
 }
 
 @Override
 public Widget asWidget() {
  return this;
 }

 @Override
 public HasClickHandlers getCancelButton() {
  return cancelButton;
 }

 @Override
 public DialogBox getDialogBox() {
  return this;
 }

 @Override
 public HasValue<String> getEmail() {
  return email;
 }

 @Override
 public HasValue<String> getFirstName() {
  return firstName;
 }

 @Override
 public HasValue<String> getLastName() {
  return lastName;
 }

 @Override
 public HasClickHandlers getSaveButton() {
  return saveButton;
 }

}

Picking Up The Event From The Event Bus

Now that we have our presenter and view in place, we can add the necessary binding so that our ContactPopupPresenter paints the ContactPopupView on screen the moment it receives an AddContactEvent from the eventBus.

Open up ContactPopupPresenter.java and add the following to its bind() method:
private void bind() {

  eventBus.addHandler(AddContactEvent.TYPE, new AddContactEventHandler() {
   public void onAddContact(AddContactEvent event) {
    contact = new ContactImpl();
    setValues();
    display.getDialogBox().center();
   }
  });

 }

As you can see, we add a handler to our eventBus that will handle any event of type AddContactEvent. If the eventBus receives any such event, the onAddContact(AddContactEvent event) method will get called, and in it, we create a new empty Contact object, and paint our ContactPopupView on screen. But wait, notice the call to setValues() method? This is a private method we need to add as well. What this will do is display the fields of the newly created contact object in the popup's fields. Let's add it now:
 private void setValues() {
  display.getFirstName().setValue(contact.getFirstName());
  display.getLastName().setValue(contact.getLastName());
  display.getEmail().setValue(contact.getEmail());
 }

But why do this if the fields are empty? True, in this case it may not make much sense, but imagine if we were to make our app in such a way that any newly created Contact starts with some kind of dummy data, or a pre-generated id. In that case, we will need to show it in the fields. Also, later on when we start to add the plumbing for editing of Contacts, you will see its a good idea to do this.

Adding The Presenter To The AppController

Just like with the ContactPresenter before, we also need to instantiate ContactPopupPresenter in the AppController. Open up AppController.java and edit it so that it will look like below:
package gaej.example.contacts.client;

import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.user.client.ui.HasWidgets;

import gaej.example.contacts.client.presenter.ContactPopupPresenter;
import gaej.example.contacts.client.presenter.Presenter;
import gaej.example.contacts.client.ContactsServiceAsync;
import gaej.example.contacts.client.presenter.ContactsPresenter;
import gaej.example.contacts.client.view.ContactPopupView;
import gaej.example.contacts.client.view.ContactsView;

public class AppController implements Presenter {

 private final HandlerManager eventBus;
 private final ContactsServiceAsync rpcService;
 private HasWidgets container;
 
 private ContactsPresenter contactsPresenter;
 private ContactPopupPresenter contactPopupPresenter;
 
 AppController(HandlerManager eventBus, ContactsServiceAsync rpcService) {
  this.eventBus = eventBus;
  this.rpcService = rpcService;
  
  bind();
 }
 
 private void bind() {
  
  contactsPresenter = new ContactsPresenter(eventBus, rpcService, new ContactsView() );
  contactPopupPresenter = new ContactPopupPresenter(eventBus, rpcService, new ContactPopupView() );
  
 }
 
 @Override
 public void go(HasWidgets container) {
  this.container = container;
  contactsPresenter.go(this.container);
  contactPopupPresenter.go(this.container);
 }

}

You may wonder why we actually call contactPopupPresenter.go(this.container) since it does not do anything. We do this more out of conformity, and you will see later when we add the History (browser back and forward button) why we do this.

Fire up the application and click on the Add button to see what you have done so far. You will notice our new ContactPopupView pops up. Nice, but we cant do anything with it anymore since the Save and Cancel buttons don't work yet.

The Cancel Button

Let's start by adding a ClickEvent to the Cancel button so we click this dialog away. Open up ContactPopupPresenter.java and add the following ClickHandler to the bind() method:
display.getCancelButton().addClickHandler(new ClickHandler() {
   public void onClick(ClickEvent event) {
    display.getDialogBox().hide();
   }
  });

Now if you press the Cancel button, the popup box will disappear, just as expected. That was easy! How about the Save button?

The Save Button

The Save button ClickHandler is a little bit more complex, but only a little. What we want to do is, once the Save button is clicked, the popup gets closed, and the contacts list on the initial screen gets updated. This means we need to add a ClickHandler to the Save button, as well as fire an event that tells the ContactsPresenter to update the list for displaying!

OK, so first up let's add the new EventHandler interface, which we shall call SavedContactEvent:
package gaej.example.contacts.client.event;

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

public interface SavedContactEventHandler extends EventHandler {
 public void onSavedContact(SavedContactEvent event);
}

And now the SavedContactEvent class:
package gaej.example.contacts.client.event;

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

public class SavedContactEvent extends GwtEvent<SavedContactEventHandler> {

 public static Type<SavedContactEventHandler> TYPE = new Type<SavedContactEventHandler>();
 
 @Override
 protected void dispatch(SavedContactEventHandler handler) {
  handler.onSavedContact(this);
 }

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

}

Alright, the event plumbing is in place. Let's now add the appropriate code to the ContactPopupPresenter. First add the ClickHandler to the bind() method:
private void bind() {
  
  eventBus.addHandler(AddContactEvent.TYPE, new AddContactEventHandler() {
   public void onAddContact(AddContactEvent event) {
    contact = new ContactImpl();
    display.getDialogBox().center();
   }
  });
  
  display.getCancelButton().addClickHandler(new ClickHandler() {
   public void onClick(ClickEvent event) {
    display.getDialogBox().hide();
   }
  });
  
  display.getSaveButton().addClickHandler(new ClickHandler() {
   public void onClick(ClickEvent event){
    doUpdate();
    display.getDialogBox().hide();
   }
  });
  
 }

Now we add the private method doUpdate() to ContactPopupPresenter:
private void doUpdate() {
  contact.setFirstName(display.getFirstName().getValue());
  contact.setLastName(display.getLastName().getValue());
  contact.setEmail(display.getEmail().getValue());
  
  rpcService.updateContact(contact, new AsyncCallback<Contact>() {
   public void onSuccess(Contact result) {
    eventBus.fireEvent(new SavedContactEvent());
   }
   public void onFailure(Throwable caught) {
    Window.alert("Failed to save contact!");
   }
  });
 }

So now we added the ClickHandler to the Save button, which calls the private method doUpdate(). This method then makes and rpcService call to update the contact (remember that we created an empty Contact object when we clicked the Add Contact button on the initial screen). If the saving of the Contact is successful, we fire of a SavedContactEvent and close the ContactPopupView.

Now let's add the handler to the eventBus to pick up this SavedContactEvent, and refresh the contact list on the initial screen:
private void bind() {
  display.getAddButton().addClickHandler(new ClickHandler() {
   public void onClick(ClickEvent event){
    eventBus.fireEvent(new AddContactEvent());
   }
  });
  
  eventBus.addHandler(SavedContactEvent.TYPE, new SavedContactEventHandler() {
   @Override
   public void onSavedContact(SavedContactEvent event) {
    fetchLightWeightContacts();
   }
   
  });
 }

All done! Go ahead and give the app a spin to see if you can add a contact. Next up we will create a slightly more complex EventHandler which can transmit data as well. This is useful when you want to do some conditional action, such as edit a contact when the user clicks on a specific contact in the contact list.

No comments:

Post a Comment