Transparent Property Change Listeners in Ruby
One of my side projects is a framework for building Swing apps using JRuby. I’ve always maintained that Swing is a nice framework - it’s just that Java is a lousy language for writing it in.
One of the areas that Swing is weak in, is binding components to the underlying model. For those unfamiliar with Swing, its designed with a nice way of loosely coupling the model to the GUI components. This is done using listeners - typically Property Change Listeners - which fire whenever the value of some property changes and can be coded to explicitly replicate the state from the model to the view or (vice versa).
…Which is really nice in theory, but in practice it’s not a clean or easy task. It’s usually done badly as there is so much glue code to write if you want a nice pure OO design (developers hate writing boiler plate code). There are Java frameworks like JGoodies Binding and the PropertyChangeSupport class built into the JDK that help out, but in typical Java style, these can get quite complicated, and still require you to carefully manage new Objects to make sure they are manually bound correctly. It usually just ends up being a burden in real life which no one follows properly. (For an example of how NOT to write Swing properly - see Sun’s own Swing tutorials…)
I figured it should be possible to do this in a more Rubyish way. Something that is smooth and easy to use, and provides the best practice as the path of least resistance. I googled a bit and found RubyBeans revisited by Olivier Ansaldi, which seems cool… but involves having to subclass your bean classes from a common base class - some thing I was wanting to avoid. Ideally, the bean classes should be pure and simple Ruby classes build with attr_accessor. (what you’d call a POJO if were in Java land - although this concept is so obvious in Ruby I haven’t even come across a name for it)
Here’s what I came up with:
class ListenerSupport public def self.listen_to(klass, *properties) setup(klass) properties.each do |p| klass.class_eval do alias_method "#{p}_orig=", "#{p}=" define_method "#{p}=" do |value| old_value = send "#{p}" send "#{p}_orig=",value fire_property_changed p, old_value, value end end end end private def self.setup(klass) klass.class_eval do def fire_property_changed(prop,pre,post) return if pre == post @listeners[prop].each do |l| l.property_changed(self, prop, pre, post) end unless @listeners.nil? end def add_property_listener(prop,listener) @listeners ||= {} prop_listeners = @listeners.include?(prop) ? @listeners[prop] : [] @listeners[prop] = prop_listeners prop_listeners << listener unless prop_listeners.include? listener end end end end
The general idea is that the original class is modified so that all properties that are listened to have their accessor write methods intercepted, and any property change listeners are notified when that property changes.
Let’s walk through how it works:
- A standard Ruby class with property accessors is created. e.g.
class RubyBean attr_accessor :foo, :bar end - A call to
ListenerSupport.listen_tois made to modify the class to allow properties to be listened to.ListenerSupport.listen_to RubyBean, :foo, :bar listen_toadds methodsadd_property_listenerandfire_property_changedto allow listeners to be added toRubyBeanfor specific properties.listen_tothen aliases the original methodsfoo=andbar=tofoo_orig=andbar_orig=. New methods forfoo=andbar=are defined that delegate the work to the original method, then callfire_property_changedto notify the listeners.- Listeners are called, and any custom action (such as GUI component binding) can be carried out.
An example of this in action
class BeanListener def property_changed(obj, property, old_value, new_value) puts "#{obj.to_s} updated property:#{property}. old:#{old_value}. new:#{new_value}" end end class RubyBean attr_accessor :foo, :bar end ListenerSupport.listen_to RubyBean, :foo, :bar bean = RubyBean.new bean.add_property_listener(:foo,BeanListener.new) bean.add_property_listener(:bar,BeanListener.new) bean.foo = 'foo1' bean.foo = 'foo2' bean.bar = 'bar1' bean.bar = 'bar2' bean.bar = 'bar2'
Which outputs:
# updated property:foo. old:. new:foo1
# updated property:foo. old:foo1. new:foo2
# updated property:bar. old:. new:bar1
# updated property:bar. old:bar1. new:bar2
Which shows that the listener is being called each time the property changes
Although this doesn’t show any kind of GUI binding, it does show how easy it is to make ordinary Ruby classes (or even Javabeans in JRuby) behave as if they had property change support built in from the start. It’s an important piece of the design a smooth and easy GUI binding framework.
Update - Fixed code after feedback from Pit Capitain in comments - should have used class_eval rather than instance_eval to define new methods in setup()
February 8th, 2007 at 9:00 pm
Julian, there’s no need for define_method and eval in the setup method. If you change “instance_eval” to “class_eval”, you can simple use def to define the two methods. In any case, you can remove the calls to eval. To get rid of a warning (run your code with ruby -w) change the line “@listeners = {} if @listeners.nil?” to “@listeners ||= {}”. Regards, Pit
February 9th, 2007 at 11:33 am
Thanks for the feedback pit capitain
Changes incorporated in the original post (and prompted me to read up more on class_eval)