Skip to content

Using Guava’s EventBus To Decouple Components of a Swing Application

December 20, 2011

I had a dilemma.

I had two separate Java command-line utilities to parse astronomical catalogs and convert them to JSON for use in MongoDB. One would read the files available from NASA’s HEASARC archive (in a weird format called TDAT), and one would read the DAT files from the Vizier archive.

The problems:

1. The utilities, although they were both under the same Maven parent POM, were still in different directories. I’d have to switch back and forth between the directories, which got annoying.

2. The configuration for both utilities used XML, but the schemas were slightly different. Also, one used SAX and one used DOM.

So, I decided that a frontend would be necessary. It was time to introduce myself to Swing. Unfortunately, Swing is a library that would make a Sith Lord say “dammmnnnn… that’s EVIL.”

The catalog parsing app was based on a BorderLayout with 4 major components: a menubar, a tree control, an editor control, and a statusbar control. Each of these were contained in a JPanel. Okay, in reality, they were classes that extended JPanel:

  • MenuBarPanel.java – Menubar for the application
  • TreePanel.java – display catalogs available for export
  • EditorPanel.java – display the first 20 lines or so of generated JSON
  • StatusBar.java – a statusbar to show what was going on with the app
  • ApplicationPanel.java – Panel containing a BorderLayout(), which held each of the other Panel implementations
  • ApplicationFrame.java – Class extending JFrame, which contained the ApplicationPanel. Probably not strictly necessary, but cleaner.

There was also a PopupMenu that was meant to be attached to the TreePanel:

  • CatalogPopupMenu.java – right-click context menu, extends JPopupMenu

I quickly learned that although each Component was separated into its own Panel, each tended to get tangled up with each other. Especially the TreePanel and the CatalogPopupMenu. Oh my GOD, did those two get tangled up.

Normally, the components have Listeners assigned to them to listen for Events. For example, TreePanel.java would have something like this:

DefaultMutableTreeNode topNode = new DefaultMutableTreeNode("Catalogs");
// add nodes to the topNode through repeated create/add logic
JTree tree = new JTree(topNode);
tree.addMouseListener(new MouseAdapter() {
    public void mousePressed(MouseEvent e) {
        popupMenu.show(e.getComponent(),e.getX(),e.getY());
    }

    public void mouseReleased(MouseEvent e) {
        popupMenu.show(e.getComponent(),e.getX(),e.getY());
    }
});

Then, in order to figure out what was going on in TreePanel, CatalogPopupMenu would have something like this:

// somehow we needed to have an existing reference to the tree control already; this was a pain to get
JMenuItem menuItem = new JMenuItem("Export to JSON");
menuItem.addActionListener(new AbstractAction() {
    public void actionPerformed(ActionEvent e) {
        String selectedText = (String) ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
            JsonExporter jsonExporter = new JsonExporter().setCatalogName(selectedText); // JsonExporter uses a Builder Pattern; method returns JsonExporter
        }
    });
}

(To be honest, the logic in the actionPerformed() method was much worse, I trimmed it down quite a bit here.)

We now had two tangled classes where the listeners referenced the other class. This didn’t go well with the concept of “Loose Coupling, High Cohesion”. More to the point, the code was ugly and difficult to maintain.

Enter Guava 10.

I’m a fan of Guava because of its ImmutableMap and BiMap implementations. I was quite happy to find out that as of the 10.X release, it now contained a simple EventBus implementation. (For those of you that aren’t overly familiar with that, EventBus is an implementation of the GoF observer pattern, allowing classes to fire/post events and listen for those events to actually react to them).

First, I used Guice (in the words of Jar-Jar Binks: dependency injection issa NICE) to create a SingtonEventBus for the application:

public class CatalogModule extends AbstractModule {

    @Override
    protected configure() {
        bind(EventBus.class).in(Singleton.class);
    }

    @Provides
    ConfigMap provideConfig() {
        ConfigMap config = new ConfigMap();
        config.putAll(new ConfigParser("config.xml").getConfig());
        return config;
    }
}

(Some notes: The EventBus had to be a singleton or various instances would have been created, which may or may not have created listeners. Also, the provideConfig() method was necessary as I had a ConfigMap class, which was a simple extension of HashMap, that contained the data from the XML config file)

I then went back and added the @Singleton annotation to each of the classes involved in the GUI layout (ApplicationFrame, ApplicationPanel, MenuBarPanel, TreePanel, CatalogPopupMenu, etc).

I also needed to inject the Panels via a constructor. Guice claims that it supports method injection, but it’s never worked all that well for me and I didn’t feel like fighting with it. Here’s the relevant portion of the ApplicationPanel.java file:

@Singleton
public class ApplicationPanel extends JPanel {

    private EventBus eventBus;

    private EditorPanel editorPanel;
    private MenuPanel menuPanel;
    private StatusBarPanel statusBarPanel;
    private TreePanel treePanel;

    private CatalogPopupMenu popupMenu;

    public ApplicationPanel() {
        super(new BorderLayout());

        init();
    }

    @Inject
    public ApplicationPanel(EventBus eventBus,
                            EditorPanel editorPanel,
                            MenuPanel menuPanel,
                            TreePanel treePanel,
                            StatusBarPanel statusBarPanel,
                            CatalogPopupMenu popupMenu) {
        super(new BorderLayout());

        this.eventBus = eventBus;

        this.editorPanel = editorPanel;
        this.menuPanel = menuPanel;
        this.treePanel = treePanel;
        this.statusBarPanel = statusBarPanel;
        this.popupMenu = popupMenu;

        eventBus.register(this.editorPanel);
        eventBus.register(this.popupMenu);

        init();
    }

(I’ll explain the register() methods in a bit).

My next step was setting up the event handling. Guice requires that a handler method be created to manage the posted event. This handler method must be annotated with the @Subscribe annotation, and must contain ONLY ONE argument. (Somehow I’m reminded of the Holy Hand Grenade of Antioch. But anyways…)

I also created some basic Event classes. For handling the right-click on the JTree to show the context menu, I created an event named ShowContextPopupEvent.

public class ShowContextPopupEvent {

    private Component component;
    private int x;
    private int y;

    public ShowContextPopupEvent(Component component, int x, int y) {
        this.component = component;
        this.x = x;
        this.y = y;
    }

    public Component getComponent() {
        return component;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }
}

There isn’t much here… the class simply takes the component, x position, and y position of the click event.

Next, I registered a handler event in CatalogPopupEvent.java:

    @Subscribe
    public void handlePopupEvent(ShowContextPopupEvent e) {
        this.show(e.getComponent(), e.getX(), e.getY());
    }

Now, the MouseAdapter in the TreePanel.java class looks like this:

        @Override
        public void mousePressed(MouseEvent e) {
            doPopup(e);
        }

        @Override
        public void mouseReleased(MouseEvent e) {

            doPopup(e);

        }

        public void doPopup(MouseEvent e) {
            ShowContextPopupEvent event = new ShowContextPopupEvent(e.getComponent(), e.getX(), e.getY());
            eventBus.post(event);
        }

The event is fired/posted to the eventbus via the eventBus.post() method. However, we aren’t done yet… one more step.

And now the final step: registering the handler. You may have noticed the eventBus.register() methods in earlier code. You may have also noticed me stating I’d explain them later. This is later.

Each class defining an event handler method must register with the event bus.

These lines were in the constructor of the ApplicationPanel.java class:

eventBus.register(this.editorPanel);
eventBus.register(this.popupMenu);

This causes the event bus to actually sit and notice that holy guacamole, there are methods in those two classes annotated with @Subscribe!

And there you have it. A Swing application with limited coupling, communicating between JPanels via events passed through an EventBus. Drawback: the code is a bit heavier, both for the Guice implementation and event classes. Still, it’s a tradeoff I’m relatively okay with making given the choice between that and garbage code.

About these ads

From → Uncategorized

4 Comments
  1. artfullyContrived permalink

    Nice read.

    Should the part

    “Guice requires that a method be created and annotated with the @Subscribe annotation.”

    read

    “The event bus requires that a subscriber method be created and annotated with the @Subscribe annotation.”

    • I did a bit of updating to that section. I prefer the term “handler method” to “subscriber method”, but hopefully it’s a bit more clear now.

  2. Jose permalink

    Hi, I´m just learning event bus, is there anyway I can get the sourcecode??, that would really help me thanks

    • I’ll have to see if I still have the source code. Its been a few months since I wrote this…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: