Refresh data bindings when any key in `self.items` changes

Currently, if you have some text box bound to self.item['value'] and you have a method written like this:

def update_value():
    self.item['value'] = 'Something'

Then it won’t update the text box.

Whereas, if you write:

def update_value():
    self.item = {
        'value': 'Something
    }

Then it will correctly update the textbox - since setting self.item calls self.refresh_data_bindings().

It’d be nice if setting a key inside self.items also called self.refresh_data_bindings so we don’t have to constanty copy the dictionary, so if we have many keys, we can do this:

self.item['key'] = value

instead of this:

self.item = dict(self.item, key=value)

Which makes it less obvious what’s going on - and doesn’t look like any other dictionary updates we might have elsewhere.

2 Likes

Related: it looks like if you have a data binding to self.item['key'] and it has write-back enabled, then anything else reading that key doesn’t really work because it’s presumably using the self.item['key'] = 'value' format.

So if the issue in the first post can’t be fixed then it’d be good to change the write back code to actually change self.item so other data bindings are refreshed.

Hi there, and welcome to the forum!

This is an interesting design question, and something we thought a lot about when we created data bindings. I’ll start with the concrete advice, and then talk a bit about why we did it this way.

First things first - if you want to cause a refresh after updating self.item, the standard way is something like this:

def update_value():
  self.item['value'] = 'Something'
  self.refresh_data_bindings()

Likewise, if you want to update the rest of your form every time a text box changes, you should bind to its change or pressed_enter or lost_focus events and either do an update or call refresh_data_bindings() then. (If you have multiple TextBoxes, you can make one ‘refresh’ event handler, and set all their lost_focus events to call the same method.)

It would be nice to update everything automatically whenever you change some variable. So why didn’t we do it? Well, we decided it would be impractical, for a number of reasons:

  • There is no guarantee of what sort of object self.item really is. (It could be a dict; it could be a table row; it could be something else entirely! There’s a convention that it’s a dict-like object you can access with square brackets, but you can use it however you like. Eg if you set a RepeatingPanel’s items property to a list of strings, the item of each template instance will be a string.)

  • It’s totally normal to bind to nested expressions (eg if self.item is a table row, and that table has an author column that links to the Users table, I could bind a Label’s text to self.item['author']['email'] to display the author’s email address). If you wanted to do automatic updates, you’d somehow need to track every change to the nested object too!

  • Also, data bindings can use any Python expression - there’s no requirement to bind to self.item['something']. Eg if you want to calculate a value in a method, you can bind to self.my_method().

    (Just be aware that data bindings run when you call init_components(). So if my_method uses some internal attributes, make sure you initialise them before calling init_components().)

    (Bonus advanced hint: If you want to bind to a complicated piece of code, but you also want to write back, make a settable @property and then bind to that.)

Fundamentally, Python does not have a way of automatically finding out when an arbitrary object is modified. (You can approximate it with, eg, the Traits module, but that can be can be a bit mind-bending, even for experienced Python users. And we care about Anvil being comprehensible! Trying to be too clever about automatic updates is one of the things that makes, eg, Angular, hard to use.)

So we decided to cut with the grain of the language, and make updates (ie refresh_data_bindings()) an explicit action, rather than trying to do it automatically.

I hope that gave you a bit of insight into why we built Anvil that way :slight_smile:

4 Likes

Awesome, thanks for the detailed response! This definitely makes a lot of sense, I didn’t realise self.item could be arbitrary. The @property thing could be useful, the UI doesn’t encourage complex data bindings so I figured that wasn’t a great idea. Do you have any examples of using that to bind with write back? :slight_smile:

Definitely agree about Angular! TBH my mental model here was React, though that does require you to return a copy in the same way as setting self.item to be fair. Being explicit makes a lot of sense, and seems better than the copy approach.

I think there’s only one thing I’m a bit unclear on: could you explain how write back data bindings should be used in a simple case? Super simple motivating example: https://anvil.works/ide#clone:3Y5QJFKBKU6R6XLO=IPWIJY4FY6R53CR3P26UNIOC

Here I’ve got a text box that has a data binding with write back enabled, and a label that’s bound to the value it’s writing to. But nothing seems to happen, if I don’t add a change event to the checkbox - which feels odd. In one of the tutorials you showed writing back to the database like this, by just binding and enabling write back.

So I added a change handler to the text box, and in it I just do self.refresh_data_bindings() - but this doesn’t work either, in fact I can’t type in the text box any more. I tried printing the bound value and it’s always empty at this point - I assume that change calls before the write back, and the refresh is clearing both components.

The only thing that seems to work is to put both a write to the bound value self.item['value'] = self.text_box.text, and then the self.refresh_data_bindings() in the change handler - and then it works perfectly. But if I do that then I’m not using the data binding on the text box at all, and I’m writing manually what it seems happens automatically if I bind to eg. a database row.

In this situation, would it be recommended to just do it manually in the change event and drop the data binding completely? Or am I missing something - you mentioned “either do an update or call refresh_data_bindings() then.” but I seem to have to do both an update and the refresh to make it do the right thing in my example.

Aha - the answer here is that TextBoxes don’t perform data-binding writeback on every keystroke. (That would be a lot of database updates!) Instead, writeback occurs just before the lost_focus and pressed_enter events are fired. So by the time either of those events have been fired, self.items will be updated and you can trigger a refresh:

  def text_box_1_lost_focus (self, **event_args):
    # This method is called when the TextBox loses focus
    self.refresh_data_bindings()

Every component that supports data binding writeback for a property has specific events that trigger writeback. They’re fixed for built-in Anvil components, of course – but if you’re creating a custom component, you can choose which events trigger writeback for which properties of your component.

(PS: I’ve updated the TextBox and TextArea documentation to clarify which events trigger data binding writeback; this will hit the website tomorrow.)

3 Likes

Ain’t that the truth!!!

1 Like

Gotcha, thanks again! Sounds like a custom component is the way to go, which should be fairly straightforward for this use case. :slight_smile: