Data Bindings when creating a form entirely in code

I have an app with several forms that are very similar - and the few differences could easily be parameterised.

Is there some way I can create a form entirely within code so I can avoid the repetition?

I’ve tried looking at the mro of ordinary form classes and they seem to inherit from anvil.ColumnPanel

If I create a class that does the same, I can use it successfully via hash routing but it’s missing one vital capability - data binding.

If I try to add an event handler for refreshing_data_bindings, as I might on an ordinary form, that event isn’t recognised. I suppose that makes sense - you wouldn’t expect to find it on a ColumnPanel

Is there something I’m missing here? What else, other than inherit from ColumnPanel must I do in order to define a valid form class?

In the case of a design-time form, I think some of the data binding code is generated from the .yaml file. I would rather leverage this, if I could, than recreate it.

A hybrid approach might therefore be best:

  1. Define the common elements, including any common data bindings, as a conventional Form.
  2. Instantiate that Form (or a subclass) via code.

This isn’t the exact solution you’re looking for, but I beleive Layouts is the exact feature trying to address this issue. I just begun using it in some of my new projects and it has greatly reduced the repetitive UI build. I analogize them to a parent class from which my children inherit from.

Still not a pure code solution, but a step closer! I found positioning to be the biggest headache when going the full code route.

1 Like

In this case, a few of the parameters would be best as class attributes - so I’m investigating whether a code-only solution is feasible so that I can then create those classes on the fly.

If that’s not possible, I’ll just shift them to instance attributes, knock a custom component or two, bung them in a dependency and request a third party code.

For now, I have it mostly working except for data bindings and I’m wondering if that’s just me missing something or whether there’s a fundamental blocker here.

Hi @owen.campbell,

Yes, creating a form (or a component, which is the same thing as far as Anvil is concerned) in code is entirely supported! You’re doing exactly the right thing - just inherit from the base component you want to inherit from, and set up your components in your __init__.

However, you can’t create data bindings this way, because data bindings are part of the drag-and-drop designer rather than the code. In much the same way that placing components in the designer is “GUI sugar” for writing a bunch of self.add_component(...) calls in your __init__ function, data bindings are just a GUI way to do things you can do from code: namely, updating component properties from code, or updating data when component events fire.

For example, let’s imagine that I have a TextBox on my form, with its text property data-bound to self.foo, with write-back enabled. The equivalent code is something like:

class MyForm(ColumnPanel):
    def __init__(self, **properties):
        self.foo = "bar"
        self.text_box_1 = TextBox()
        self.text_box_1.add_event_listener('lost_focus', self.text_box_1_writeback)
        self.refresh_data_bindings()

    def refresh_data_bindings(self):
        self.text_box_1.text = self.foo

    def text_box_1_writeback(self, **event_args):
        self.foo = self.text_box_1.text

As you can see, there’s nothing really special about data bindings. They’re just a way of automatically generating some event handlers and a list of assignment statements to run when refresh_data_bindings() is called.

The only other piece of “sugar” is the item property that is automatically created on every Form created with the designer. In fact, the only special thing about the item property is that it calls refresh_data_bindings() every time you set it. We could do that ourselves if we wanted, with the @property decorator:

    def __init__(self):
       # ...whatever else you want to do...
       self._item = {}

    @property
    def item(self):
        return self._item

    @item.setter
    def item(self, new_value):
        self._item = new_value
        self.refresh_data_bindings()

That’s all there is! There is no man behind the curtain. Data Bindings are just a designer shortcut for code you could write yourself.

6 Likes

A thought for @yahiakalabs/@danbolinson: Perhaps I should give an Anvil User Group session sometime, on “building an Anvil app from scratch”. I could create a whole app in Notepad, then run it with the App Server, and demonstrate that everything you do with the drag-and-drop editor is something you can do from code.

Would there be demand for that, do you reckon?

14 Likes

Well, I for one would enjoy it!

1 Like

That would be a fun exercise!

It would be great to see the creator’s approach to this!