Event system introduction

The event module provides a simple system for properties and events, to let different components of an application react to each-other and to user input.

In short:

Event

An event is something that has occurred at a certain moment in time, such as the mouse being pressed down or a property changing its value. In this framework events are represented with dictionary objects that provide information about the event (such as what button was pressed, or the new value of a property). A custom Dict class is used that inherits from dict but allows attribute access, e.g. ev.button as an alternative to ev['button'].

The HasEvents class

The HasEvents class provides a base class for objects that have properties and/or emit events. E.g. a flexx.ui.Widget inherits from flexx.app.Model, which inherits from flexx.event.HasEvents.

Events are emitted using the emit() method, which accepts a name for the type of the event, and a dict: e.g. emitter.emit('mouse_down', dict(button=1, x=103, y=211)).

The HasEvents object will add two attributes to the event: source, a reference to the HasEvents object itself, and type, a string indicating the type of the event.

As a user, you generally do not need to emit events explicitly; events are automatically emitted e.g. when setting a property.

Handler

A handler is an object that can handle events. Handlers can be created using the connect decorator:

from flexx import event

class MyObject(event.HasEvents):

    @event.connect('foo')
    def handle_foo(self, *events):
        print(events)

ob = MyObject()
ob.emit('foo', dict(value=42))  # will invoke handle_foo()

This example demonstrates a few concepts. Firstly, the handler is connected via a connection-string that specifies the type of the event; in this case the handler is connected to the event-type “foo” of the object. This connection-string can also be a path, e.g. “sub.subsub.event_type”. This allows for some powerful mechanics, as discussed in the section on dynamism.

One can also see that the handler function accepts *events argument. This is because handlers can be passed zero or more events. If a handler is called manually (e.g. ob.handle_foo()) it will have zero events. When called by the event system, it will have at least 1 event. When e.g. a property is set twice, the handler function is called just once, with multiple events, in the next event loop iteration. It is up to the programmer to determine whether only one action is required, or whether all events need processing. In the latter case, just use for ev in events: ....

Another useful feature of this system is that a handler can connect to multiple events at once:

class MyObject(event.HasEvents):

    @event.connect('foo', 'bar')
    def handle_foo_and_bar(self, *events):
        print(events)

To create a handler from a normal function, use the HasEvents.connect() method:

h = event.HasEvents()

# Using a decorator
@h.connect('foo', 'bar')
def handle_func1(self, *events):
    print(events)

# Explicit notation
def handle_func2(self, *events):
    print(events)
h.connect(handle_func2, 'foo', 'bar')

Event emitters

Apart from using emit() there are certain attributes of HasEvents instances that generate events.

Properties

Settable properties can be created easiliy using the prop decorator:

class MyObject(event.HasEvents):

    @event.prop
    def foo(self, v=0):
        ''' This is a float indicating bla bla ...
        '''
        return float(v)

The function that is decorated is essentially the setter function, and should have one argument (the new value for the property), which can have a default value (representing the initial value). The function body is used to validate and normalize the provided input. In this case the input is simply cast to a float. The docstring of the function will be the docstring of the property (e.g. for Sphynx docs).

An alternative initial value for a property can be provided upon instantiation:

m = MyObject(foo=3)

Readonly

Readonly properties are created with the readonly decorator. The value of a readonly property can be set internally using the _set_prop() method:.

class MyObject(event.HasEvents):

    @event.readonly
    def foo(self, v=0):
        ''' This is a float indicating bla.
        '''
        return float(v)

    def _somewhere(self):
        self._set_prop('foo', 42)

Emitter

Emitter attributes make it easy to generate events, and function as a placeholder to document events on a class. They are created with the emitter decorator.

class MyObject(event.HasEvents):

    @event.emitter
    def mouse_down(self, js_event):
        ''' Event emitted when the mouse is pressed down.
        '''
        return dict(button=js_event.button)

Emitters can have any number of arguments and should return a dictionary, which will get emitted as an event, with the event type matching the name of the emitter.

Labels

Labels are a feature that makes it possible to infuence the order by which event handlers are called, and provide a means to disconnect specific (groups of) handlers. The label is part of the connection string: ‘foo.bar:label’.

class MyObject(event.HasEvents):

    @event.connect('foo')
    def given_foo_handler(*events):
            ...

    @event.connect('foo:aa')
    def my_foo_handler(*events):
        # This one is called first: 'aa' < 'given_f...'
        ...

When an event is emitted, the event is added to the pending events of the handlers in the order of a key, which is the label if present, and otherwise the name of the handler. Note that this does not guarantee the order in case a handler has multiple connections: a handler can be scheduled to handle its events due to another event, and a handler always handles all its pending events at once.

The label can also be used in the disconnect() method:

@h.connect('foo:mylabel')
def handle_foo(*events):
    ...

...

h.disconnect('foo:mylabel')  # don't need reference to handle_foo

Dynamism

Dynamism is a concept that allows one to connect to events for which the source can change. For the following example, assume that Node is a HasEvents subclass that has properties parent and children.

main = Node()
main.parent = Node()
main.children = Node(), Node()

@main.connect('parent.foo')
def parent_foo_handler(*events):
    ...

@main.connect('children.*.foo')
def children_foo_handler(*events):
    ...

The parent_foo_handler gets invoked when the “foo” event gets emitted on the parent of main. Similarly, the children_foo_handler gets invoked when any of the children emits its “foo” event. Note that in some cases you might also want to connect to changes of the parent or children property itself.

The event system automatically reconnects handlers when necessary. This concept makes it very easy to connect to the right events without the need for a lot of boilerplate code.

Note that the above example would also work if parent would be a regular attribute instead of a property, but the handler would not be automatically reconnected when it changed.

Patterns

This event system is quite flexible and designed to cover the needs of a variety of event/messaging mechanisms. This section discusses how this system relates to some common patterns, and how these can be implemented.

Observer pattern

The idea of the observer pattern is that observers keep track (the state of) of an object, and that object is agnostic about what it’s tracked by. For example, in a music player, instead of writing code to update the window-title inside the function that starts a song, there would be a concept of a “current song”, and the window would listen for changes to the current song to update the title when it changes.

In flexx.event, a HasEvents object keeps track of its observers (handlers) and notifies them when there are changes. In our music player example, there would be a property “current_song”, and a handler to take action when it changes.

As is common in the observer pattern, the handlers keep track of the handlers that they observe. Therefore both handlers and HasEvents objects have a dispose() method for cleaning up.

Signals and slots

The Qt GUI toolkit makes use of a mechanism called “signals and slots” as an easy way to connect different components of an application. In flexx.event signals translate to readonly properties, and slots to the handlers that connect to them.

Overloadable event handlers

In Qt, the “event system” consists of methods that handles an event, which can be overloaded in subclasses to handle an event differently. In flexx.event, handlers can similarly be re-implemented in subclasses, and these can call the original handler using super() if needed.

Publish-subscribe pattern

In pub-sub, publishers generate messages identified by a ‘topic’, and subscribers can subscribe to such topics. There can be zero or more publishers and zero or more subscribers to any topic.

In flexx.event a HasEvents object can play the role of a broker. Publishers can simply emit events. The event type represents the message topic. Subscribers are represented by handlers.