GWT

Data Binding

StrataCode adds powerful, simple, intuitive data binding based on Java expressions for attaching user interface to model, business rules, workflow, and configuration. Data binding expressions are converted into static method calls on the Bind class during the transformation from StrataCode to Java. One of the challenges of data binding frameworks is making complex expressions efficient and easy to debug so this design focuses on those goals.

There are three types of bindings:

:=  forward: sets disabled based on the length of entryText
=:  reverse: calls addEntry when the form is submitted and submitCount is incremented
:=: bidirectional: keeps entryText and value in sync.

Here's a simple example which uses:

class UserForm {
    String entry;
    boolean submitEnabled := entry != null && TextUtil.length(entry) > 0;
    SubmitEvent submitEvent;
    submitEvent =: FormManager.submitEntry(entry);
}
Here's an example using schtml which includes a bi-directional binding:
<form submitEvent="=: addEntry()">
    <input type="text" value=":=: entryText" disabled=":= TextUtil.length(entryText) == 0">
</form>

Excel-like formula for Java

Think of your application having an Excel-like sheet of properties, and expressions but instead of a grid, it's a tree of types and their properties. You can override any cell/property either by value or formula. The formula include any Java expression. You can use data binding with any existing Java library, with methods or static functions. You can override any value or formula in a new layer.

Several modern platforms implement some form of reactive, declarative programming commonly called data binding. Data binding is a software pattern where you use expressions that are evaluated automatically when properties change. These expressions can update other properties or invoke methods. Instead of worrying about loops, if statements, etc. you can do more with these data binding rules with less code. It's easier to change the expressions to control the logic of your program.

StrataCode's data binding system is based on over 20 years of experience with building large systems using data binding techniques. To benefit the most from data binding, we've found that you can effectively expose a rich, easily configured layer of features and functionality in binding rules. Use intermediate properties to break up complex rules. Or trigger a method which performs step-by-step computation when properties change. These techniques let you hide the more complex logic, keeping it separate from the declarative, configurable layer, but expose a very rich configurable set of properties and rules.

With StrataCode layers, technical users can collaborate in multiple ways with developers. If they use the same layer, their changes affect only the lines of code they touch in the tool. They might be editing changes in a new layer, where the developer has placed only the editable rules and values, or by recording changes in a transparent layer which records just their changes. No longer do developers have to design in customizability up front or code it in later. Instead, they can develop the cleanest, simplest code, knowing customization later will be an easy cut-and-paste.

StrataCode Data Binding

Here's how data binding works in StrataCode. A data binding expressions can be any Java expression attached to a new special type of assignment operator:

class MyClass {
  int a := 2 * b;
  int b = 3;
}

Changes are propagated along the equals sign in the direction of the colon(:) in an assignment. When you say := the left side updates when the right side is changed. So in the example above, when b is changed, a is set to 2 times b.

There are two other data binding operators. When you use =: the right side of the Java expressions is updated or evaluated when the left hand side changes. When you use :=: the change goes both ways. In this case, the expression must have an inverse expression which StrataCode can evaluate when the left hand side changes or an error is generated.

Any Java expression can be used on the right side of an := or =: expression. For :=, events are automatically propagated through expressions which invalidate the binding in many cases. That's easy to say but was a lot harder to implement in a clean and efficient way.

The =: bindings are StrataCode's answer to function closures. When the property on the left hand side changes, the expression on the right hand side is executed. That event can be delivered asynchronously, so it batches execution, invokes an RPC call, switches to the UI update thread, or whatever the framework layers need to do to prepare for that method call. Frequently you call a method passing it some parameters taken from the context of the binding expression.

When a binding is performed on a method which has side-effects, i.e. it's return value changes even though it's parameters did not change, bindings can become stale - i.e. they do not refresh automatically. SC lets you refresh all bindings globally to address this problem, or mark specific bindings that need to be refreshed. You can do this once just before refreshing the user interface to support stale bindings.

Data Binding

StrataCode supports data binding using Java expressions on a type's properties: typically fields or JavaBean style getX/setX methods.

Data Binding Simple Examples

Foo {
  int b = 3;
  int a := b;
}
A's value is set whenever b's value changes. A is initialized to b's initial value.
Foo {
   int b;
   int a = 3;
   a =: b;    // sets b to 3 on init and b tracks changes from a
}
In this example, b's value is set whenever a's value changes, b is initialized to a's initial value.
Foo {
   b = 3;
   a :=: b;
}
If a changes, b is set. If b changes, a is set. A is initialized to b's initial value. Methods and array operations are supported as well:
class Foo {
  int[] c = {1,2,3};
  int i = 0;
  int d :=: c[i];   // d is initialized to 1 and tracks changes
                    // to either i or c.  if d is changed, it
                    // updates the appropriate element of c.
}

For expressions which invoke a method, bi-directional bindings are supported only if the method declares a BindSettings annotation with the reverseMethod attribute set. see the getting started for more details on that.

But reverse-only binding expression to method calls are allowed. In this case, the binding has nothing to do with defining the value of the property. Instead the method is called when the property is changed. So in this case, you can even pass the value of the property to the method asa parameter to implement a simple property change handler:

// Calls the method changeHandler whenever d changes with the
// current value of d.
int d =: changeHandler(d);  

public void changeHandler(int currentD) {
   System.out.println("*** d changed: " + currentD);
}
Implement raised/lowered border swap based on the selected state of the JToggleButton:
class BevelToggle extends JToggleButton {
   border := selected ? new SoftBevelBorder(BevelBorder.LOWERED) : new SoftBevelBorder(BevelBorder.RAISED);
}

Removing or Replacing Bindings

One of the challenges with programming with rules is turning off the old rule when you dispose the object. StrataCode stores bindings with the objects they belong to. When you dispose of the objects using a utility method provided, the bindings that object defines goes away as well.

Another challenge is removing an old rule when you want to customize it with a new rule. StrataCode makes this easy as well. When the binding expression is either := or :=: (i.e. it has some "forward" component), there is a single binding expression that is "owned" sense by the field which is used in the binding definition. If you define a new forward binding on the same field in a subclass or sub layer, it replaces the previous forward binding expression on that field.

That rule ensures only one rule defines the value for any given property. But it is very convenient to have multiple reverse-only bindings for the same property so this rule does not apply to them. You can use multiple =: definitions on the same property and all of them will take effect. The only way to remove a reverse only definition is using the API: Bind.removePropertyBindings(Object obj, String propertyName).

When an object is disposed by StrataCode (DynUtil.dispose(Object obj)), it's bindings are automatically removed on all fields. If you discard an instance you created explicitly in your code, and it uses data binding expressions, you'll need to call Bind.removeBindings(object) or better DynUtil.dispose(object) to also remove it from the dynamic typing system if that is active. Otherwise, the listeners added by those binding expressions won't be removed and the instances methods will still fire. Eventually all of the information will be garbage collected and the bindings go away at that point but it's best to dispose of instances you create to stop those bindings as soon as possible.

StrataCode object instances are disposed automatically. For example, session scoped components will be have their bindings removed when the session expires.

Sending Binding Events

Data binding rules run when properties change. At the runtime level, there needs to be a "sendEvent" call made, usually in the setX method to signal a property change. You have a few choices on how to add these events.

  1. When StrataCode compiles this code, it injects the necessary "sendEvent" call automatically only when properties are used in a binding expression. If you bind to a field, that field is turned into getX/setX methods and all references to that field are converted automatically as needed.

  2. If you mark a property with the @Bindable annotation, StrataCode will add the sendEvent call even if that property is not used in binding expressions. You will need to do this if you use StrataCode to generate compiled libraries where users do not have the source.

  3. You can send events manually in code to trigger events. If you do, you should mark the property the annotation @Binding(manual=true) to tell StrataCode the property is bindable. Otherwise, it will either add the binding logic again itself (for code it compiles) or give you a warning (for compiled code).

Performance and Scalability

To implement this pattern most effectively StrataCode uses code injection and compile-time validation of bindings to make the most powerful, bindable Java expressions available. StrataCode detects which expressions cannot be made bindable and warns you at cmpile time. You can control the warnings through easy annotations. It's easy to create bindable libraries of objects and methods to provide even more power in your dynamic relations including objects which trigger events to invalidate all methods on the instance, method pairs that implement complex bi-directional relationships. When an expression is not bindable via event firing, you can mark bindings or objects to be refreshed - where the bindings are validated and only fired if their values have changed. This is not as low-latency and scalable as event firing but a nice fallback to code injection. In fact, many modern reactive frameworks only support this model and do hand waving at complex applications that use bindings (i.e. more than 10K bindings).

In StrataCode a data binding is simply an instance of an event listener created when that property is initialized. It's registered with it's owning object so there's only one binding that defines the value for each property on the object. The process of sending an event and firing a binding allocates no memory.

Debugging Problems

You can enable tracing of all binding events to figure out what's happening. It's possible for conflicting binding rules to be defined which generate infinite loops. StrataCode catches those errors hopefully at compile time but if necessary at runtime. It reports the list of bindings involved, whether in Java or Javascript and deactivates them. When you enable tracing, it's easy to track down which bindings are at fault, as you trace the flow of values. Currently this requires that you are the kind of programmer who can read log files.

Better tools could make it easier to trace down logic errors of all kinds in data binding expressions, capturing the log files for selected properties, providing both the organization and access to stack traces for specific changes.

See debugging for details.