Data Binding

It's relatively common for user-interface frameworks built these days to implement some form of reactive, declarative programming commonly called data binding. Data binding is an event-driven software pattern that relates properties via expressions, automatically updating properties when other properties change. Simple rules between properties can be enforced via these simple expressions. To implement more complex relationships, it's recommended to implement one or more methods that are perhaps fired by property changes.

StrataCode's implementation of data binding provides a clean, continuous way to make your code more declarative, without adding many new concepts, and forcing a declarative style on your application with no easy exit. It's data binding system is based on Java expressions making the code readable and easier to move code in or out of a data binding expression. Additionally, you can easily invoke methods from binding events or use a method call as part of a data binding expression. The resulting style of programming integrates data binding as a natural way that scales as your code-complexity increases.

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. You can set breakpoints in any get/set methods. Quickly see the listeners on any property. Set tracing overall or on specific properties. StrataCode's IntelliJ plugin makes it easy to switch from StrataCode to the generated Java and back again.

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 properties

Excel provides a grid of cells and lets you specify a formula or value for each cell which can refer to the other cells. StrataCode components that model with a single name-space of types and properties. Each property can have a value or a formula which can refer to other properties. The result is a quick way to convert normal Java expressions to binding expressions that can react and make your application more declarative. Rules can easily be moved into a downstream layer and customized by a different developer or replaced to create a new version.

Data binding expressions can include any Java expression. If necessary, the code generation will inject 'send event' calls into the setX methods for properties used in binding expressions. A warning is issued if some part of the expression does not send necessary events or not marked as bindable via annotations. You can use data binding with any existing Java library, with methods or static functions and control behavior with annotation layers.

StrataCode's data binding system is based on experience with building large systems with 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 to cover a large share of the business rules. For more complex rules, you can use intermediate properties to simplify or trigger a method which performs step-by-step computation. 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. No longer do developers have to design in customizability and managability up front or to make structural changes to add it later. Instead, they can develop the cleanest, simplest code, knowing customization later will be an easy cut-and-paste, or just let downstream layers override their defaults. And building customization apis is much easier because these layering and editing features are built-in to the runtime, IDE, and management UIs. To create a "customization layer", a developer can cut and paste the relevant code into a new file using static typing. It can be at that point handed off to another team - operations, marketing, etc.

In some situations, a developer will need to share a file with other team members. StrataCode helps with this tool by providing automatic, incremental editing of source code layers from normal forms. Comments, order of items in the file, are maintained creating 'diffable' changes. Developers maintain the same level of control over important code assets while offloading the maintenance.

Examples of Data Binding

Here are some basic examples showing how data binding in StrataCode works. A forward data binding expressions can be any Java expression attached to the ":=" 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 use the := operator, the left side updates when the right side changes. 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 a := or =: expression. For :=, events are automatically propagated through expressions which invalidate the binding when passed as method arguments, used in arithmetic, conditional, and ternary (aka question-mark expressions).

The =: bindings are StrataCode's answer to avoiding closures, so code stays in the type system for better tracing and debugging. When the property on the left hand side changes, the expression on the right hand side is executed. That event can be delivered right away, or asynchronously so it supports a method call via RPC. For 'swing', binding events are run right away unless the current thread is not the UI thread in which case they are run later by the UI thread. Frameworks have flexibility to code-generate meta-data as well as controlling the state using a few thread-local variables to manage the context.

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, or perhaps a setX method does not fire a changed event, bindings can become stale. SC lets you refresh all bindings periodically to address this problem, or you can use an annotation to specify specific bindings to be auto-refreshed (or refresh a specific binding with an api call). In the web framework, if you set 'refreshBindings' on the html page, all bindings in that page are refreshed after UI events are delivered, or after client/server changes have been applied.

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" by the field. 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 and to remove all bindings on child objects as well. 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. When you use the -vb option, you see all binding events that are delivered so it's a quick way to validate if there are any binding leaks.

StrataCode object instances created through the frameworks are disposed automatically. For example, session scoped components will be have their bindings removed when the session expires, or elements of a 'repeatTag' are disposed when they are removed.

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 compile 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.

Multi-threading

There are a number of mechanisms in StrataCode that framework developers can use to provide the appearance of a safe, declarative, single-threaded environment for domain model objects that leverages multiple threads behind the scenes. Before delivering user-interface events, framework code sets up a CurrentScopeContext - a list of all of the available ScopeContexts for the current thread. StrataCode acquires the proper lock for each scope - either a read/write, or a read-only depending on how the current thread uses information in that scope.

When you need to use a binding expression which delivers change events to a different "CurrentScopeContext" - you set the @Bindable(crossScope=true) annotation on the binding. When a cross-scope binding is initialized, it's CurrentScopeContext is saved as the "initialization context". Before applying the binding, the code will check if the CurrentScopeContext matches the "initialization context". If not, it's put into a queue for that context. If there's a waiter for that context, it will wake up and process it with the right locks. If not, it waits till the next request with that context.

TODO: Detect when we need crossScope binding? Or just set it by default for certain frameworks that might have multi-threaded behavior?

Debugging Problems

You can enable tracing of all binding events to figure out what's happening at the fine-grained level. It's best though to set @Bindable(trace=true) or @Bindable(verbose=true) on the properties you need to follow. If you use -vb, you see a lot out output and have to search for the properties you need. If you enable -vba, you get all setX traffic for all bindable properties which is a real lot, but maybe yields some insight if there's an error at the end.

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 trace through code by reading log files.

See debugging for details.