Event Driven Features

Using this to petition Anvil for new and glorious Event Driven Features for a decoupling of logic that will be swift, and savage. People already using these concepts don’t need to be sold on how useful they are, and hopefully I can lay out a case to making these features Anvil native.

This will enhance Components (Custom + Standard) as well as Server Code.

Vision For The UI:
In the App Browser a new section called Events is now visible. It is similar in functionality to the Client Code section, in that, it allows for hierarchical event structures to be created.

All events can have 0 or N number of paramater defined as well.

When a Component is selected there is now a new section in the Toolbox called Events. This has access to the Events listed in the App Browser. There is also a way to adding an arbitrary number of mappings between the existing events and a collection of functions that will fire on that event. The scope of available functions will be limited to the Component scope in this context. Basically a 1 to many relationship of event to functions.

Vision For Programmatic Access:
A new anvil.events module will be added with functionality, demonstrating using pseudocode:

0 Param, No Return Value, Example:
Decorator with 0 paramater events. Signature of the events match the signature of the function, else an error is indicated in the UI

@anvil.server.callable
@anvil.events.fire_on_events(['Event A', 'Event B'])
# The event list is retrieved from the new Events section, and will execute this function when the appropriate events are fired
def print_something():
  print('Im printing something')

Calling events Event A and Event B from Form code

from ._anvil_designer import TOSTemplate
from anvil import *
from anvil.events import events

class TOS(TOSTemplate):
  def __init__(self, **properties):
    events.fire_event(['Event A'])
    # print_something() function is executed on the server
    events.fire_event(['Event B'])
    # print_something() function is executed on the server again
    self.init_components(**properties)

The @anvil.events.fire_on_events(['Event A', 'Event B']) decorator can also be put on functions within forms or custom components.

1 Param, No Return Value, Example:
Decorator with 1 paramater events, firing multiple events at the same time. Signature of the events match the signature of the function, else an error is indicated in the UI

@anvil.server.callable
@anvil.events.fire_on_events(['Event C', 'Event D'])
# The event list is retrieved from the new Events section, and will execute this function when the appropriate events are fired
def print_some_param(my_param):
  print(my_param)

Calling events Event C and Event D from Form code

from ._anvil_designer import TOSTemplate
from anvil import *
from anvil.events import events

class TOS(TOSTemplate):
  def __init__(self, **properties):
    events.fire_events([  ['Event C', 'hello world']],    ['Event D', 'Hello person'] ])
    # print_some_param() function is executed on the server twice
    self.init_components(**properties)

1 Param, With Return Values, 1 Event Listener Doesn’t Respond In Timeout Period Example:
Decorator with 1 paramater events, firing multiple events at the same time. Signature of the events match the signature of the function, else an error is indicated in the UI

@anvil.server.callable
@anvil.events.fire_on_events(['Special Promotion'])
# The event list is retrieved from the new Events section, and will execute this function when the appropriate events are fired
def send_sms_to_customer(message, customer):
# do stuff
return response_code


@anvil.server.callable
@anvil.events.fire_on_events(['Special Promotion'])
# The event list is retrieved from the new Events section, and will execute this function when the appropriate events are fired
def send_email_to_customer(message, customer):
# do stuff, but it takes to long to respond. All late responses will be sent to a late responses log that is viewable later by the programmer.
  return response_code

Calling event Special Promotion from Form code

from ._anvil_designer import TOSTemplate
from anvil import *
from anvil.events import events

class TOS(TOSTemplate):
  def __init__(self, **properties):
    timeout = #define timespan that if exceeded will return only available results
    results = events.fire_events(timeout_span = timeout, [  ['Special Promotion', 'You get a deal!', 'customer 123']], )
    for result in results:
        print(result) # this will return 1 response code, that is the one from the send_sms_to_customer function 
    self.init_components(**properties)

I’m probably missing the point, but it almost sounds like you’re unfamiliar with the event capabilities Anvil already does have. For starters, in the UI, there is a section of the Properties box for every component that allows you to map a function (which could in turn call multiple other functions, if you like) to each of that component’s default events. Here’s what that looks like for a text box:

You can also add an event to any component with code using the add_event_handler method, and you can raise events from code using the raise_event method. (The only place I could find these documented together, though, is in the API documentation here.)

Are you just proposing UI/API changes, or is there some functionality missing that this stuff doesn’t provide for? Others may understand better, as I feel I don’t fully grasp the purpose of events myself, aside from the basics like “click” events.

I am aware that you can create events and event handlers dynamically, and yes, it would be possible to do the centralization of an event hierarchy manually, but I think having native support for it would be useful, and help with larger projects where autocompletion can help programmers explore the possible options.

As far as a single use case, it decouples some event A (call it the onclick event of some button in a form) from the code that needs to be called, for example the button is called “panic” and when it is clicked, you have to explicitly call server functions send_sms and send_email within the event handler.

By decorating the server function with an event called “panic” there’s no longer a need to explicitly call those functions in the event handler.

Standard Way:

  def button_panic_click(self, **event_args):
    customer = anvil.users.get_user()
    panic_message = 'its panic time'
    anvil.server.call('send_sms', panic_message, customer)
    anvil.server.call('send_email', panic_message, customer)

Event Driven Way:

  def button_panic_click(self, **event_args):
    customer = anvil.users.get_user()
    panic_message = 'its panic time'
    events.fire_event(['panic', panic_message])

map a function (which could in turn call multiple other functions, if you like)

This is what this specific bit of event driven features would automate, you wouldn’t even need a wrapper function to call, it effectively gets generated for you.

Generalizing 1 level, a function wrapper that could be used between Forms and Server code, you essentially have a collection of functions with matching signatures. You loop through this registration list, calling one at a time. If you want to add a new bit of functionality, you have to add it to the registration list.

The second level generalization, is meta programming and reflection to essentially scan the program for functions with matching decorators, and create this list for you. Hope that doesn’t confuse things too much…

1 Like

I’m still having trouble seeing the benefit, but I’m guessing it somehow allows a kind of decoupling that makes it easier to build and maintain complex apps.

I’m reminded of this related idea: Publish-Subscribe Messaging - #3 by hugetim Aside from the UI stuff, I suspect your idea could likewise be built as a custom Anvil add-on.

Just to brainstorm on one piece, I’m imagining a single server function called server_event, say, which would call other functions defined in a server module, partly to reduce the number of server round-trips:

@anvil.server.callable
def server_event(event):
    ...

@events.fire_on_events(['panic'])
def send_sms(panic_message, customer):
    ...

@events.fire_on_events(['panic'])
def send_email(panic_message, customer):
    ...

it somehow allows a kind of decoupling that makes it easier to build and maintain complex apps.

Exactly. It creates a decoupling of events from code that cares about those events, and good find as far as the existing pub sub project, must have slipped from my mind.

Exactly the same concept for this side:
publisher.publish(channel="general", title="Hello world")

The real difference is that the subscriber code is autogenerated by scanning functions with whatever the event name was as an attribute, and then adding them to the event handler.

In the documentation here:

from ._anvil_designer import MySubscribingFormTemplate
from .common import publisher


class MySubscribingForm(MySubscribingFormTemplate):

    def __init__(self, **properties):
        publisher.subscribe(
            channel="general", subscriber=self, handler=self.general_messages_handler
        )
        self.init_components(**properties)

    def general_messages_handler(self, message):
        if message.title == "Hello world":
            print(message.title)

In this existing architecture you can publish from anywhere, and catch those subscriptions from anywhere which is great.

The thing that using metaprogramming with reflection and attributes would do is, remove the need to write the message handling code at all.

For example in the standard way, you would create a ceneralized event handler like:

def general_messages_handler(self, message):
        handle_hello_world(message)
        handle_goodbye_world(message)
        # 200 more conditional handlers... jeeze what a pain...  which module was that handler in again?

With the magic fancy meta programming stuff…

@anvil.events.fire_on_events(['general'])
def handle_hello_world(self, message):
    # do stuff...

@anvil.events.fire_on_events(['general'])
def handle_goodbye_world(self, message):
    # do stuff...

# meta programming code, scans the codebase for functions decorated with 'general', and adds them in the general_messages_handler

# this function is now autogenerated and not visible to the user
def general_messages_handler(self, message):
       handle_hello_world(message)
       handle_goodbye_world(message)
        # 200 individual functions called, with appropriate function signatures, that don't need
       # anyone to register them / call them in here :)

The real usefulness comes from not having to remember which code cares about what event, and doing the explicit registration with a handler. It’s trivial with smaller code, but you get huge returns the more complex the app.

1 Like

Hmm… whether developing a new event, or debugging an old one, the first thing I’d want to know [about the event-handling sequence] is who gets called, in what order. That would seem to be a lot easier if there was an explicit list to read.

2 Likes

I agree, and since we’re dreaming, just call:
print(events.handler['my_event_name'])

The order shouldn’t be too important, as it’s essentially a “fire and forget” semantics on a collection of methods.

It depends. In some event-handling systems, event-handlers get to say whether they’ve “consumed” the event, i.e., whether or not subsequent handlers should be called. This is the “chain of responsibility” pattern at work.

Interesting! Yeah I haven’t heard of that, but I could see where it would be useful, especially if you wanted to return early on batching a bunch of work.

Yup. Sometimes you’re just notifying everyone. Other times, you’re passing a job down a line, and the first volunteer who says “I’ve got it!” gets the work.

Then, in a whiteboarding system, you may have a volunteer who says, “Well, I’ve done this much of it, and put my answers on the whiteboard. Let’s see if someone else can pick up from there.” That might mean re-raising the same event, or raising a whole new one.

1 Like

Have you seen the get_event_handlers method listed here? It returns a tuple of handlers that I imagine is in order.

2 Likes

Yes, now that you mention it. Around two years ago. If any new event-machinery is derived from this Feature Request, it would be good to include an equivalent method.

1 Like