-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
DataAPI Developer Discussion (WIP)
SteveOC edited this page Apr 15, 2020
·
4 revisions
Thinking experimentally of a higher level abstraction, using reflect and a functional approach.
I dont expect this to compile or run .. but its useful to play with the idea.
package binding
// Value - bare with me here please
type Value reflect.Value
// Binding records an instance of a connection bewteen
// some data (type Observable)
// some widget/UI element (type Notifiable)
// the accessor methods used (type Handler)
type Binding struct {
Data Observable
Element Notifiable
Handler Handler
}
// NewBinding convenience func
func NewBinding(o Observable, e Notifiable, h Handler) *Binding {
b := &Binding{o,e,h}
o.AddListener(b)
}
// Observable is some data that can have listeners attached
type Observable interface {
AddListener(*Binding)
DeleteListener(*Binding)
Update() // fire all the listeners
}
// Handler provides Get and Set methods and
// tracks internally what type it operates with
type Handler interface {
Kind() reflect.Kind
Get() Value
Set(Value)
}
// A Bindable is anything that implements both
// Observable and Handler interfaces
type Bindable interface {
Observable
Handler
}
// Notifiable is an object that gets notified when data changes
// typically a widget - but could be any UI element
type Notifiable interface {
Notify(Binding)
}
package binding
// Observer implements a basic observable
type Observer struct {
sync.RWMutex // because locking
listeners []*Binding
}
// AddListener adds the binding
func (o Observer) AddListener(b *Binding) {
o.Lock()
defer o.Unlock()
for _,v := range o.listeners {
if v == b {
return // dont double up
}
}
o.listeners = append(o.listeners, b)
}
// DeleteListener deletes the matching binding if found
func (o Observer) DeleteListener(b *Binding) {
o.Lock()
defer o.Unlock()
for k,v := range o.listeners {
if v == b {
o.listeners = append(o.listeners[:k], slice[k+1:]...)
return
}
}
}
// Update fires all the listeners
func (o Observer) Update() {
o.RLock()
defer o.RUnlock()
for _,v := range o.listeners {
updateChan <- v
}
}
// primitive processing loop using a queue
var updateChan = make(chan Binding, 1000)
// run me in a go-routine to do all the notification processing please
func processingLoop() {
for {
select {
case b <- updateChan:
b.Element.Notify(b)
}
}
}
package binding
// String implements an observable and a string handler
type String struct {
watch *Observer
value string
}
func NewString(value string) *String {
return &String{
watch: NewObserver(),
value: value,
}
}
func (s *String) Kind() reflect.Value {
return reflect.String
}
// Get/Set pair to implement a Handler
func (s *String) Get() Value {
return reflect.ValueOf(s.value)
}
// Set/Get pair to implement a Handler
func (s *String) Set(v Value) {
s.value = v.Elem().String()
s.watch.Update()
}
func (s *String) GetString() string {
return s.value
}
func (s *String) SetString(value string) {
s.value = value
s.watch.Update()
}
// Float64
type Float64 struct {
watch *Observer
value float64
}
func NewFloat64(value float64) *Float64 {
return &Float64{
watch: NewObserver(),
value: value,
}
}
func (f *Float64) Kind() reflect.Kind {
return reflect.Float64
}
func (f *Float64) Get() Value {
return reflect.ValueOf(f.value)
}
func (f *Float64) Set(v Value) {
f.value = v.Elem().Float()
s.watch.Update()
}
func (f *Float64) GetFloat64() float64 {
return f.value
}
func (f *Float64) SetFloat64(value float64) {
f.value = value
f.watch.Update()
}
package binding
type WrapHandler struct {
Parent Handler
Kind reflect.Kind
Getter func(Value) Value
Setter func(Value) Value
}
func (w WrapHandler) Kind() reflect.Kind {
return w.Kind
}
func (w WrapHandler) Get() Value {
return w.Getter(Parent.Get())
}
func (w WrapHandler) Set(v Value) {
Parent.Set(w.Setter(v))
}
// now lets provide some pre-baked wrappers !!
// this is all generate-able in most cases
// StringHandler wraps any existing handler and
// returns a new handler that always Gets/Sets strings
// regardless of the next-in-chain type
func StringHandler(h Handler) wrapper {
return WrapHandler {
Parent: h,
Kind: reflect.String,
Getter: func(v Value) Value {
// v is anything, return string, which is easy
str := fmt.Sprintf("%v", v.Elem())
return reflect.ValueOf(str)
},
Setter: func(v Value) Value {
// v is string, convert it into the target type
switch h.Kind() {
// massive set of cases .... take a string, convert to kind
case reflect.String:
return v
case reflect.Float64:
f,_ := strconv.ParseFloat(v,64)
return reflect.ValueOf(f)
}
},
}
}
func Float64Handler(h Handler) wrapper {
return WrapHandler {
Parent: h,
Kind: reflect.String,
Getter: func(v Value) Value {
// v is anything, return float
if h.Kind() == reflect.Float64 {
return v
}
// just convert it into a string then parse that
f,_ := strconv.ParseFloat(v.Elem().String(),64)
return reflect.ValueOf(f)
},
Setter: func(v Value) Value {
// v is float, convert it into the target type
switch h.Kind() {
// massive set of cases .... take a string, convert to kind
case reflect.Float64:
return v
case reflect.String:
str := fmt.Sprintf("%.2f", v.Elem().Float())
return reflect.ValueOf(str)
}
},
}
}
package main
import (
"fyne.io/fyne/binding"
)
// 7GUI temp converter again
const KC = 273.15
var Temperature = binding.NewFloat64(0)
// Celcius is a wrapped handler that converts from/to celcius/kelvin
func Celcius(t *Temperature) binding.WrapHandler {
return binding.WrapHandler{
Parent: t.Handler,
Kind: reflect.Float64,
Getter: func(v Value) Value {
// assert v.Kind is float64, which we know is true
c := v.Float()-KC
return reflect.ValueOf(c)
},
Setter: func(v Value) Value {
k := v.Float()+KC
return reflect.ValueOf(k)
},
}
}
// Farenheit is a wrapped handler that converts from/to farenheit/kelvin
func Farenheit(t *Temperature) binding.WrapHandler {
return binding.WrapHandler{
Parent: t.Handler,
Kind: reflect.Float64,
Getter: func(v Value) Value {
// assert v.Kind is float64, which we know is true
f := ((v.Float() - KC) * 1.8) + 32.0
return reflect.ValueOf(f)
},
Setter: func(v Value) Value {
k := (v.Float() - 32.0) / 1.8 - 32.0
return reflect.ValueOf(k)
},
}
}
// Bind creates a new Binding between this widget
// and the Bindable passed in.
// The Bindable's base Handler is applied, but can
// be customised using the widget.Handler() call below
func (e *Entry) Bind(value binding.Bindable) *Entry {
b := binding.NewBinding(value, e, value)
if b.Handler.Kind() != reflect.String {
b.Handler = binding.StringHandler(h.Handler)
}
e.binding = b
return e
}
// Handler (optionally) sets the handler for this binding
func (e *Entry) Handler(h binding.Handler) *Entry {
if e.binding != nil {
if h.Kind() != reflect.String {
h = binding.StringHandler(h)
}
e.binding.Handler = h
}
return e
}
// Unbind disconnects the widget and puts the binding out for garbage collection
func (e *Entry) Unbind() {
if e.binding != nil {
e.Data.DeleteListener(e.binding)
e.binding = nil
}
}
func (e *Entry) Notify(b binding.Binding) {
if e == nil {
// is actually possible, so trap it here
return
}
// we can get the data from the binding
// we know that the handler always returns a string value
value := b.Handler.Get().String()
e.setText(value)
}
func (e *Entry) setText(value string) {
// ... do all the things here to set the text in the widget
// but NOT the code to update the bound data
}
func (e *Entry) SetText(value string) {
if e.value == value {
return // nothing has changed
}
e.setText(value)
b.binding.Handler.SetString(value)
}
package main
import (
"fyne.io/fyne/app"
"fyne.io/fyne/binding"
"fyne.io/fyne/widget"
)
func main() {
a := app.New()
temperature := NewTemperature()
w := a.NewWindow("Temp Converter")
w.SetContent(widget.NewVBox(
widget.NewLabel("Kelvin"),
widget.NewEntry().Bind(temperature),
widget.NewLabel("Celcius"),
widget.NewEntry().Bind(temperature).Handler(Celcius),
widget.NewLabel("Farenheit"),
widget.NewEntry().Bind(temperature).Handler(Farenheit),
widget.NewLabel("Temperature Slider"),
widget.NewSlider(0,10000).Bind(temperature),
))
w.ShowAndRun()
}
- TODO, fill this in
- Widgets can implement as many bindings as they need (ie - slider with minBinding, maxBinding, etc)
- The
Binding
tuple of {Observable, Notifiable, Handler} ends up solving the relational issues, so each connection between a widget and a data source can (optionally) override the handler. - The "middleware" using Handler wrappers is closer to being idiomatic go. (still not right yet, but closer)
- Replacing the synchronous updater (with all its circular reference problems) with a queue looks pretty staightforward, and fixes all the circular dependency issues
- Passing reflect.Value around simplifies the way different things can be connected together ?
- reflect performance ? Needs to benchmark
if most of the internals of databinding took interfaces in place of widgets, it becomes possible to do some new things hadnt considered before
- The thing that you Bind() to doesnt have to be a widget. So you could bind some data (like a struct of Customer Details) to a layout.formLayout() maybe.