-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Data API 2: Binding Package
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
- List*
- ProgressBar
- Radio
- Select
- Slider
- Table*
- Tree*
*The List, Table, and Tree widgets have not been designed / implemented yet, however an initial proposal for list is available (List Proposal).
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.
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.
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.
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.
Notifiable is a simple interface that represents an object that can be notified.
type Notifiable interface {
Notify(Binding)
}
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.
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 {
AddListenerFunction(func(Binding)) *NotifyFunction
AddListener(Notifiable)
DeleteListener(Notifiable)
}
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 is expected to bind to List as well as the individual element bindings within it.
type List interface {
Binding
Length() int
Get(int) Binding
}
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 is expected to bind to Map as well as the individual element bindings within it.
type Map interface {
Binding
Length() int
Get(string) Binding
}
Typed Bindings provide type-specific constructors, getters, setters, and listeners for a single data item.
func NewString(value string) String {...}
type String interface {
Get() string
Set(string)
AddStringListener(func(string)) *NotifyFunction
}
- Bool
- Float64
- Int
- Int64
- fyne.Resource
- Rune
- String
- *url.URL
- Byte
- Float32
- Int8, Int16, Int32
- Uint, Uint8, Uint16, Uint32, Uint64
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).
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)))
})