Making objects event capable - the better way

The previous section gave you a rough overview about how to use the OcempGUI event system with your own objects. As you might have seen, using only the notify() method and lengthy if-else conditions might not always be the best idea. Also, overriding the notify() method whenever the functionality of an object should change is not the best, especially, if it should be runtime dependant.

You might consider using the BaseObject from the ocempgui.object module instead. It offers ready to use signal slots and methods to bind callbacks at runtime, improving its and your flexibility without big effort.

How the BaseObject works

The BaseObject is event driven, which means, that it acts and reacts upon events it receives and that it can raise events. Hereby you have to distinguish between Signals and Events. Signals are certain identifiers, a BaseObject can listen to, while Events are a combination of a Signal and additional data.

What does that mean in practice? As you already know from the section called “Building event driven systems”, objects will register themselves at an event manager with a specific signal, they listen to. Events in turn carry a specific signal id and additional data. A BaseObject will register itself at an event manager with its signals. If events are passed to the event manager, it will distribute them to the object, if needed. The object in turn will react according to its programming instructions.

To allow your object to react according to the application needs on certain events easily, the BaseObject supports the connection of callbacks to signals you can define for it. A callback is a method or function, which should be invoked, when the object receives a certain event.

Signal handling of the BaseObject class.

Figure 2.  Signal handling of the BaseObject class


Creating event capable objects

The first thing you should do is to let your existing object or the newly created one inherit from the BaseObject class. Afterwards you can unleash its full power by adding just a minimal set of code.

from ocempgui.object import BaseObject

class OwnObject (BaseObject):
    def __init__ (self):
        BaseObject.__init__ (self)
    
    ...
        

Example 16.  Inheriting from the BaseObject class


That is not all of course. You also have to set up the signals the object has to listen to and create callbacks. Let us create a small ping-pong example, where two objects react upon a 'ping' and 'pong' signal.

The BaseObject has a _signals attribute, which basically is a dictionary with the signal ids it listens to as keys and list for the callbacks as values. To allow your object to listen to the 'ping' or 'pong' signal, you have to add those to this dictionary.

from ocempgui.object import BaseObject

class OwnObject (BaseObject):
    def __init__ (self):
        BaseObject.__init__ (self)
        self._signals["ping"] = []
        self._signals["pong"] = []
    ...
        

Example 17. Adding a signal to the object


Note

The list as value is mandatory to allow callbacks to be connected to those signals. If you are going to supply other types as value, keep in mind, that it is unlikely that connecting or disconnecting callbacks will work as supposed.

Now we just need to make the notify() aware of those signal types and let it invoke the appropriate callbacks, which will be connected to those signals.

class OwnObject (BaseObject):
    ...
    def notify (self, event):
        if event.signal == "ping":
            self.run_signal_handlers ("ping")
        elif event.signal == "pong":
            self.run_signal_handlers ("pong")
        

Example 18. Setting up the notify() method


As you see, the run_signal_handlers() takes care of invoking the connected callbacks. Now you have anything set up to allow the object to listen to specific events, to connect callbacks to it and let it invoke them, when it receives the specific signal.

To connect methods or functions as callbacks to a specific signal, the connect_signal() method of the BaseObject can be used. It allows additional data to be passed to the callback by specifiying the data right after the signal and callback. If the callbacks is not needed anymore, it can be disconnected using the disconnect_signal() method.

class OwnObject (BaseObject):
    ...

my_obj = OwnObject ()
ev_callback1 = my_obj.connect_signal ("ping", ping_callback, data)
ev_callback2 = my_obj.connect_signal ("pong", pong_callback, data1, data2)
...

my_obj.disconnect_signal (ev_callback1)
my_obj.disconnect_signal (ev_callback2)
        

Example 19. Connecting and disconnecting callbacks.


Now it just needs to be connected to an event manager. In contrast to the earlier section, you do not need to register any signal of your BaseObject inheritor manually. When you connect it to an event manager, it will automatically do that for you.

class OwnObject (BaseObject):
    ...


manager = EventManager
my_obj = OwnObject ()

# Any signal of the object will be registered automatically.
my_obj.manager = manager
        

Example 20. Connecting the object to an event manager.


The last important thing to know about the BaseObject is its ability to emit events. If the object is connected to an event manager, you can let it send events through the manager with the object its emit() method. The syntax is the same as if you would emit events on the EventManager directly.

Now let us look at the example we just went through (the following one is slightly modified only). You can find the example as python script under examples/baseobject.py

# BaseObject usage example.
from ocempgui.object import BaseObject
from ocempgui.events import EventManager

# Callbacks, which should be invoked for the object.
def ping_callback (obj, additional_data):
    print "The object is: %s" % obj.name
    print "Passed data is: %s" % additional_data

def pong_callback ():
    print "Another callback with no arguments."

# Object implementation, which can listen to specific events.
class OwnObject (BaseObject):
    def __init__ (self, name):
        BaseObject.__init__ (self)
        self.name = name
        # The object should be able to listen to 'ping' and 'pong'
        # events.
        self._signals["ping"] = []
        self._signals["pong"] = []

    def notify (self, event):
        # This simple notify method will not be used in this
        # example. Instead, the signals are invoked directly.
        if event.signal == "ping":
            self.run_signal_handlers ("ping")
        elif event.signal == "pong":
            self.run_signal_handlers ("pong")

manager = EventManager ()

# Create an object and connect callbacks to its both events.
my_obj = OwnObject ("First object")
ev1 = my_obj.connect_signal ("ping", ping_callback, my_obj, "data")
ev2 = my_obj.connect_signal ("pong", pong_callback)

# Connect it to the event manager
my_obj.manager = manager

# Invoke the connected signals handlers for a specific event.
manager.emit ("ping", None)
manager.emit ("pong", None)

# After disconnecting a callback, it will not be invoked anymore.
my_obj.disconnect_signal (ev1)
my_obj.disconnect_signal (ev2)
manager.emit ("ping", None)
manager.emit ("pong", None)

Example 21. Ping-Pong with a BaseObject


Events on demand: the ActionListener

The ocempgui.object module includes another event capable object class, the ActionListener, which inherits from the BaseObject class, but allows you to create and delete signals and listening queues as you need them without the necessity to subclass.

The ActionListener creates the signal id and a callback queue, when you connect a callback to it and registers itself for this signal automatically at its event manager. This can be extremeley useful, if a more flexible event capable object type is needed, which does not need to do any sanity checks on the event data. Instead it will send the event data to the callback as well, which then can work with it.

Once more let us create a ping-ping example using the ActionListener class instead of a BaseObject now.

You can find the example as python script under examples/actionlistener.py

# ActionListener usage example.
import sys
from ocempgui.events import EventManager
from ocempgui.object import ActionListener

count = 0

def emit_pong (event, manager):
    print "emit_pong received: [%s] - emitting pong..." % event
    manager.emit ("pong", "pong_event")

def emit_ping (event, manager):
    global count
    if count > 10:
        sys.exit ()
    count += 1
    print "emit_ping received: [%s] - emitting ping..." % event
    manager.emit ("ping", "ping_event")

# Create an event manager and two ping-pong listeners.
manager = EventManager ()

listener1 = ActionListener ()
listener1.connect_signal ("ping", emit_pong, manager)
listener1.manager = manager

listener2 = ActionListener ()
listener2.connect_signal ("pong", emit_ping, manager)
listener2.manager = manager

# start ping-pong actions
print "Starting Ping-Pong"
manager.emit ("ping", "ping_event")

Example 22. Ping-Pong with the ActionListener


Looking at the first interesting line, line eight,

def emit_pong (event, manager):
    print "emit_pong received: [%s] - emitting pong..." % event
    manager.emit ("pong", "pong_event")
        
you will notice, that the signature of the function slightly has changed. Instead of receiving the additional data of a callback connection, it gets the event data of the event here. As second object it receives an EventManager object, on which it emits a pong event with additional data then.

Line twelve and following does the same, but breaks, if it was invoked more than ten times.

The next lines of interest are line twenty-three to twenty-nine,

listener1 = ActionListener ()
listener1.connect_signal ("ping", emit_pong, manager)
listener1.manager = manager
        
in which two ActionLister objects will be created and signal slots and callbacks for the 'ping' and 'pong' events will be set up. In line twenty-five and twenty-nine the objects will be registered at the event manager created earlier in the code.

Note

Although this class is very mighty, you should not use it as base for own event capable classes. When the feature set and code amount of your own classes grow, it easily can happen, that you oversee events or that you do not understand which signals it should deal with anymore.

It is however a good and valuable class type to work as proxy or to delegate events to different functions or methods in a context sensitive manner.