Disabling user interaction while server callable runs

I’d like to disable user interactions while anvil.server.call() functions run. In other words, I don’t want users to be able to interact with anything at all while the blue spinner is present. Right now, the blue spinner appears, but it is still possible for a user to interact with buttons on the page.

This is a problem because these buttons represent active_filters which are passed as a list to an Uplink server function that queries my backend database. The active_filters buttons’ click events are set to remove the button (thereby removing the filter from active_filters).

When the server function completes, part of its response validates which active_filters may remain so its important the list (i.e., the buttons) remains intact while the database query runs. If a user is able to click one of the buttons before the server call has completed, it’s effectively corrupting the list of active_filters.

How do I suspend interactions pending completion (or timeout) of an anvil.server.call() invocation?

1 Like

One way could be to show an alert box (non-dismissable) and call the server function from there, closing it and returning the value to the parent form upon completion.

So, create a new form called “blocking_server” with a label saying “Please wait…”.
Then in your main form, fire up the alert box like this (untested and from memory) :

from blocking_server import blocking_server
...
result = alert(blocking_server(params_to_pass_to_server),title = "Running Server Code", dismissable = False, buttons = [])

Then in that form call the server function and close the alert box like this :

results = anvil.server.call(...)
self.raise_event("x-close-alert", value=results)

Does that help?

1 Like

@david.wylie Yes, this helps a lot. This is exactly the kind of hacky-yet-sensible solution I was looking for. The best I’d come up with on my own was a hacky-and-terrifyingly-fragile button enabler/disabler situation. I can take it from here - thanks very much!

@david.wylie is this the same kind of thing you could accomplish using python context manager (i.e., “with” statement)? Example,

with Notification(title='please wait while I run a server function...'):
  anvil.server.call('my_server_func')

ooo…I do keep forgetting about that one.

@kevin - def. give @alcampopiano’s idea a go. If it works it’s much neater.

@david.wylie will do, thank you. Will the Notification() in @alcampopiano’s with block disable other interactions the same way an Alert() would?

Not sure actually, you’d have to give it a go.

@kevin Yes I believe so as long as you use an Alert rather than a Notification, and set it so it cannot be dismissed. For example:

with alert(title='please wait...', buttons=[], dismissible=False):
      anvil.server.call('my_server_func')

@alcampopiano - Got it. I’ll give it a go and will report back when I have it working.

1 Like

As promised, here’s the update.

@david.wylie’s first recommendation (raising an alert and processing the server call from there) appears to work as expected. That said, it introduces a good amount of complexity, and (unfortunately for me) I’d need to do quite a bit of refactoring to deploy it successfully in my app.

@alcampopiano’s idea didn’t work out. The with context works great with Notifications, but it doesn’t disable other interactions while inside the with block. Users were still able to click buttons visible in the background of the notification. Using an alert in a with context didn’t work at all - once the alert is called, further processing seems to be suspended until the alert is dismissed. So, while it did raise an alert and prevent interaction, it didn’t allow the server call to proceed.

I also took a shot in the dark and tried something like this, but I couldn’t get it working:

with alert(content=content_blocking_form()):
    anvil.server.call('foo_bar')
    self.raise_event('x-close-alert', value=None)

After all that, I wound up skipping both notifications and alerts, and instead I recursively disable/enable all buttons from a MainForm method (I have a multi-page app with shared nav that’s owned by a form called MainForm).

The MainForm method looks like this:

def recursively_toggle_buttons(self, component, enabled):
  if isinstance(component, Button):
    component.enabled = enabled
  elif isinstance(component, (self.__class__, ColumnPanel, FlowPanel, DataGrid, DataRowPanel, LinearPanel, XYPanel, GridPanel)):
    for cmp in component.get_components():
      self.recursively_toggle_buttons(cmp, enabled)

And then my interaction handler contains something like this:

get_open_form().recursively_toggle_buttons(
  get_open_form(),
  enabled=False
)
anvil.server.call('foo_bar', active_flags=active_flags_list)
get_open_form().recursively_toggle_buttons(
  get_open_form(),
  enabled=True
)

This has the effect of disabling all buttons while the server call executes and then re-enabling them all when it’s done. It’s not ideal since I may need some custom handling for specific buttons in the future, but it does the trick for now.

I think there’s a feature request in here: a flag to disable user interactions while inside a with Notification(): context. Thoughts?

Further ideas/improvements welcome. Thanks again for your help.

1 Like

Glad you got something working, though I would take issue with your solution being less complex than mine :slight_smile:

If I think if anything else I’ll post.

Here’s an attempt from a different angle.

This just sets everything to invisible while the server function runs. It is a bit of an odd solution, but it does prevent interaction in the content panel while the server runs.

with Notification('', title='please wait...'):
  self.content_panel.visible=False      
  anvil.server.call('my_server_func')
  self.content_panel.visible=True

2 Likes

@david.wylie - hah, fair point! :joy:

@alcampopiano - that’s a clever workaround, good idea. I must admit it’s quite a bit simpler than using recursion to toggle all buttons. I’d still need to manage nav buttons, but that’s easy enough. I’ll give it a shot and ask some users for feedback. Thanks!

I saw your alert updates as well, and I got stuck on the same problem - seems like everything stops until the alert is dismissed. So if the alert can’t be dismissed everything stops permanently. :slight_smile:

1 Like

Just an aside:

You probably know this already, but you can get rid of the blue spinner with anvil.server.call_s('server_func').

I thought I would mention that since having both the notification and blue spinner up at the same time may be visually unappealing, and the nice thing about the with statement is that the notification will close automatically when things are finished.

1 Like

Thanks, @alcampopiano! I caught that in the docs and tried it out. Turns out people really like watching spinners spin, so I had to put it back in for now.

As an aside, I was hoping I could make it cleaner by using .call_s() and a notification containing a Form instance with its own custom spinner. Something like this:

from Loader_Notification import Loader_Notification
...
with Notification(Loader_Notification()):
    anvil.server.call_s('server_func')

But sadly Forms as content only work for alerts, notifications are text-only. Maybe someday.

:+1: