Skip to content
MVVM

MVVM: observables, commands, binders

go-widgets/mvvm, its toolkit and terminal binders, mvvmtk's one-call helpers, and an undo stack.

go-widgets/mvvm is generics only, with no reflection. Its core package imports no widget package at all, so one view-model drives the pixel toolkit and the terminal widgets alike.

PrimitiveRole
Observable[T]a property: Get, Set, Subscribe. Setting an equal value does nothing.
Commandan action, with CanExecute and RaiseCanExecuteChanged
ObservableList[T]a collection that reports each insert, remove, replace, move and reset

A form

type FormVM struct {
	Name  *mvvm.Observable[string]
	Names *mvvm.ObservableList[string]
	Save  *mvvm.Command
}

vm := &FormVM{Name: mvvm.NewObservable(""), Names: mvvm.NewObservableList[string]()}
vm.Save = mvvm.NewCommand(
	func() { vm.Names.Append(vm.Name.Get()); vm.Name.Set("") },
	func() bool { return vm.Name.Get() != "" }, // CanExecute
)
mvvm.BindCanExecute(vm.Save, vm.Name) // Save greys out while the name is empty

Binding

The generic binders reach a widget through a pointer to its value field and a pointer to its callback slot: BindField (two-way), OneWay, BindCommand, BindList. A widget fires its callback from its event handler and leaves a direct field write silent, and Observable.Set ignores an equal value — so a two-way binding can echo without recursing.

The toolkit is moving its widgets to exposing their state as an observable (Label.Text() returns an *mvvm.Observable[string]). There is then no field to seed and no callback to compose, only two properties that must agree: BindTwoWay(src, dst, invalidate).

Widgets whose callback takes several arguments get a small named adapter in a per-back-end package, the only packages that import a back-end:

PackageAdapters
mvvm/tkbindBindRange (the two-handle RangeSlider), BindContainer and BindCardActive (a container’s children and a card layout from a list or an index), BindStore and BindTable (a Table over a data store: sorting, grouping and validated inline edits)
mvvm/tuibindBindDropdown, BindTableSelection

mvvmtk: one call per widget

go-widgets/mvvmtk fills in each toolkit widget’s real field and callback names, so the view never names them:

unbind := mvvmtk.BindText(search, vm.Query, win.Invalidate) // search.Text ⇄ vm.Query
defer unbind()
HelperDirection
BindText (SearchEntry), BindEntryText, BindChecked, BindSelectedIndex, BindSpin, BindListSelection, BindViewSwitchertwo-way
BindLabel, BindProgressview-model → widget
BindListItems, BindDropDownOptions, BindViews, BindTreelist → widget
BindCommanda button’s click, its Disabled state and its greying follow the command

Every helper returns an unbind that detaches the binding and restores the callback that was there before. A button bound to a command that cannot run takes no click, no Enter or Space, and no keyboard focus.

Undo and redo

mvvm/undo is a command stack built on the core primitives alone, so it works under any view:

s := undo.New() // unlimited, coalescing on; undo.WithLimit(n), undo.WithCoalescing(false)
s.Push(undo.NewCommand("Write hello", do, undoIt))
s.Undo()
s.Redo()

mvvm.BindCommand(s.UndoCommand(), &undoBtn.OnClick, setEnabled)
mvvm.OneWay(s.UndoTextBinding(), &undoBtn.Text, repaint) // "Undo Write hello"

A run of keystrokes coalesces into one step, and a push after an undo drops the redo tail.

Created on by tannevaled

Modified on by tannevaled