#1123 Event pod and bindability

qualidafial Sat 19 Jun 2010

I wanted to revive the discussion in #589 and see if we can't make some headway on a unified event dispatching API.

For the past several months I've been working in Flex. I will pause for a moment while you pity me.

Actually it's not all that bad, but Flex programming definitely has its share of WTF moments. But I digress.

One thing I've come to appreciate is how easy it is to create bindable properties and methods in Flex:

class Person
{
  [Bindable]
  public var name:String;
}

var p:Person = new Person;
p.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE,
  function(event:PropertyChangeEvent):void
  {
    trace("name changed: "+event.target.name);
  } );

p.name = "Matthew"; // => name changed: Matthew

By adding that [Bindable] tag the compiler automatically injects some code into the Person class for you. Namely, your class now implements the IEventDispatcher interface, with default implementations for all interface methods. Also, the name property is transparently enhanced to:

private var _name:String; // underscore is flex convention..

public function get name():String { return _name; }

public function set name(value:String):void
{
  var oldValue:String = _name;
  _name=value;

  dispatchEvent(PropertyChangeEvent.createUpdateEvent(
      this, "name", oldValue, value));
}

Moreover you can override the autogenerated code by providing your own method body, to get more fine-grained control over the events.

You can also use methods (not just fields) as binding sources, e.g. the built-in ArrayCollection class:

[Bindable("collectionChanged")]
public function getItemAt(index:int):Object {
  ...
}

public function addItemAt(item:Object, index:int) {
  ...
  dispatchEvent(new Event("collectionChanged"));
}

It is difficult to overstate how many things become possible and even trivial when event notification is woven into the fabric of the language.

By way of counterexample: I've been a member of the Eclipse Data Binding project for a few years. One of the larger undertakings I've been involved in was a universal property API, which in a nutshell encapsulates the getter, setter and listener API of a particular property. This is necessary because there are so many different types of getter/setter and listener APIs in Java:

  • Beans - basically POJOs plus property change notification, e.g. addPropertyChangeListener(String propertyName, PropertyChangeListener)
  • AWT/Swing - event notification e.g. addMouseListener(MouseListener)
  • SWT - which has listener APIs similar to AWT, as well as "untyped" listeners e.g. addListener(int eventType, Listener)

I literally spent 6 months of my free time writing an API that was capable of bridging between all these getter/setter and listener patterns. What a waste of time, simply because the Java language/API lacks a decent standard.

In Flex, I saved myself six months of workarounds simply because the language has set a standard for everyone to follow. Thus in the six months I've been at my job, I've written a full-on data binding builder library in a fraction of the time and code size that it took in Java. Now I can do cool stuff like this:

Bind.twoWay(
    Bind.from(model, 'order.customer.address.street'),
    Bind.from(street1Input, 'text')
  );

or this:

Bind.from(model, 'person.age')
    .convert(Converters.intToString)
    .to(ageInput, 'text');

Bind.from(ageInput, 'text')
    .validate(Validators.stringToInt)
    .convert(Converters.stringToInt)
    .validate(Validators.greaterThan(0))
    .to(model, 'person.age');

We don't have to do everything before 1.0. (although that would be really nice!)

I do think it's critical though to take a few first steps to formalize a basic event model, so that we have something common for everything to build on going forward.

I propose we start by introducing an event pod:

class Event
{
  const Obj type; // <-- this slot now available!
  Obj source;

  new make(Obj type, Obj source)
  {
    ...
  }
}

mixin EventDispatcher
{
  Void addListener(Obj type, |Event| listener)
  Void removeListener(Obj type, |Event| listener);
}

**
** Helper implementation similar in spirit to PropertyChangeSupport in Java
**
class Listeners : EventDispatcher
{
  Void notifyListeners(Event);
}

Note that the event type is an Obj. This will allow APIs to use whatever event type they want, be it an enum, a Slot instance, or whatever. My guess is that enums will be used for pure events e.g. mouse clicks, whereas a Slot instance e.g. #name will be used for field change events.

The next step would be to introduce a @Bindable facet, possibly in a separate bind pod:

facet class Bindable
{
  const Obj type;
}

We can also add a FieldEvent class specifically for field value changes:

class FieldEvent : Event
{
  Field field;
  Obj? oldValue;
  Obj? newValue;

  new make(Obj type, Obj source, Field type, Obj? oldValue, Obj? newValue)
      : super.make(type, source)
  {
    ...
  };
}

Notice how there's no compiler magic yet?

At this point I can go crazy and implement a full data binding library for Fantom.

Those who want their code to be bindable can hard code the events:

class Person : Listeners // antipattern! -- extending Listeners for brevity only
{
  @Bindable(type=#name)
  public String name := ""
  {
    get
    set
    {
      notifyListeners(FieldEvent.make(#name, this, #name, &name, &name=it));
    }
  }
}

Later when we are ready for some real compiler magic, we can go the rest of the way and wire things up, so that attaching a @Bindable tag to a slot (or the class itself) mixes in the EventDispatcher interface if it isn't already, and generates a notifyListeners call wherever a setter block is not explicitly provided.

Many of you will no doubt have noticed that EventDispatcher differs from the API currently in FWT.

The current design in FWT works well for events. The problem happens when you extend the same pattern to fields, you end up with either:

  • A doubling of class size, since now you have to declare an on<FieldName>Change for every bindable field; or
  • You declare a single onChange and fire change events for all fields through that. However this has only pushed the problem to the client side, where listeners have to discriminate based on FieldEvent.field or some other criteria.

I hope I've given you guys a taste of how what a killer feature this could become for Fantom. Looking forward to your responses.

brian Mon 21 Jun 2010

I am all for a standardized event and property change model. On the other hand I don't want to spent efforts on new features until we wrap up what we already have. My main concern with trying to rush this feature is that during previous discussions we've had some really unique ideas for how to make fields/method more first class concepts to weave in behavior such automatic property eventing. I'd really like to have lots of time to explore those solutions because I believe they are really promising.

The flip side of the discusion as you point out is that we might potentially get stuck with the fwt EventListeners design and it might not be able to take advantage of whatever new design we dream up. That is definitely a valid concern, but I'm not sure that one class should force our hand.

I'd definitely be interested in others thoughts.

qualidafial Mon 21 Jun 2010

Would it help if I cooked up a prototype event pod?

brian Mon 21 Jun 2010

My thoughts on this issue is that this isn't about getting the code written, but rather figuring out the right abstractions and design. For example before we really dived into a first class eventing system these are the sorts of things I'd like to explore:

  • #706: making field/methods more OO to allow mixing in other classes on a per instance basis (this was a really interesting discussion)
  • I am also very interested some of the ideas Micrsoft is doing with LINC to events or making LINC reactive

qualidafial Tue 22 Jun 2010

`#706`: making field/methods more OO to allow mixing in other classes on a per
instance basis (this was a really interesting discussion)

Interesting. There was another discussion about instrumenting slots using facets, but I came away with the impression that brian and andy were opposed to it. I'm glad to hear it's still on the table.

I'm curious how the proposals there jive with #668--making java annotations and Fantom facets play nice, while simultaneously introducing instance-scope facets seems contradictory, unless I'm missing something.

brian Tue 22 Jun 2010

I'm curious how the proposals there jive with #668--making java annotations and Fantom facets play nice, while simultaneously introducing instance-scope facets seems contradictory, unless I'm missing something.

Of the two, definitely 668 is something I plan on doing soon as a requirement for 1.0. That should jive well with the way facets work today. Then where we go from there is where the discussion opens up.

jodastephen Tue 22 Jun 2010

A recent blog post on JavaFX with some things to think about.

qualidafial Tue 22 Jun 2010

Based on my experiences in Flex, I think that language support for binding expressions is generally not as useful as you'd expect. In nearly all cases we've ended up reverting binding expressions to API calls on our own binding library, since you get more control over the binding pipeline, including validations, conversions, coalescing multiple binding sources, controlling round trip behaviors and timings. You just don't get that degree of control with language level bind expressions.

qualidafial Tue 22 Jun 2010

One thing to think about in this is that field changes don't necessarily map one-to-one with event types. Sometimes you want multiple properties to be mapped to the same event type (my design) or listener list (jodastephen's design in #706).

One example of this would be a rich text control with both a text property and an htmlText property. Both are views over the same content, but with different levels of detail. Ideally you would want to fire a single event so that listeners on either property could respond.

A binding library needs a way to inspect properties reflectively to know where to listen for events. This was the basis of the @Bindable facet in the original post on this thread.

Login or Signup to reply.