About data binding with custom components

A custom component with data binding between self.text_box_1.text and self.item works if self.item is a dictionary, but it doesn’t work if it’s a string

This simple app has two custom components, in one the data binding uses a dict in the other a string:
https://anvil.works/ide#clone:QLTLEDI7BCHJYX3N=QC67OJCFCVVSZLO2IZQJPLXZ

Is it possible to get self.item to work with a string?

EDIT
The problem is in the write back. After modifying the values in the text boxes, the dictionaries are updated, the strings are not.

1 Like

What is the erroneous result you see?
I might be missing something but the two strings print ok.

Oops… I forgot to mention a little detail: the write back doesn’t work. (thanks, I edited the original post)

Hmm, does seem that way.

I think because binding is fixed to “item” the writeback is hard coded to a dict type (Anvil Central will need to confirm that).

Is there a reason you don’t want to use a dict?

I want to bind textbox.text to a property of the custom component defined in the “use as component” dialog box. The property is a text, the binding works in one direction, but it doesn’t write back.

I think I found the solution. I don’t understand why it works with dictionaries and not with texts, but now I have a working custom component.

Here are the steps to get the custom component text property to play nice with the data binding:

  • In the Use as component dialog:

    • create the change event
    • create the text property
    • check the allow data binding write-back in the text property settings
    • choose the change event in the drop down
  • raise the change event when the text changes:

    def text_box_change (self, **event_args):
      self.raise_event('change', **event_args)
    
  • create both the getter and the setter for the text property:

    @property
    def text(self):
      return self.text_box.text
    
    @text.setter
    def text(self, value):
      self.text_box.text = value
2 Likes

Hi @stefano.menci,

Summary: You got to exactly the right solution in the end. To explain what you were seeing before, I ended up writing an in-depth explanation of data bindings. I’m including it here because it might be useful for others.

Just to restate the problem, as I understand it: You were using data-binding to set the item property on a custom component. Within that custom component, you were either binding self.item or self.item['foo'] to the text property of a TextBox (with writeback switched on).

How data bindings work

Data bindings are actually very simple. If you set the binding on the self.component_1's foo property to SOME_EXPRESSION, then when the refreshing_data_bindings event triggers on your form, we execute:

self.component_1.foo = SOME_EXPRESSION

(Setting self.item = [anything] on your form automatically triggers a data binding refresh; you can also do it manually with self.refresh_data_bindings().)

Writeback

If the foo property is enabled for writeback, then when self.component_1 raises an event that triggers that writeback (eg a CheckBox’s change event triggers writeback on its checked property), we execute this:

SOME_EXPRESSION = self.component_1.foo

Worked example

My form has self.item set to a row from the People table, which has a “name” column. I’ve got self.name_box's textbound to self.item['name'], with writeback enabled.

  1. I set self.item = person_row. This triggers an automatic data-binding refresh.
  2. The data binding runs: self.name_box.text = self.item['name']. The name appears in the text box.
  3. I now edit the text box, and click away.
  4. The lost_focus event triggers data binding writeback on the text property of a TextBox
  5. The data binding runs: self.item['name'] = self.name_box.text. This updates the database.

What happened in your original example

You had two data bindings. Inside your custom component, you’d bound self.text_box_1.text to self.item. (This is a little unconventional; traditionally self.item is dict-like, but it won’t cause any problems.) Then, outside your custom component, you bound self.custom_component_1.item to self.str_1.

  1. You set up your form. The data binding duly writes the value of self.str_1 to self.custom_component_1.item
  2. This triggers a data binding refresh within the custom component. It sets self.custom_component_1.text_box_1.text to self.custom_component_1.item
  3. You edit the textbox, and trigger data binding writeback
  4. The data binding runs: self.custom_component_1.item = self.custom_component_1.text_box_1.text.
  5. …it stops here. There is no automatic mechanism to update the outer form’s self.str_1.

Aside: Why did this work when you used dicts? Because if two variables refer to the same dict object, you can change it through one reference and view it through the other:

x = {'answer': 42}
y = x
y['answer'] = 43
print x['answer'] # Prints 43

So, if you’re setting a dict (or a database row) via data bindings, and you’re only updating keys in that dict (or columns in that DB), you don’t need writeback to make the new value available to outside code, because everyone is still looking at the same object. (You might still want an event so the outer form knows to update itself, but that’s a separate issue.)


As I said at the start, the solution you came to in the end is exactly the correct one. item is a built-in property for all custom components, and it doesn’t support writeback. If you want to trigger writeback on a property of a custom component, you need to make a new property, enable it for write-back, and then make an event that triggers write-back for that property. (You have done all this - you’ve made a property called text and an event called change that triggers write-back for it.)

6 Likes

Incredibly useful - can you sticky this somewhere?

Thanks @meredydd for the detailed explanation of the data binding and for confirming that my solution is correct.

I would like a clarification about the use of the text property of my custom component: I created the property in the custom component dialog and it was working with the exception of the write back. Then I created the getter and the setter and… what happened to the property previously created? Was it overwritten? Was it “augmented”?

I created the property in the custom component dialog and it was working with the exception of the write back. Then I created the getter and the setter and… what happened to the property previously created?

Custom properties, just like data bindings, are just a shorthand for standard Python assignment. If you have a custom property called foo, then init_components() will do this on startup:

self.foo = <value from designer>

If you don’t do anything else, that just sets a normal attribute on your object called foo. I’m sure you do this all the time.

But if you use @property, you’re making a descriptor that handles the foo attribute. This is a standard Python feature: now, whenever you read or write self.foo, you actually call those getter and setter functions. This is very helpful, because it lets you do things (like update the UI) whenever you read or write that attribute.


Fun digression:

Ever wondered why self.my_func(x) gets translated to def my_func(self, x)? Where does that extra argument come from? It’s because Python functions work as descriptors!

Getting self.my_func actually invokes the descriptor, which generates a shim function that adds the self value to its arguments before calling the original function. So if you do method = self.my_func, and call then method(x), it works. method is a shim function - it takes one argument, and knows what self is.

Descriptors are one of my favourite parts of Python. If you want to learn more about them, you can start here.

2 Likes

Conclusion: I need to do both, making the descriptor and defining the custom component property.

Making a descriptor with this code will ignore the attribute created by adding the property in the custom component definition:

  @property
  def text(self):
    return self.text_box.text
  
  @text.setter
  def text(self, value):
    self.text_box.text = value

But I still need to add the property in the dialog, so it shows in the list of component properties in the IDE.

Yes, in this case you need to do both. You define the custom component property so you can edit the text attribute in the Anvil designer (and data-bind it!). You make a descriptor for the text attribute (using @property), so you can read or update a TextBox whenever someone reads or writes the text attribute.

There’s no “ignoring attributes”. The custom component property always does the same thing (it assigns a value to self.text). By creating a descriptor for text, you govern what happens when that assignment runs.

Does that help explain it?

Yes, it is clear.

I used ignored because I was thinking about something like this, where text is first defined by the IDE, then is shadowed by my getter/setter.

class MyCustomComponent:
  text = None  # created by the IDE and ignored because of the following

  @property
  def text(self):
    return self.text_box.text
  
  @text.setter
  def text(self, value):
    self.text_box.text = value

Whether this is what really happens or not, it doesn’t matter. I wanted to be sure that by creating my own implementation of the text property I wouldn’t break some of the magic that happens under the hood.

Ah, yes, exactly. For what it’s worth, the standard jargon for that is “override” – we say the descriptor overrides the default attribute behaviour.

A post was split to a new topic: NoneType does not support indexing

Extremely useful info. It helped me fix the data binding write-back of my custom component.

2 Likes