alef-1::ruby::gui_actor

Jean Lazarou
February 17, 2008

Preface

Recently, I read an article (Using the Actor pattern in GUIs) presenting the Actor pattern used in GUI development. The article applies the Actor pattern in a GUI example.

Here, I am going to present a Ruby implementation of the example. For the GUI aspects, I use the Swiby framework based on Java/Swing. Therefore, the GUI part is JRuby specific.

After a quick explanation of the pattern, I am going to present the same example as the Using the Actor pattern in GUIs article written in Ruby, then explain how I implement the Actor pattern and how I build the GUI.

The Actor pattern principle

The principles of the Actor pattern, also Active object pattern, is to call a method that delegates the actual execution to another thread.

This pattern is useful in GUI applications because GUIs, like Java/Swing, expect udpates to execute from the GUI thread. A long running task executes in its own thread, to prevent non responsive GUI behavior, and uses this pattern to update the GUI.

The example

The example shows a tea maker, making tea is a long running task. The tea maker task that needs time to heat water, notifies the registered listener when the water is ready by calling its handle_brewing_complete method. We can implement the tea maker as follows:

class TeaMaker

  attr_accessor :listener
  
  def brew()
    
    sleep 5 # the long running task
    
    @listener.handle_brewing_complete
    
  end
  
end

The user interface presents a button to start heating the water (see the brew button in the GUI, in the following figure).

The code that executes, when the user clicks on the brew button, is running in the GUI thread. It starts the brewing task in a new thread.

    button('Brew', :name => :brew) {
      
      context[:brew].enabled = false
      context[:state].text = 'Brewing started'
      
      Thread.new do
        tea_maker.brew
      end
      
    }

The button call, in the above listing, adds a button named brew, with Brew as text, to the window.

Everything between the opening and closing curly braces is the action handler code:

Next picture shows the GUI after the user clicked the brew button.

Once the water is ready, the brewing task must update the GUI. It does so by calling the registered listener:

    @listener.handle_brewing_complete

The listener updates the GUI, in its handle_brewing_complete implementation:

def f.handle_brewing_complete()
  context[:state].text = 'Brewing complete'
  context[:brew].enabled = true
end

The listener method is added to the window object stored in the f variable (more on this later), it

Next figure shows the GUI when water is ready.

Because the GUI update should not run from the brew task thread, the listener registration wraps the implementation inside an Actor object:

tea_maker.listener = Actor.new(f)

The Actor class implements the actor pattern, it delegates the execution to the GUI thread.

The Actor implementation

The code that implements the listener (brewing completion) is wrapped in an Actor instance. The actor acts as a listener but does not implement the handle_brewing_complete method. It implements the method_missing, called when a method call cannot be resolved.

  def method_missing method, *args

    @method = method
    @args = args
    
    EventQueue.invokeLater self
    
  end

It stores the missing method name (symbol) and the arguments. Then calls the static method method_missing passing it-self as argument.

The EventQueue.invokeLater method takes a Runnable object and executes (later) the run method from the GUI thread. Therefore, The actor implements the Runnable interface.

The run method, here, calls a method, using the missing name, on the wrapped object.

  def run
    @delegate.send(@method, *@args)
  end

Here is the Actor class:

class Actor
  
  include java.lang.Runnable
  
  def initialize delegate
    @delegate = delegate
  end
  
  def run
    @delegate.send(@method, *@args)
  end
  
  def method_missing method, *args

    @method = method
    @args = args
    
    EventQueue.invokeLater self
    
  end
  
end

To declare that the class implements the Runnable interface use include method (in JRuby). Creating an Actor object needs the object instance to wrap as argument (see the initialize method).

Building the GUI...

The following code is the one that builds the window:

f = form {
  
  title 'Tee Maker'
  
  width 200
  height 100
  
  content {
    label ' ', :name => :state
    button('Brew', :name => :brew) {
      
      context[:brew].enabled = false
      context[:state].text = 'Brewing started'
      
      Thread.new do
        tea_maker.brew
      end
      
    }
    button('Quit') {
      exit
    }
  }
  
  visible true
  
}

The code is easy to read. form creates a window, with the given title and size. It adds one label and two buttons. The handler for the brew button was presented above. The handler for the quit button exits the application. The code assigns the window to a variable named f.

Next, we use the f variable to extend the object with a new method, as described earlier, named handle_brewing_complete to register as a listener to the tea maker.

def f.handle_brewing_complete()
  context[:state].text = 'Brewing complete'
  context[:brew].enabled = true
end

tea_maker.listener = Actor.new(f)

Download

Here is the whole code for the tea maker.

Run the code by executing: jruby -Iswiby/lib tee_maker.rb.

To run the code you need to install the Swiby library (for the GUI part).