Hot questions for Using EventBus in gwt

Question:

I have a GWT Cell Tree that I use to display a file structure from a CMS. I am using a AsyncDataProvider that loads data from a custom RPC class I created. I also have a Web Socket system that will broadcast events (File create, renamed, moved, deleted etc) from other clients also working in the system.

What I am trying to wrap my head around is when I recieve one of these events, how I correctly update my Cell Tree?

I suppose this problem would be analogus to having two instances of my Cell Tree on the page, which are presenting the same server-side data and wanting to ensure that when the user updated one, that the other updated as well, via using the EventBus.

I feel this should be pretty simple but I have spent about 6 hours on it now with no headway. My code is included below:

NOTE: I am not using RequestFactory even though it may look like I am it is my custom RPC framework. Also, FileEntity is just a simple representation of a file which has a name accessible by getName().

private void drawTree() {

        // fileService is injected earlier on and is my own custom rpc service
        TreeViewModel model = new CustomTreeModel(new FileDataProvider(fileService));
        CellTree tree = new CellTree(model, "Root");

        tree.setAnimationEnabled(true);

        getView().getWorkspace().add(tree);

    }

    private static class CustomTreeModel implements TreeViewModel {

        // I am trying to use a single AsyncDataProvider so I have a single point of loading data which I can manipulate (Not sure if this is the correct way to go)
        public CustomTreeModel(FileDataProvider dataProvider) {
            this.provider = provider;
        }


        public <T> NodeInfo<?> getNodeInfo(final T value) {

            if (!(value instanceof FileEntity)) {

                                // I already have the root File loaded in my presenter, if we are at the root of the tree, I just add it via a list here
                ListDataProvider<FileEntity> dataProvider = new ListDataProvider<FileEntity>();

                dataProvider.getList().add(TreeWorkspacePresenter.rootFolder);

                return new DefaultNodeInfo<FileEntity>(dataProvider,
                        new FileCell());
            } else {

                                // Otherwise I know that we are loading some tree child data, and I invoke the AsyncProvider to load it from the server
                provider.setFocusFile(value);

                return new DefaultNodeInfo<FileEntity>(provider,
                        new FileCell());
            }
        }

        public boolean isLeaf(Object value) {

            if(value == null || value instanceof Folder)
                return false;
            return true;
        }

    }

    public class FileDataProvider extends AsyncDataProvider<FileEntity> {

        private FileEntity focusFile;
        private FileService service; 

        @Inject
        public FileDataProvider(FileService service){
            this.service = service;
        }

        public void setFocusFile(FileEntity focusFile){
            this.focusFile = focusFile;
        }

        @Override
        protected void onRangeChanged(HasData<FileEntity> display) {


                service.getChildren(((Folder) focusFile),
                        new Reciever<List<FileEntity>>() {

                            @Override
                            public void onSuccess(List<FileEntity> files) {

                                updateRowData(0, files);

                            }

                            @Override
                            public void onFailure(Throwable error) {

                                Window.alert(error.toString());
                            }

                        });

            }
        }



    /**
     * The cell used to render Files.
     */
    public static class FileCell extends AbstractCell<FileEntity> {

        private FileEntity file;

        public FileEntity getFile() {
            return file;

        }

        @Override
        public void render(Context context, FileEntity file, SafeHtmlBuilder sb) {
            if (file != null) {
                this.file = file;
                sb.appendEscaped(file.getName());
            }
        }
    }

Answer:

Currently there is no direct support for individual tree item refresh even in the latest gwt version.

But there is a workaround for this. Each tree item is associated with an value. Using this value you can get the corresponding tree item.

In your case, i assume, you know which item to update/refresh ie you know which File Entity has changed. Use this file entity to search for the corresponding tree item. Once you get the tree item you just need to expand and collapse or collapse and expand its parent item. This makes parent item to re-render its children. Your changed file entity is one among the children. So it get refreshed.

public void refreshFileEntity(FileEntity fileEntity)
{
       TreeNode fileEntityNode = getFileEntityNode(fileEntity, cellTree.getRootTreeNode() 

       // For expnad and collapse run this for loop

       for ( int i = 0; i < fileEntityNode.getParent().getChildCount(); i++ )
    {
        if ( !fileEntityNode.getParent().isChildLeaf( i ) )
        {
            fileEntityNode.getParent().setChildOpen( i, true );
        }
    }

}

public TreeNode getFileEntityNode(FileEntity fileEntity, TreeNode treeNode)
{
       if(treeNode.getChildren == null)
       {
           return null;
       }
       for(TreeNode node : treeNode.getChildren())
       {
            if(fileEntity.getId().equals( node.getValue.getId() ))
            {
                 return node;
            }
            getEntityNode(fileEntity, node); 
       }
}

Question:

I want to be able to listen to all events dispatched on the Event Bus, regardless of type. How can I do this?


Answer:

Taking a cue from Thomas's suggestion, here is a sample solution:

    public class MyEventBus extends SimpleEventBus {

    @Override
    public void fireEvent( GwtEvent<?> event ) {
        trackEvent( "Event Fired", event.getClass().toString() );
        super.fireEvent( event );
    }

    public native void trackEvent( String category, String action ) /*-{
        $wnd._gaq.push([ '_trackEvent', category, action ]);
    }-*/;
}

Question:

I'm starting with GWT and learning Event bus concepts now. I find this solution extremely complicated. So I tried to simplify it by writing prototype by myself to see all problems.

At first I will write about my understanding of event bus (that can be completely wrong). We have events like this

public class FooEvent extends GwtEvent<FooHandler> {
    public static Type<FooHandler> TYPE = new Type<FooHandler>(); //as event type integer ID

    //for.. hm.. probably some inner use in Event Bus
    @Override public Type<FooHandler> getAssociatedType() {
        return TYPE;
    }

    //for handling
    @Override protected void dispatch(FooHandler handler) {
        handler.someMethod(this);
    }
}

handler interface,

public interface FooHandler extends EventHandler {
    void someMethod(FooEvent event);
}

usage

eventBus.addHandler(FooEvent.TYPE, new FooHandler() {
    @Override
    public void someMethod(FooEvent event) {
        //bla-bla
    }
});
eventBus.fireEvent(new FooEvent());

Thats it. And now my prototype.

//replaced GwtEvent
interface UniGwtEvent { 
}

//than, event pretty simple
public class FooEvent extends UniGwtEvent  {
}
//replaced GwtEventHandler. You should not create special handler class per event!
public interface UniEventHandler<T extends UniGwtEvent> {
    void handle(T event);
}
//event bus prototype(in pseudocode)
class UniEventBus {
    //map. keys getted from class. as I understand, it's possible from GWT 1.5 see http://code.google.com/p/google-web-toolkit/issues/detail?id=370 
    public <T extends UniGwtEvent> void addListener(Class<T> event, UniEventHandler<T> handler){
        map.put(event.getName(), handler);
    }
    public void fireEvent(UniGwtEvent event){
        if(map.contains(event.getClass().getName())){
            map.get(event).handle(event);
        }
    }
}

usage

eventBus.addListener(FooEvent.class, new UniEventHandler<FooEvent>(){
    @Override
    public void handle(FooEvent event) {
        bla-bla
    }
});
eventBus.fireEvent(new FooEvent());

I think this solution is much better since you shouldn't make unnecessary Type manipulation and create Handler Class per event. I see only one disadvantage - you should specify generic type on handler creation. But I suppose there are many other disadvantages or ever issues that makes this solution impossible. What are they?


Answer:

There is no obvious advantage to using your implementation. As I read it there are two differences between yours and GWT's EventBus:

  1. Using Strings instead of Type objects to bind event handlers to event types. This is not a meaningful difference - there's no penalty to having more types in your application and I suspect that, at runtime, Strings will use slightly more resources than Types.

  2. Dispatching events to the appropriate handlers directly instead of delegating to the event type. I prefer GWT's approach here because it affords flexibility in how events are dispatched. One might, for example, want handlers to implement two different methods that are invoked depending on the context of the event. Take the following (trivial) example:

    public class ExampleEvent extends GwtEvent<ExampleEvent.Handler> {
      public interface Handler extends EventHandler {
        void onExample(Integer id);
        void onExample(String name);
      }
    
      private final Integer id;
      private final String name;
    
      public ExampleEvent(Integer id) {
        this.id = id;
        this.name = null;
      }
    
      public ExampleEvent(String name) {
        this.name = name;
        this.id = null;
      }
    
      public void dispatch(Handler handler) {
        if (name != null) {
          handler.onExample(name);
        } else {
          handler.onExample(id);
        }
      }
    }
    

    In this case delegating dispatch to the event allows us to take an action that must be performed for every handler (determining whether the event contains an id or a name) without requiring that the test be performed in every individual event handler.

I recommend using GWT's EventBus implementation - it works and it is tested.

Question:

I've analyzed HandlerManager and I do not see how it handles event source. Line 117:

public void fireEvent(GwtEvent<?> event) {
    ...
    Object oldSource = event.getSource();
    event.overrideSource(source);
    try {

      // May throw an UmbrellaException.
      eventBus.fireEvent(event); // <--- LOOK HERE
    } catch (com.google.web.bindery.event.shared.UmbrellaException e) {
      throw new UmbrellaException(e.getCauses());
    } finally {
      ....
    }
}

But simple event bus implementation has following code, line 86:

@Override
public void fireEvent(Event<?> event) {
    doFire(event, null); // <---- SOURCE IS NULL???
}

@Override
public void fireEventFromSource(Event<?> event, Object source) {
    if (source == null) {
      throw new NullPointerException("Cannot fire from a null source");
    }
    doFire(event, source);
}

So, HandlerManager does not fire events with source, because it always calls doFire(event, null);

Could you make it clear how does HandlerManager fire event for source? As HandlerManager used in Widget, how does it fire events for Widget instance only?


Answer:

doFire in SimpleEventBus only changes the event's source if the source argument is not null.

HandlerManager first sets the event's source with overrideSource and then call doFire with an implicit null source so it won't overwrite it.

QED.

Question:

Background Story: I am developing a GWT application, using the standard MVP design pattern, and also using RPC to get data from my custom data handling servlet (does a lot behind the scenes). Anyway, my goal is to create a very simple custom caching mechanism, that stores the data returned from the RPC callback in a static cache POJO. (The callback also sends a custom event using the SimpleEventBus to all registered handlers.) Then when I request the data again, I'll check the cache before doing the RPC server call again. (And also send a custom event using the EventBus).

The Problem: When I send the event from the RPC callback, everything works fine. The problem is when I send the event outside the RPC callback when I just send the cached object. For some reason this event doesn't make it to my registered handler. Here is some code:

public void callServer(final Object source)
{
    if(cachedResponse != null)
    {
        System.err.println("Getting Response from Cache for: "+ source.getClass().getName());

        //Does this actually fire the event?
        eventBus.fireEventFromSource(new ResponseEvent(cachedResponse),source);

    }
    else
    {
        System.err.println("Getting Response from Server for: "+ source.getClass().getName());
        service.callServer(new AsyncCallback<String>(){

            @Override
            public void onFailure(Throwable caught) {
                System.err.println("RPC Call Failed.");
            }

            @Override
            public void onSuccess(String result) {
                cachedResponse = result;
                eventBus.fireEventFromSource(new ResponseEvent(cachedResponse),source);
            }
        });
    }
}

Now I have two Activities, HelloActivity and GoodbyeActivity (taken from: GWT MVP code) They also print out messages when the handler is called. Anyway, this is the output I get from the logs: (Not correct)

Getting Response from Cache for: com.hellomvp.client.activity.HelloActivity Response in GoodbyeActivity from: com.hellomvp.client.activity.HelloActivity Getting Response from Cache for: com.hellomvp.client.activity.GoodbyeActivity Response in HelloActivity from: com.hellomvp.client.activity.GoodbyeActivity

What I expect to get is this:

Getting Response from Cache for: com.hellomvp.client.activity.HelloActivity
Response in HelloActivity from: com.hellomvp.client.activity.HelloActivity
Getting Response from Cache for: com.hellomvp.client.activity.GoodbyeActivity
Response in GoodbyeActivity from: com.hellomvp.client.activity.GoodbyeActivity

And I will get this expected output if I change the above code to the following: (This is the entire file this time...)

package com.hellomvp.client;

import com.google.gwt.core.client.GWT;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.hellomvp.events.ResponseEvent;

public class RequestManager {

private EventBus eventBus;
private String cachedResponse;
private HelloServiceAsync service = GWT.create(HelloService.class);

public RequestManager(EventBus eventBus)
{
    this.eventBus = eventBus;
}

public void callServer(final Object source)
{
    if(cachedResponse != null)
    {
        System.err.println("Getting Response from Cache for: "+ source.getClass().getName());

        service.doNothing(new AsyncCallback<Void>(){

            @Override
            public void onFailure(Throwable caught) {
                System.err.println("RPC Call Failed.");
            }

            @Override
            public void onSuccess(Void result) {
                eventBus.fireEventFromSource(new ResponseEvent(cachedResponse),source);
            }
        });
    }
    else
    {
        System.err.println("Getting Response from Server for: "+ source.getClass().getName());
        service.callServer(new AsyncCallback<String>(){

            @Override
            public void onFailure(Throwable caught) {
                System.err.println("RPC Call Failed.");
            }

            @Override
            public void onSuccess(String result) {
                cachedResponse = result;
                eventBus.fireEventFromSource(new ResponseEvent(cachedResponse),source);
            }
        });
    }
}
}

So the point it out, the only change is that I created a new RPC call that does nothing, and send the event in its callback, with the cached data instead, and it causes the application to work as expected. So the Question: What am I doing wrong? I don't understand why 'eventBus.fireEvent(...)' Needs to be in an RPC Callback to work properly. I'm thinking this is a threading issue, but I have searched Google in vain for anything that would help.

I have an entire Eclipse project that showcases this issue that I'm having, it can be found at: Eclipse Problem Project Example

Edit: Please note that using eventBus.fireEventFromSource(...) is only being used for debugging purposes, since in my actual GWT Application I have more than one registered Handler for the events. So how do you use EventBus properly?


Answer:

If I understand your problem correctly you are expecting calls to SimpleEventBus#fireEventFromSource to be routed only to the source object. This is not the case - the event bus will always fire events to all registered handlers. In general the goal of using an EventBus is to decouple the sources of events from their handlers - basing functionality on the source of an event runs counter to this goal.

To get the behavior you want pass an AsyncCallback to your caching RPC client instead of trying to use the EventBus concept in a way other than intended. This has the added benefit of alerting the Activity in question when the RPC call fails:

public class RequestManager {
  private String cachedResponse = null;
  private HelloServiceAsync service = GWT.create(HelloService.class);

  public void callServer(final AsyncCallback<String> callback) {
    if (cachedResponse != null) {
      callback.onSuccess(cachedResponse);
    } else {
      service.callServer(new AsyncCallback<String>(){
        @Override
        public void onFailure(Throwable caught) {
          callback.onFailure(caught);
        }

        @Override
        public void onSuccess(String result) {
          cachedResponse = result;
          callback.onSuccess(cachedResponse);
        }
      });
    }
  }
}

And in the Activity:

clientFactory.getRequestManager().callServer(new AsyncCallback<String>() {
  @Override
  public void onFailure(Throwable caught) {
    // Handle failure.
  }

  @Override
  public void onSuccess(String result) {
    helloView.showResponse(result);
  }
});