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.
HasEventsclass provides objects that have properties and can emit events.
- There are three decorators to create
- There is a decorator to
connecta method to an 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 old and new value of a property). A custom
class is used that inherits from
dict but allows attribute access,
ev.button as an alternative to
The HasEvents class¶
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
Events are emitted using the
method, which accepts a name for the type of the event, and optionally a dict,
emitter.emit('mouse_down', dict(button=1, x=103, y=211)).
The HasEvents object will add two attributes to the event:
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.
A handler is an object that can handle events. Handlers can be created
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
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,
for ev in events: ....
In most cases, you will connect to events that are known beforehand, like those they correspond to properties, readonlies and emitters. If you connect to an event that is not known (as in the example above) it might be a typo and Flexx will display a warning. Use ‘!foo’ as a connection string (i.e. prepend an exclamation mark) to suppress such warnings.
Another useful feature of the event 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
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')
Apart from using
emit() there are
certain attributes of
HasEvents instances that generate events.
Settable properties can be created easiliy using the
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)
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 attributes make it easy to generate events, and function as a
placeholder to document events on a class. They are created with the
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.
Connection string syntax¶
The strings used to connect events follow a few simple syntax rules:
- Connection strings consist of parts separated by dots, thus forming a path. If an element on the path is a property, the connection will automatically reset when that property changes (a.k.a. dynamism, more on this below).
- Each part can end with one star (‘*’), indicating that the part is a list and that a connection should be made for each item in the list.
- With two stars, the connection is made recursively, e.g. “children**” connects to “children” and the children’s children, etc.
- Stripped of ‘*’, each part must be a valid identifier (ASCII).
- The total string optionally has a label suffix separated by a colon. The label itself may consist of any chars.
- The string can have a ”!” at the very start to suppress warnings for connections to event types that Flexx is not aware of at initialization time (i.e. not corresponding to a property or emitter).
An extreme example could be
"!foo.children**.text:mylabel", which connects
to the “text” event of the children (and their children, and their children’s
children etc.) of the
foo attribute. The ”!” is common in cases like
this to suppress warnings if not all children have a
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
@h.connect('foo:mylabel') def handle_foo(*events): ... ... h.disconnect('foo:mylabel') # don't need reference to handle_foo
Dynamism is a concept that allows one to connect to events for which
the source can change. For the following example, assume that
HasEvents subclass that has properties
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): ...
parent_foo_handler gets invoked when the “foo” event gets
emitted on the parent of main. Similarly, the
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
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.
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.
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.
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
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.
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.
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.