Skip to content

Data API 2: Binding Package

Andy Williams edited this page Apr 2, 2024 · 35 revisions

Note

This document is archived as it refers to a design process. The live documentation is available at https://docs.fyne.io/explore/binding

The objective of this page is to define a Data Binding API that could be used by all widgets that need to update data and also be updated whenever the data is changed;

  • Button, Check, Entry, Hyperlink, Icon, Label, ProgressBar, Radio, Select, Slider

We should also consider how it will work for these future widgets (List Proposal, Collection Widgets).

  • List, Table, Tree

Background

A UI is responsible for displaying information and providing ways to interact with the underlying data. However this data could be changed at any time by any system including the UI, in fact the data may still be loading from the file system or network.

As the data changes, the UI should be updated. Similarly, when the UI changes the data, other systems that use the data should be updated. This behaviour is called a two-way binding.

Implementing a two-way binding correctly requires the developer to write a lot of boilerplate which requires more effort to test and maintain. The goal of this project is to implement a simple and intuitive API for two-way data binding.

Previous Work

In a previous iteration (Data API) the reflection package was used to establish a binding between a widget and a piece of data, however this had some shortcoming that this new API is intended to resolve;

  • Implicity < Explicity - reflection was used to establish a binding by setting a function field in a widget and would perform type conversion behind the scenes. This opacity made it difficult for developers to understand, and harder to test and debug.
  • Single Binding Only - since reflection would lookup the function field by name, it was only possible to have one binding per widget.
  • Performance and Deadlock Susceptibility - when a binding was updated it would trigger all listeners on the same thread, and a circular dependency could cause locking issues and race conditions.

Design

This two-way data binding API encompasses the data in an wrapper that provides access to the data and will notify registered listeners whenever the data is changed.

The API is designed to work with many types of data, and many types of data structures without requiring the developer to use many type casts. Since GoLang doesn't support generics, these type-specific bindings are generated programmatically.

TODO bind to address of data rather than data itself enabling data to be updated outside of binding, and then call binding.Update to trigger.

Update Queue

When the underlying data is changed, a binding will enqueue a function into a channel. The Updater is a goroutine that iterates through the channel and executes each function, akin to the event loop. This ensures that updates are triggered in order.

TODO where will this live in the codebase, and who will start & stop it?

TODO should there be a separate update queue or should we use the event loop? On issue here is that each window has its own event loop

Architecture

The developer creates a binding that wraps a piece of data and then binds that to a widget. The widget will register itself as a listener of the binding and will get notified when the binding changed. Other systems can also register as listeners to respond appropriately to changes.

Naming

There are various parts of a binding setup, using the example widget.NewEntry().BindText(binding.NewString(mystring)) the following general naming applies:

widget.NewEntry() .BindText( binding.NewString( mystring ))
widget connection binding data
A widget is any GUI element that wishes to display the value of bound data. Connection is the action of connecting a binding to a widget, it may specify the widget property to connect to Binding is the logic of keeping bound widgets up to date with data changes Data is the underlying source of data

Notifiable Interface

Notifiable is a simple interface that represents an object that can be notified when a binding is updated.

type Notifiable interface {
	Notify(Binding)
}

Binding Interface

Binding is the base interface of the Data Binding API and defines the functions to add and remove the listeners notified when the bound data changes.

In situations where it is not possible to implement Notifiable, this interface defines AddListenerFunction to add a closure as a listener. The given function is wrapped in a new NotifyFunction which is then returned so it can be passed to DeleteListener.

type Binding interface {
	AddListener(Notifiable)
	DeleteListener(Notifiable)
}

List Interface

List encompasses a list of bindings and provides methods to get the Length of the list and Get individual elements of the list. List will notify its listeners whenever the list is changed. List will not notify its listeners when an individual element is updated, instead the UI expects child widgets within a list widget to bind to the individual elements within it.

type List interface {
	Binding
	Length() int
	Get(int) Binding
}

Map Interface

Map is similar to List but encompasses a map of string to binding and provides methods to get the Length of the map and Get individual elements of the map. Map will notify its listeners whenever the map is changed. Map will not notify its listeners when an individual element is updated, instead the UI expects child widgets within a list widget to bind to the individual elements within it.

type Map interface {
	Binding
	Length() int
	Get(string) Binding
}

NotifyFunction

NotifyFunction is a type that implements Notifiable by calling the encompassed function field, which makes it possible for a function closure to listen to a binding.

n := binding.NewNotifyFunction(func(binding.Binding) {
	// Do something
})
someBinding.AddListener(n)
...
someBinding.DeleteListener(n)

Implementation

Generated Type-Specific Bindings

Typed Bindings provide type-specific constructors, getters, setters, and listeners for a single data item.

func NewString(value string) String {...}

type String interface {
	Binding
	Get() string
	Set(string)
	AddStringListener(func(string)) *NotifyFunction
}

Supported Types

  • bool, float64, int, int64, fyne.Resource, rune, string, *url.URL

The following types could also be considered in the future:

  • byte, float32, int8, int16, int32, uint, uint8, uint16, uint32, uint64

List and Map types

The generated bindings could be updated to include support for list and map helpers, such as:

NewStringList([]string) List
NewStringMap(map[string]string) Map

These functions will make it easier to pass static data into APIs where dynamic data can be used but is not required. For example:

widget.NewSelect(binding.NewStringList("Option 1", "Option 2"))

Widgets

Each widget that wants to support bindings will have various functions of the form Bind<field>(Binding). A button, for example, may include BindText(String) and BindIcon(Resource). Each of these methods will return the widget reference, this means that binding can be included in the same line of code as the constructor function. For widgets where there is one clear primary element (such as the text of a Label, or the value of a slider) a convenience Bind(Binding) of the appropriate type should also be provided.

Impact on existing widgets

The Data Binding API extends the current Widget API to provide Bind methods for individual parts of the Widget. For example, Radio has BindOptions(List) and BindSelected(String), ProgressBar has BindMin(Float64), BindMax(Float64), BindStep(Float64), and BindValue(Float64).

TODO Discuss how this may affect widgets that take in static data (Select, Label etc) - should that move to binding or are we going to persist both types of data?

TOD How will this affect the constructors (NewSelect, NewLabel etc)? Should the convention be that each widget has two constructors, one takes no parameters, and the other takes static data? The former sets default values and is intended to have Bind calls chained to it, while the later should cover most simple (no data binding) use cases.

Examples

String mirror

This example demonstrates how to bind an Entry to a Label so that the values are always the same.

text := binding.NewString("")
entry := widget.NewEntry().BindText(text)
label := widget.NewLabel("").BindText(text)

String length

This example demonstrates how to bind an Entry to a Label so that the Label displays the number of characters in the Entry.

text := binding.NewString("")
entry := widget.NewEntry().BindText(text)

label := widget.NewLabel("0")
text.AddStringListener(func(s string) {
	label.SetText(strconv.Itoa(len(s)))
})