#1225 Unified event and data binding pods

qualidafial Tue 21 Sep 2010

I've been working on porting the data binding stack we wrote at my job to Fantom (with permission of course). It's been a huge help in wiring up our model to our GUI while avoiding copy/paste errors and such. I've summarized how the API works here.

The event pod is pretty much complete, and is what I hope we can fit into Fantom before 1.0, in order to avoid compatibility problems with FWT.

The bind pod is slowly coming together. The Property and Watcher classes are basically complete (although not well-tested) so I'm at a stage where I can move on to implementing the Bind class itself.

If anybody's interested in helping out or just taking a look, the project is on Bitbucket:

https://bitbucket.org/qualidafial/fantom-bind

Edit: remove my user ID in bitbucket URL :)

brian Tue 21 Sep 2010

We've had some good discussions on event binding.

I am going to say Andy needs to lead the effort to decide if/how event and data bindings work into Fantom. Anything we do needs to works equally well in both JVM/SWT and in the JavaScript/DOM.

My main concern right now is getting what we have now stable.

@qualidafial, can you review again what the key problems are with fwt::EventListeners and what prevents it from being future-proof for use with a binding/eventing system?

Can we just fix that to ensure that we can plug-in enhanced binding systems like yours or in a future Fantom release?

qualidafial Wed 22 Sep 2010

@qualidafial, can you review again what the key problems are with fwt::EventListeners and what prevents it from being future-proof for use with a binding/eventing system?

By itself there's nothing wrong with the current EventListeners design.

However when you look at the larger context of databinding, which includes bindable model properties as well as UI toolkits, it makes sense to try to find a common denominator to serve them all.

In this sense, the FWT design sets some precedents that are overkill for generic events like property changes.

Take for example the EventListeners slot-per-event-type pattern, e.g. Widget.onKeyDown, onMouseDown, etc. Following this pattern would be tedious in a non-UI class, and would pollute an otherwise clean API:

class Contact
{
  Str name
  Str surname
  StreetAddr streetAddr
  EmailAddr emailAddr

  once EventListeners onNameChange() { EventListeners() }
  once EventListeners onSurnameChange() { EventListeners() }
  once EventListeners onStreetAddrChange() { EventListeners() }
  once EventListeners onEmailAddrChange() { EventListeners() }
}

My solution was to consolidate the listener registration and event dispatching process into a single class event::Listeners, and to introduce an event::EventDispatcher mixin with a single listeners slot so that everything gets accessed in one place. This way your Mundane Old Fantom Object (MOFO) is minimally effected by the introduction of events:

class Contact : EventDispatcher
{
  Str name
  Str surname
  StreetAddr streetAddr
  EmailAddr emailAddr

  once Listeners listeners() { Listeners() }
}

So now the event registration procedure looks slightly different:

Text
{
  onModify.add { echo(it) }
}

Could be written instead as:

Text
{
  listeners.add(EventId.modified) { echo(it) }
}

Which is admittedly more verbose. The upside is that you now have a central switchboard which a binding library can use to register all event notifications.

Now I actually like the existing on<EventType>.add methods as it keeps the code nice and readable. So I would propose we modify the existing class to become a thin wrapper over the EventDispatcher.listeners property:

class EventListeners
{
  new make(Listeners listeners, Obj eventType) { /* init private fields */ }

  Void add(|Event| listener) { listeners.add(eventType, listener) }
  Void remove(|Event| listener) { listeners.remove(eventType, listener) }
  Void fire(Event event) { listeners.dispatch(event) }
}

class Text : Widget
{
  once EventListeners onModify() { EventListeners(listeners, EventId.modified) }
}

Aside: I think there are still some on<EventType> methods that need to be change to once methods.

I am not familiar with the FWT-JS port so Andy would have to comment on whether this design change would be a problem. On the SWT side this is actually a good fit for SWT's untyped listeners API.

Can we just fix that to ensure that we can plug-in enhanced binding systems like yours or in a future Fantom release?

The event pod in its present form is the fix I'm proposing. Everything else (including compiler magic) can come later.

There's one more thing we could do which would not be too drastic I think. If we can agree on the convention, I also propose including the Bindable facet in the event pod (it is currently in the bind pod) and start annotating our FWT fields which have events associated with them.

class Text : Widget
{
  @Bindable { eventType = EventId.modified }
  Str text := ""
}

This way if you say:

Bind.from(textControl, "text")
    .to(model, "name")

Then the binding framework can introspect the text field and determine what event type to listen for.

brian Wed 22 Sep 2010

I agree that property changes should be handled more generically. But in order to support that feature we will definitely need to figure out our compiler magic for auto-generating the hooks.

My thoughts on an event pod is that we shouldn't add that to the standard distro until we've figured out the compiler magic and overall design. Some of our brainstorming was pretty out there, so who knows what the final design might actually look like.

So my immediate concern is only that we believe we can retro-fit the current fwt::EventListeners with a new design in the future. Or maybe the problem is better expressed as "how could we make EventListeners pluggable today?".

If we solve that problem elegantly, then you should easily be able to use FWT today with your event pod and it would ensure that in the future we could either add event or something like it into the a future Fantom release.

qualidafial Sun 17 Oct 2010

Just a note to anybody interested in data binding, I've pushed some changes to the bitbucket address above.

This is a rudimentary prototype with the basic pieces working: a binding pipeline where you declare the source of the pipeline, the steps to take along the pipeline, and the final destination of the data.

The project is not well tested at this time so there are likely some rough edges. Binding from multiple sources (Bind.fromAll) is not working yet.

See BindTest.fan for a basic example of usage.

qualidafial Tue 19 Oct 2010

Update: I've been refactoring the project and cleaning things up a bit. All the steps are working now, including binding from multiple sources.

Here's a quick sample of what's possible so far.

Simple binding:

Bind.from(person, Person#firstName)
    .to(view.firstNameText, Text#text)

Validation / conversion:

Bind.from(view.ageText, Text#text)
    .validate |Str val->Bool|
    {
      Int.fromStr(val, 10, false) != null
    }
    .convert |Str val->Int|
    {
      Int.fromStr(val)
    }
    .to(person, Person#age)

Binding from multiple sources:

Bind.fromAll([
      Bind.from(person, Person#firstName)
      Bind.from(person, Person#lastName)
    ])
    .format("Welcome back {0} {1}!")
    .to(view.welcomeMessage, Label#text)

Two-way bindings:

Bind.twoWay(
    Bind.from(person, Person#age)
        .convert |Int age|
        {
          age.toStr
        },
    Bind.from(view.ageText, Text#text)
        .validate |Str val->Bool|
        {
          Int.fromStr(val, 10, false) != null
        }
        .convert |Str val->Int| { Int.fromStr(val) }
    )

Mutexes (semaphores used to make related bindings Mutually exclusive). One common use is when a data point is separated into multiple UI fields:

mutex := Mutex()

Bind.from(person, Person#pet)
    .lock(mutex)
    .convert |pet->Bool| { pet != null }
    .to(view.hasPetCheckbox, Button#selected)

Bind.from(person, [Person#pet, Pet#name])
    .lock(mutex)
    .to(view.petNameText, Text#text)

Bind.from(view.hasPetCheckBox, Button#selected)
    .to(view.petNameText, Widget#enabled)

Bind.fromAll([
      Bind.from(view.hasPetCheckBox, Button#selected),
      Bind.from(view.petNameText, Text#text)
    ])
    .lock(mutex)
    .convert |Bool hasPet, Str name->Pet?|
    {
      return hasPet ? Pet { it.name = name } : null
    }
    .to(person, Person#pet)

The above demonstrates a somewhat common UI interaction where you have some master radio button / checkbox that determines the enablement of some text control in the UI. It also shows how to combine the values from both UI elements when writing the UI state back to the model.

Most importantly, each binding locks on the same mutex, so that only one pipeline can be executing at a time. Without this mutex, it would be possible for events to daisy chain in an infinite loop: person.pet changes, firing model-to-UI binding, which updates the check box, which fires the UI to model binding, writing a new value to person.pet, which fires the model-to-UI binding again, etc until a stack overflow occurs. Using a mutex short-circuits the feedback loop so that only one binding in a group can run at a time.

We also support pausing a pipeline until some condition is met, such as clicking the "Apply" button:

person.name = "James"

applyCondition := Condition()

Bind.twoWay(
    Bind.from(person, Person#firstName)
    Bind.from(view.firstNameText, Text#text)
        .await(applyCondition)
    )

view.firstNameText.text = "Jim" // person.firstName unchanged
view.firstNameText.text = "Jimbo" // person.firstName unchanged

applyCondition.signal // person.firstName changed to "Jimbo"

Note that this is all working with respect to the library alone. However none of the above sample code will work with FWT if/until it is patched to implement the event::EventDispatcher interface, and all observable properties are annotated with a @Bindable tag.

I'm still working on getting adequate tests in place to make sure everything in the binding library works. After that I'll clone the Fantom core on bitbucket and start patching FWT with EventDispatcher and @Bindable implementations as appropriate.

Meanwhile I hope this has piqued your interest and look forward to hearing your thoughts.

brian Tue 19 Oct 2010

The "builder" API looks quite nice and readable.

What happens if the link doesn't validate? Is there a way to specify some error message?

The mutex idea is a bit tough to get your head around. Could you do something like track who initiated an event to prevent eventing looping?

qualidafial Tue 19 Oct 2010

Right now the validate step just aborts the pipeline if validation fails.

In practice I've found that with most validations you want to be able to report the validation result somewhere. However sometimes you just want to use a validator to abort the pipeline, with no feedback. Whatever solution we come up with needs to support both these use cases, without sacrificing API cleanliness.

Eclipse DataBinding does collect validation information as part of the pipeline and allows you to wire up that information into some sort of user feedback. However I'm not totally happy with the way the API turned out in Eclipse so I'm on the lookout for a better way to solve this problem. I won't be introducing anything into fantom-bind until I find a solution that serves both cases without being ugly.

The mutex thing is definitely complex. I usually have to use a whiteboard to diagram and explain the concept to my coworkers. Maybe there is another way to solve this problem but believe it or not this is the simplest solution I've found so far. I'm certainly open to suggestions.

You could possibly track the event source to prevent looping, but that might not be enough if you're binding from multiple sources. If we went this route then probably an implicit mutex at the root of the pipeline would do the job.

qualidafial Wed 20 Oct 2010

Woo hoo, email is back up.

Now everybody come read about my recent work on data binding for Fantom. :)

yachris Wed 20 Oct 2010

Hey qualidafial,

Thanks for doing this -- certainly looks interesting! It would, perhaps, be nice if the pod names you chose were a bit less generic -- the names bind and event (let alone examples!) could conflict with something else. This is kind of an ongoing problem with Fantom, IMHO... one big namespace for pod names.

Oh, and some GUI examples would be nice too.

yachris Wed 20 Oct 2010

An interesting JavaScript library that's based on data-binding (sorry if you've seen this already):

http://knockoutjs.com/

qualidafial Fri 22 Oct 2010

It would, perhaps, be nice if the pod names you chose were a bit less generic -- the names bind and event (let alone examples!) could conflict with something else.

The reason the pod names are generic is that I hope to contribute them to Fantom once they reach maturity.

The examples pod was an accident, I intended that to be a folder containing standalone .fan scripts. I'll have to go in and remove the build.fan to make sure nobody gets a rogue examples pod in their fantom base going forward... :)

qualidafial Wed 27 Oct 2010

Just a quick note that I've wrapped up my work on the event and bind pods, this last week was spent getting adequate tests in place.

I've also cloned Fantom core to Bitbucket, where I will be working on updating FWT with @Bindable facets as appropriate, as well as refactoring fwt::EventListeners into an thin adapter around the event::Listeners class.

Once that's complete, then everything should be ready to take Fantom data binding for a spin!

brian Wed 27 Oct 2010

as well as refactoring fwt::EventListeners into an thin adapter around the event::Listeners class.

I just want to re-iterate that I would like to figure out how the dependency works in reverse, or better yet have no dependency.

qualidafial Fri 5 Nov 2010

I've been reworking the properties design, and just committed some changes that will allow arbitrary listener APIs to be used with my data binding pod.

Here are the main collaborators and how they relate to eachother:

** Mixin for accessing / mutating a property
const mixin Accessor
{
  Obj? get(Obj obj)
  Void set(Obj obj, Obj? val)
}

** Wraps a generic no-arg function in a Listener capable of adding/removing
** listeners on a particular observer API.
const mixin ListenerAdapter
{
  Listener adapt(|->| delegate)
}

** An object capable of adding/removing itself as a change event listener, and
** which parlays change events to a wrapped function
mixin Listener
{
  Void addTo(Obj obj)
  Void removeFrom(Obj obj)
}

** Represent a (possibly nested) property
const mixin Property : Accessor
{
  ** Converts the input object into a Property, using one of the other from*
  ** methods depending on type.
  static Property from(Obj obj)

  ** Converts the list of objects into a nested property, in order from ascendent
  ** to descendant properties.
  static Property fromList(List list)

  ** Converts the named property (possibly nested using foo.bar.baz notation)
  ** into a property for that name.
  static Property fromStr(Str propertyName)

  ** Converts the field into a property that accesses the specified field.
  static Property fromField(Field field)

  ** Returns a property that is the result of chaining this property to the 
  ** given child property
  Property chain(Obj childProperty)

  ** Returns a Watcher for watching this property
  **
  ** @param child child watcher. These aren't the droids you're looking for.
  Watcher watcher(Watcher? child := null)
}

** Watches a (possibly nested) property chain starting from a host (root) object
** And invokes changeHandler whenever a change is detected.
class Watcher
{
  const Accessor accessor
  Listener listener
  Watcher? child

  Obj? host
  |->|? changeHandler
}

So for any client to implement their own bindable properties, they just need a proper accessor implementation (usually FieldAccessor or NamedFieldAccessor can just be reused) and an appropriate listener adapter implementation for the target object's listener API.

In the coming days I'll be checking in some properties to manipulate Fwt controls. Once I get that checked in, everybody should be able to actually try out some data binding goodness with FWT.

I do have one issue I'm looking for advice on. Right now, you can pass any object as the property to Bind.from and it is up to Property.from to resolve that into a Property object. I like the ease of this usage pattern and would prefer to keep it.

However I would also like users to be able to plug in different ways to resolve those objects into properties, so that somebody writing their own properties could plug in their own PropertyAdapter instance or something.

I was thinking of using a sys::Service for this, e.g.

const class PropertyAdapterService : Service
{

  // use actors and/or AtomicRef under the hood

  Property adapt(Obj property)

  Void addAdapter(PropertyAdapter adapter)

  Void removeAdapter(PropertyAdapter adapter)

  PropertyAdapter[] getAdapters()

  Void setAdapters(PropertyAdapter[] adapters)
}

const mixin PropertyAdapter
{
  Property? adapt(Obj property)
}

Is this the idiomatic way to do this in Fantom?

brian Fri 5 Nov 2010

Can you explain the different between Listener and ListenerAdaptor? Same for Property vs PropertyAdaptor?

What are examples of Properties other than fields?

qualidafial Sat 6 Nov 2010

Can you explain the different between Listener and ListenerAdaptor?

The idea with Listener and ListenerAdapter is to bridge the various listener APIs out there. My event::EventDispatcher API is one example, fwt::EventListeners is another.

ListenerAdapter takes in a generic listener, |->| at this point, and returns a Listener object which is responsible for creating a wrapper around that generic listener if necessary. In the case of Func type listeners it's not really necessary since a |->| will just ignore any arguments passed in.

The Listener object is also responsible for knowing how to add and remove your listener (or the listener wrapper) to the target object.

  • In the case of fwt::EventListeners, call e.g. onModify.add(listener) or onModify.remove(listener)
  • In the case of event::EventDispatcher call listeners.add(eventType, listener) or listeners.remove(eventType, listener)

Now that fact that your generic listener object might have to be adapted to fit some object's listener API means that we have to save the wrapper somewhere for when it's time to remove it as a listener.

Same for Property vs PropertyAdaptor?

Probably "adapter" is the wrong word here. We're not really adapting one object to another, we're converting some easy to express property memento into a full-blown property.

  • sys::Field gets converted into BindableFieldProperty
  • sys::Str gets converted into a NamedBindableFieldProperty
  • sys::List gets its elements converted into properties, which are then strung together to form a nested property.

The intent here is to encapsulate the construction of properties somewhere else so that all you have to give is a Str or a Field (e.g. Person#name) or a List or some other object of your choosing when setting up a binding, and those easier-to-use mementos will be converted into properties for you by PropertyAdapter (PropertyConverter?).

What are examples of Properties other than fields?

  • Nested properties, e.g. person.address.city
  • Any time you use Java objects via FFI, e.g. JavaBeans, POJOs, EMF, SWT, Swing, AWT, DOM, etc
  • Computed fields, e.g.

    class Person {

    @Bindable { eventType = "name" }
    Str firstName
    
    @Bindable { eventType = "name" }
    Str lastName
    
    @Bindable { eventType = "name" }
    Str fullName() { "$firstName $lastName" }

    }

Edit: can't get the code listing above to display properly--something about being located right after a list item.

qualidafial Fri 12 Nov 2010

I've just committed changes to include (as far as I can tell) all the widget properties you might care to bind.

I'd like to invite anybody interested to take a look at the API again and let me know what you think. See the examples folder for usage.

What's in there now is a pretty simple example, and I'm looking for ideas for a sample application to demonstrate several data binding features. If you have any ideas, please let me know.

qualidafial Tue 16 Nov 2010

I've just checked in some more changes on the data binding project.

BindingExample.fan is now an example contact list application. It's not complete but a couple core pieces are working:

  • Table model binding
  • Basic two-way bindings on several fields
  • Master-detail bindings (select a contact in the table on the left, edit the details in the form on the right)

I'm still working on getting the changes to update live back to the table. Right now the only option is to call Table.refreshAll which seems a bit heavy-handed. Would like to send updates to table in more fine-grained pieces (e.g. one row, or better one cell) but I don't see any API to do that.

Note that even though the live update to the table isn't working, the changes you make to a contact are saved immediately to the model. You can select another contact and then come back and the changes you made are still there.

That's all for tonight. Enjoy!

brian Tue 16 Nov 2010

I'm still working on getting the changes to update live back to the table. Right now the only option is to call Table.refreshAll which seems a bit heavy-handed.

If you want, please take a stab at a patch to add a refreshRows(Int[])

qualidafial Wed 17 Nov 2010

If you want, please take a stab at a patch to add a refreshRows(Int[])

Done. changeset

brian Wed 17 Nov 2010

Nice, thanks Matthew - I pushed into master repo.

qualidafial Sat 27 Nov 2010

Just a quick note: I've pushed some new changes that simplify writing data bindings in a dialog e.g. with apply/revert buttons:

applyCond := Condition()
revertCond := Condition()

Bind.twoWay(
  Bind.from(person, Person#firstName)
      .memento(revertCond),
  Bind.from(firstNameText, Text#text)
      .await(applyCond)
  )

applyButton.onAction.add { applyCond.signal }
revertButton.onAction.add { revertCond.signal }

The memento step saves the value(s) coming through the pipeline before passing those values to the next step. Whenever the condition is signaled, the latest values are sent through the pipeline again. Thus every time you click revert, the latest values from the model overwrite the text field.

The await step also saves the values coming through the pipeline. However the await step holds onto those values and does not pass them down the pipeline until the condition is signaled. Thus every time you click apply, the latest values in the text field overwrite the model.

The same conditions can be used in multiple bindings, such that an entire form can be applied/reverted by a single call to condition.signal.

Login or Signup to reply.