Skip to content

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.

Top level interfaces

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)
}

Base Observer implementation with processing queue

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)
    }
  }
}

Type Specific Implementations

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()
}

Type Conversion wrappers

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)     
      }
    },
  }
}

Custom DataItem implementation inside user app

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)
    },
  }
}

Inside the widget lib

// 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)
}

Usage in user code / widget lib

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()
}

Problems Addressed

  • 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 ?

Bad things:

  • reflect performance ? Needs to benchmark

Cool Features and Easter Eggs

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.

Welcome to the Fyne wiki.

Project documentation

Getting involved

Platform details

Clone this wiki locally