Chapter 9. Data Binding

Binding is one of the most important, powerful and useful features of JavaFX; it can simplify your code to a great extent. In this chapter, we will start with basic binding concepts and proceed toward more sophisticated binding, with appropriate examples.

What Does Binding Mean?

In general terms, binding normally means sticking one object firmly to another, forming a bond, say by tying with a rope. After the objects are bound, anything you do with one of the objects will impact the other. Displacing one of the objects, for example, will displace the other. The definition in JavaFX is on similar lines. It's the ability to create a direct and immediate relationship between two variables, where a change to one variable would change the other.

Binding in JavaFX is achieved through the bind keyword, which associates the value of the target variable with the outcome of an expression. The expression could just be anything—another variable, an object, a function call, an expression, a block, and so on. This expression is called a bound expression and is illustrated in Figure 9-1. A bound expression can be any expression that does not return Void, and so expressions involving increment or decrement operators or loops are not allowed.

Unidirectional binding

Figure 9.1. Unidirectional binding

Listing 9-1 demonstrates a simple bind.

Example 9.1. A simple bind

var x = 0;
def y = bind x * x;
x = 10;
println(y); // 10*10 = 100
x = 5;
println(y); // 5*5 = 25

Output

100
25

In this example, def y is bound to the square of x, which means that whenever the x value changes, the y value will be recalculated but not vice-versa. This is called unidirectional binding. The binding happens in one direction—from right to left. In the example, changing the x value changes y, but changing y will not change x. (There is also bidirectional binding, which we will discuss later in this chapter.) Continuing with the example, the expression x*x depends on the value of x, and so x can be considered a dependency. So when the dependency changes, the expression will be re-evaluated.

Also note that y is declared as def, primarily because no value can be assigned directly to y, and y's value can be changed only through x. If you try to change the value of y directly, that would result in a compilation error. You can also define y as a var, but assigning a value to y will still cause a runtime error, since it is not legal to change the value of y when it is unidirectionally bound to an expression already.

Listing 9-2 is an example that is nearly as simple but uses a loop.

Example 9.2. Changing a bound expression from within a loop

var x = 100;
      var y = bind x + 100;
      for( a in [5..50 step 5]){
        x = a;
        println( "x={x}, y={y}");
      }

Output

x=5, y=105
x=10, y=110
x=15, y=115
x=20, y=120
x=25, y=125
x=30, y=130
x=35, y=135
x=40, y=140
x=45, y=145
x=50, y=150

The code in this example is pretty self-explanatory: whenever the x value changes, y also changes automatically. The only difference from Listing 9-1 is that the value of the expression is changed continuously with a loop, which causes continuous re-evaluation of the expression.

Note

It is recommended to use def instead of var for variables whose value depend on a bound expression since it is not legal to assign a value directly to a bound variable, and such errors can be caught at compile time instead of runtime when using def. However, this applies only to unidirectional binds, and it would still make sense to use var in the case of bidirectional binds, as we will see later in this chapter.

Recalculation of Expressions

When the bound expression changes, the outcome is recalculated and assigned to the target variable. However, this recalculation will not be done for all the elements in the expression, as it is optimized to re-evaluate only the changed portion in the expression. Let us see this in detail with the following bound expression:

def val = bind expr1 + expr2

The value of val will be recalculated whenever expr1 or expr2 changes. However, only the changed expression will be recalculated and not both if only one of the expressions is changing.

Note

There is another important optimization technique, called lazy binding, in which the bound expression will be evaluated only when the result of the expression is being consumed. So it is unsafe to assume that val will be changed immediately when expr1 or expr2 changes when using JavaFX 1.3 or higher, but it is guaranteed to be changed before the first consumption of val's value. For more information, see the last section of this chapter, "Lazy vs. Eager Binding."

Let's prove this with an example; Listing 9-3 shows the code.

Example 9.3. Optimized re-evaluation of a bound expression

var x = 10;
var y = 20;
var z = 10;
var sum = bind addConstant() + z;
println(sum);
z = 30;
println(sum);

function addConstant() {
    println("function called");
    30 // This will be considered as the return value.
}

Output

function called
40
60

In Listing 9-3, the bound expression consists of a function call and a variable. When this expression is evaluated the first time, the value of addConstant() is calculated and remembered. It is then added to the z value, and the outcome is assigned to sum. That's why you see the "function called" message in the output.

Now when the z value changes, the outcome of the addConstant() function is not recalculated (since it is not changed), and the same value that was calculated before is fetched and replaced in the expression. However, the new z value is substituted in the expression to give the value of 60 to sum. Since the outcome of addConstant() is not recalculated, the function is not called the second time.

Thus you can see that the recalculation of bound expressions is optimized so as not to evaluate all the elements in the expression but only the changed portions. We will see more about this optimization when we explore how binding works with other forms of expressions later in this chapter.

Note

The value of the function expression is the value of the last line in the expression, which in our example is 30. So there is no need to have an explicit return statement within the function; also note that the return type is not explicitly mentioned, since it is automatically inferred from the last line of the function block.

Binding with Conditional Expressions

Bind can be used along with conditional expressions such as if-else to choose the value of the target variable conditionally.

Syntax

def x = bind if (condition) expr1 else expr2;

Here, as in any other if-else expression, if condition is true, x will have the value of expr1; otherwise, it will equal expr2. If condition is true, only expr1 is recalculated, and expr2 is ignored even if there is a change in the expr2. The inverse of this is also true. Listing 9-4 demonstrates conditional binding.

Example 9.4. A conditional bind

var mark = 50;
var status = bind if (mark >= 50) then "PASS" else "FAIL";
println(status);
mark = 30;
println(status);

Output

PASS
FAIL

When you use bind with if-else, the value of the if expression must always be a valid value and cannot be Void, though a normal unbound if-else expression can have a Void value. In other words, the following requirements must be taken care of when you use bind along with an if expression:

  • Having an else clause is mandatory

  • if and else expressions (expr1 and expr2 in the syntax shown earlier) must return a valid value and cannot be Void.

Any violation of these requirements would cause a compilation error.

Note

Remember that null is different from Void; null is a valid value and can be used in the bound conditional expressions but Void typically represents "No-value."

Binding with for Expressions

Binding can be used with for expressions as long as those expressions return a valid value and not Void. Listing 9-5 is an example of how the bind keyword can be used with the for expression.

Example 9.5. A bind with a for expression

var min = 0;
var max = 5;
def seq = bind for (x in [min..max]) 2*x;
println(seq);
max = 10;
println(seq);
min = 5;
println(seq);

Output

[ 0, 2, 4, 6, 8, 10 ]
[ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 ]
[ 10, 12, 14, 16, 18, 20 ]

In Listing 9-5, whenever min/max values change within the for expression, the portion of the sequence that would be impacted by the change would be recalculated and not the entire sequence.

For example, initially the for expression is evaluated fully and the sequence is created when it is defined. Now when the max value is changed to 10, the existing items of the seq (index 0 through 5) will not be recalculated, and only the new items are calculated and added to the sequence.

Now when the min value changes from 0 to 5, the initial five entries in the seq are discarded and the rest of the items are never touched.

There is of course, one exception to this: If an expression uses indexof, then it will be re-evaluated completely, regardless of the min/max values, since it is quite possible that some additions/deletions would obviously change the index of all the items. Listing 9-6 demonstrates how this works.

Example 9.6. A bind with a for expression using indexof

var seq = [1..10];
      def seq1 = bind for (x in seq) {
          x * (indexof x) + 2;
      }
      println(seq1);
      insert 0 before seq[0];
      println(seq1);

Output

[ 2, 4, 8, 14, 22, 32, 44, 58, 74, 92 ]
[ 2, 3, 6, 11, 18, 27, 38, 51, 66, 83, 102 ]

In this example, note that the entire sequence has changed after we insert a value at the beginning of seq; this occurs because seq elements actually depend on the index of the corresponding element in seq. Inserting an item in the beginning alters the index of all the elements in seq, and hence causes the entire seq1 to be recalculated by rerunning the for expression through its entire range.

It is important to re-emphasize here that a bound variable cannot be modified directly, and doing so will result in a runtime error. This is true of a var; In the case of a def, the variable cannot be modified directly at all, regardless of whether it is bound, and doing so will result in compile-time error.

With for expressions, one can have a non-default step value also bound to a variable in addition to the min and max values. Let us see how this works; Listing 9-7 shows the code.

Example 9.7. A bind with a for expression using a bound step value

var stepVal = 1;
      var max = 20;
      var seq = bind for (i in [0..max step stepVal] where i < max/2 )  i;
      println(seq);
      stepVal = 5;
      println(seq);
      stepVal = 8;
      println(seq);
      max = 41;
      println(seq);

Output

[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
[ 0, 5 ]
[ 0, 8 ]
[ 0, 8, 16 ]

As you see in this example, maximum as well as step values are bound to variables, and any change to either max or the step value would cause the for expression to be recalculated. Changing the step value from 5 to 8 would cause the entire sequence to be recalculated; however, changing the max value will just insert additional elements into the sequence and will not recalculate the existing elements.

It is also possible to use functions within a bound for expression. Listing 9-8 demonstrates how to do this.

Example 9.8. Functions in a bound for expression

var sum = bind for (i in [1..10] where i > 5 )  sumSeq([1..i]);

function sumSeq( seq : Integer[]):Integer {
    var sum = 0;
    for (num in seq) {sum = sum + num;}
    sum
}
println(sum);

Output

[ 21, 28, 36, 45, 55 ]

In this example, the for expression actually delegates the sum calculation to a function that is invoked whenever the i value changes. Note that a sequence has been created and passed as an argument to the function.

Binding Block Expressions

A block is a set of expressions enclosed within curly braces. The value of the block expression is the value of the last expression in the block. When the block is bound to a target variable, there are certain restrictions as to what can appear within the block. Typically, the following restrictions apply to expressions that are defined before the last statement within a block:

  • The last statement in the block must fetch a valid value and cannot be Void. Hence you cannot have statements such as println() anywhere within the bound block.

  • Only variable declarations (def/var) can appear in nonfinal positions.

  • Assignment, increment, and decrement operators are prohibited at nonfinal positions. (Assignment is allowed only if it is preceded by a variable declaration.)

  • Expressions such as while and insert/delete (see Chapter 10, "Sequences") are not allowed.

Violating any of these restrictions would result in a compilation error. As far as recalculation of expressions is concerned, any change in the expressions specified within the block would cause the entire block expression to be re-evaluated.

Syntax

def xxx = bind {
    def a = expr;
    var b = expr1;
    expr2;
}

Listing 9-9 demonstrates an example of a block expression with bind.

Example 9.9. A bound block expression

var x = 10;
var y = 10;
var z = 0;

def sum = bind {
    x + y + z;
}
println(sum);
x = 20;
println(sum);

Output

20
30

In Listing 9-9, changing the x value has caused the block expression to be re-evaluated, and the newly computed value is assigned to the dependent variable sum.

Binding Functions

A function whose outcome is bound to a target variable will be called whenever any argument of the function changes. Let us prove this with a simple example (Listing 9-10).

Example 9.10. Binding a function

var x = 10;
      var y = 20;
      var z = bind sum (x, y);
      println(z);
      x = 20;
      println(z);
      y = 30;
      println(z);

      function sum(x: Integer, y: Integer) {
          println("added");
          x + y;
      }

Output

added
30
added
40
added
50

Here, the sum() function takes two arguments, and its outcome is bound to z. So whenever the arguments of the sum() function change, the sum() function will be invoked to recalculate the value of the function-expression. So initially the sum() function is invoked and its output of 30 is assigned to z. After this, the x value changes. Hence the function is called again to recalculate the value of x+y, and the new value is assigned to z. The same applies to y as well. This is the reason you see three "added" statements in the output, and it proves that the function is invoked whenever the arguments change its values. (Again, remember that the function sum() may not be called immediately when the x/y value is changed but instead is done when the output of z is being consumed within a println after a change to x/y. This is the same lazy binding optimization that you will learn at the end of this chapter.)

The ability to bind a function to a target variable is not limited to JavaFX functions; it is also available to existing functions available in Java classes.

Note

When a bound function is invoked from within a bound context (meaning that it's called automatically because one of its parameters has changed in value), the arguments are passed by reference and so is the return value. Passing by reference means that a pointer to the actual data (the memory address) is sent and not the actual data itself. By contrast, if the same method is called from within a nonbound context (for example, the developer calling it explicitly), the arguments and return value are passed by value and not by reference.

Listing 9-11 is an example of how a Java function can be bound.

Example 9.11. Binding a Java function

import java.lang.Math;
      var d = 0;
      var x2 = bind Math.toRadians(d);

      for( r in [0..360 step 60]) {
          d = r.intValue();
          println("d={d}, x2= {x2}");
      }

Output

d=0, x2= 0.0
d=60, x2= 1.0471975511965976
d=120, x2= 2.0943951023931953
d=180, x2= 3.141592653589793
d=240, x2= 4.1887902047863905
d=300, x2= 5.235987755982989
d=360, x2= 6.283185307179586

The code in Listing 9-11 just converts angle to radians using the Java Math API, and you can see that x2 is bound to a Java function—Math.toRadians(). Likewise, you can bind any Java function from within JavaFX, including creating new objects through respective constructors.

Bound Functions

A function that is bound to a target variable using bind as shown in Listings 9-10 and 9-11 is different from a "bound" function, which will have the bound keyword in its definition. In this section you'll see how the behavior of a bound function is different from the normal bind function. First, Listing 9-12 shows an example of an ordinary bind function.

Example 9.12. A standard bind function

var name1 = "JavaFX";
var name2 = "Technology";
var filler = "Cool";

function concat(x: String, y: String) {
    "{x} {filler} {y}";
}

def s = bind concat(name1, name2);
println(s);
name1 = "Java";
println(s);
filler = "mature";
println(s);

Output

JavaFX Cool Technology
Java Cool Technology
Java Cool Technology

In Listing 9-12, you can see that when name1 changes, the function is invoked as we have seen in the previous examples. However, the body of the function is a black-box, and any change in the function's body does not cause the function to be reinvoked even though the function is bound. So with normal functions that are associated with a target variable through bind, recalculation happens only when the argument changes; bind does not really care about the expressions specified in the function body. This is why you don't see filler being changed to mature in the output, because the function is not invoked when filler is changed.

However, there are cases where one would expect bind to evaluate the function even if one of the expressions in the function's body changes. Nevertheless, this is a little expensive and should not be carried out by default unless the developer wants it explicitly. This is where bound functions come into the picture.

A bound function is invoked even when there is a change in one of the expressions specified in the function body and the arguments to the function do not change. Bound functions are explicitly marked with the bound keyword. The parameter-passing tips given for the normal bind functions apply to bound functions as well—parameters and return values are passed by reference (the memory address of the actual data) when the bound functions are invoked from a bound context and passed by value when they are invoked by the application explicitly.

Now let's change the previous example to use a bound function and see how the behavior changes; Listing 9-13 shows the code.

Example 9.13. A bound function

var name1 = "JavaFX";
var name2 = "Technology";
var filler = "Cool";

bound function concat(x: String, y: String) {
    "{x} {filler} {y}";
}

def s = bind concat(name1, name2);
println(s);
name1 = "Java";
println(s);
filler = "mature";
println(s);

Output

JavaFX Cool Technology
Java Cool Technology
Java mature Technology

As you see in Listing 9-13, now the concat() function has a bound prefix, and bound is a keyword. Thus, the last output indeed reflects the new filler value and indicates that the function was called when the filler value changed.

Note

Although bound functions look appealing, they come with a cost in performance and therefore should be used with caution. While using bound functions, the developer has to be sure under what circumstances the function will be invoked and when the expressions given within the function body are bound to change.

Binding with Object Literals

As you saw in Chapter 6, "Class Definitions," JavaFX supports class definitions similar to Java, and you can create your own classes. Unlike Java, however, JavaFX does not expect everything to be bundled within a class, and it is possible to have a FX application without any class definitions. Let us see some examples of how binding can be used with these classes.

For example, Listing 9-14 demonstrates how you create a class.

Example 9.14. Creating a class

class Employee {
    var name: String;
    var age: Number;
    var department: String;
    var id: Number;
}

Now let's see how to create an instance of the class.

var emp = Employee {
    name: "Praveen"
    age: 44
    department: "JavaFX"
    id: 334455
};

Note that we will have to use the colon character (:)instead of equal to when initializing the object literal. It is legal to omit the initialization of some of the attributes of the class if desired, and those attributes will be assigned with default values.

Now instead of hard-coding the department name for each employee, we could store it in some variable and share it across all employee instances. Let's see how binding helps in this case; Listing 9-15 shows the code.

Example 9.15. Binding with object literals

var deptName = "JavaFX";
      var empNames = ["Richard", "Praveen", "Lawrence", "Steve"];
      var emp:Employee[] = [];

      class Employee {
          var name: String;
          var age: Number;
          var department: String;
          var id: Number;

          function printInfo() {
              println("{name}, {age}, {department}, {id}");
          }
      }

      emp = for (x in [0..3]) {
          Employee {
              id: x
              name: empNames[x]
              department: bind deptName
              age: 34
          }
}

      for (e in emp) e.printInfo();
      deptName = "JavaFX BU";
      println("----------------");
      for (e in emp) e.printInfo();

Output

Richard, 34.0, JavaFX, 0.0
Praveen, 34.0, JavaFX, 1.0
Lawrence, 34.0, JavaFX, 2.0
Steve, 34.0, JavaFX, 3.0
----------------
Richard, 34.0, JavaFX BU, 0.0
Praveen, 34.0, JavaFX BU, 1.0
Lawrence, 34.0, JavaFX BU, 2.0
Steve, 34.0, JavaFX BU, 3.0

In Listing 9-15, we create four instances of the Employee class and bind the department name to the deptName variable. So initially, all four instances are created with JavaFX as the department name. That's what you see in the output. Then we change the deptName value to JavaFX BU, and you see that all four instances are getting updated to the new department name.

This example demonstrates binding at the object level. However, it is also possible to define the binding at the class level so that it applies to all the objects created. Listing 9-16 is an example showing how class-level binding is achieved for members of the class.

Example 9.16. Class-level binding

class Cube {
    var x: Number;
    var y: Number;
    var z: Number;
    var area: Number = bind x * y * z;
}

var c1 = Cube {
    x: 10
    y: 10
    z: 10
}
var c2 = Cube {
    x: 2
    y: 2
    z: 2
}
println(c1.area);
println(c2.area);
c1.x = 11;
c2.y = 4;
println(c1.area);
println(c2.area);

Output

1000.0
8.0
1100.0
16.0

As you see in the output of this example, the variable area is a multiplication of x, y, and z across all instances of Cube, so there is no need to define it once per each instance. Changing the x, y, z values of the cube would automatically recalculate the value of area for the respective instance of the class.

What we have seen here is an attribute-level binding within the class or instance. However, this would not be possible in certain cases, such as when the respective attribute is a public-init attribute. Such objects are described as immutable, and public-init attributes can only be initialized once and cannot be bound or assigned. Binding would still be possible in such cases, but in that case the entire object has to be bound. Let's see how this works; Listing 9-17 shows the code.

Example 9.17. Binding immutable objects

class MyCircle {
    public-init var centerX: Number;
    public-init var centerY: Number;
    var radius: Number;

    init {
        println("Init Called");
    }
    override function toString() {
        "centerX: {centerX}, centerY: {centerY}, radius: {radius}";
    }
}

var cx = 100.0;
var cy = 100.0;
var r = 50.0;

var circleObj = bind MyCircle {
    centerX: cx
    centerY: cy
    radius: r
}

println(circleObj);
cx = 200;
println(circleObj);
cy = 200;
println(circleObj);
r = 30;
println(circleObj);

Output

Init Called
centerX: 100.0, centerY: 100.0, radius: 50.0
Init Called
centerX: 200.0, centerY: 100.0, radius: 50.0
Init Called
centerX: 200.0, centerY: 200.0, radius: 50.0
Init Called
centerX: 200.0, centerY: 200.0, radius: 30.0

In this example, we have used the bind keyword not for the attribute but for the whole instance. This means that whenever there is a change in the values of cx, cy, or r, the whole object would be recreated and returned, since the individual attribute values cannot be changed. So as you see in the output, the init is called whenever we change the cx, cy, or r values, which indicates that a new object is being created. thus, init can be considered an equivalent of a constructor in a Java class that is called for every object creation.

There are many built-in immutable objects like this in the JavaFX APIs, such as Font, RadialGradient, LinearGradient, and so on, and this type of binding will come handy when dealing with those objects.

Note

Always keep in mind that object creation and disposal are costly operations, especially when there are more members in the class such as attributes and functions. Hence, this kind of binding must be used with caution since the whole object is recreated every time an attribute value changes.

Bidirectional Binding

So far we have only seen unidirectional binding, in which the expression on the right side is bound to the target variable on the left, and any change in the expression causes the target variable to change. This is unidirectional binding and all the examples we have seen so far are of this type. Bidirectional binding is a type of binding in which the variables on both the sides listen to each other's changes and change themselves accordingly. This way, the variables on the left and right sides always remain in sync, as shown in Figure 9-2.

Bidirectional binding

Figure 9.2. Bidirectional binding

The syntax of bidirectional binding is as follows:

var x = bind y with inverse

When the y value changes, the x value also changes, and vice-versa. Listing 9-18 is an example that demonstrates bidirectional binding.

Example 9.18. Bidirectional binding

var name = "JavaFX";
var name1 = bind name with inverse; // indicates bidirectional binding
println(name1);
name = "Java";
println(name1);
name1 = "C++";
println(name);
println(name1);

Output

JavaFX
Java
C++
C++

In Listing 9-18, please note that the name1 variable is bound to name using with inverse, which means that if name changes, name1 will also change, and if name1 changes, name will likewise change. Also note the line name1 = "C++". The println after this line indicates that both name and name1 have changed to C++. If the binding were unidirectional, name1 = "C++" would have resulted in a runtime error, saying "cannot assign to bound variable."

Currently bidirectional binding is limited to objects and variables and not expressions. That's because the variables on both the left and the right must be assignable, and you cannot have an expression in place of variables, since no value can be assigned to an expression. This perhaps makes bidirectional binding less interesting for local variables, but it would be very useful in the case of objects.

Listing 9-19 shows an example of how bidirectional binding works with object literals.

Example 9.19. Bidirectional binding with objects

class XY {
    var x: Number;
    var y: Number;
    override function toString() { "x: {x}, y: {y}" }
}
def pt1 = XY {
    x: 10
    y: 10
}
def pt2 = XY {
    x: bind pt1.x with inverse
    y: 0
}

println(pt1);
pt1.x = 20;
println(pt2);
pt2.x = 30;
println(pt1);

Output

x: 10.0, y: 10.0
x: 20.0, y: 0.0
x: 30.0, y: 10.0

Listing 9-19 is a classic example of how bidirectional binding works with object literals. As you see in the code, there are two instances of the XY class, and the x attribute of one of them is bound to the other. First pt1.x is changed, and that actually changes pt2.x. After that, pt2.x is changed, which changes pt1.x.

Because bidirectional binding is limited to just variables, it cannot be used with many of the expressions for which unidirectional binding works, such as the following:

  • for expressions

  • Conditional expressions

  • Block expressions

  • Arithmetic expressions

  • Function expressions

Even though bidirectional binding looks fairly limited, it can still be very useful with objects and primitive types.

With bidirectional binding, you can also create a chain of bound objects up to any level, where one value change will trigger a sequence of changes in the chain of bound variables. Listing 9-20 is an example of bidirectional multi-level binding.

Example 9.20. Bidirectional multi-level binding

var x1 = 10;
var y1 = bind x1 with inverse;
var z1 = bind y1 with inverse;

println("x1: {x1} y1: {y1} z1: {z1}");
x1 = 20;
println("x1: {x1} y1: {y1} z1: {z1}");
y1 = 30;
println("x1: {x1} y1: {y1} z1: {z1}");
z1 = 40;
println("x1: {x1} y1: {y1} z1: {z1}");

Output

x1: 10 y1: 10 z1: 10
x1: 20 y1: 20 z1: 20
x1: 30 y1: 30 z1: 30
x1: 40 y1: 40 z1: 40

As you can see, x1, y1 and z1 are bound to one another, and any change in any of those values causes all other variable values to change.

Lazy vs. Eager Binding

So far what we have seen is eager binding, where regardless of whether the target variable is used or not, the bound expression will be recalculated when there is a change in the bound expression. But there is another type of binding, called lazy binding, where the recalculation will occur only when the target variable is accessed. Prior to JavaFX 1.3, it was left to the application developer to decide whether to go with eager or lazy binding, and if the developer chose to go with lazy binding, the expression had to be marked with the lazy keyword.

Syntax

var <varname> = bind lazy <expr>

However, in JavaFX 1.3 the compiler has undergone lot of optimization and has the ability to choose eager or lazy binding depending on the expression being bound. Lazy binding offers high performance benefits and hence is made the default in JavaFX 1.3, so you don't have to specify the lazy keyword anymore. While it is said to be the default, there are places where lazy binding cannot be employed, especially if the value of a bound expression is consumed immediately within a trigger defined on the bound expression. Triggers are covered later in this book, but essentially, a trigger is a block of code, defined on a variable within the on replace block, that is executed whenever the value of the variable changes.

So when a bound expression changes, the value of the target variable changes and thus its trigger will immediately be called to consume the changed value. In such cases, the JavaFX compiler employs eager binding. So if you really want your binding to be eager for any reason, define a trigger on the target variable.

Let us see the exact difference between eager and lazy binding with a simple example. First, Listing 9-21 demonstrates lazy binding.

Example 9.21. Lazy binding

var x = 10;
      var val = bind multiplyByTen(x);
      for (i in [1..4]) x ++;
      println("val: {val}");

      function multiplyByTen(y: Integer):Integer {
          println("function called {y}");
          y * 10;
      }

Output

function called 14
val: 140

In this example, there is a function that is bound to a target variable, val, and takes a single argument, x. So in theory, multiplyByTen() is supposed to be called whenever the x value changes. This was the case prior to JavaFX 1.3. But with 1.3, the compiler optimizes this scenario to see if the returned value of the function is actually being consumed anywhere; it performs this check every time the function is invoked. In this case, we are just incrementing the x value five times, but the target value val (the consumer of the function's output) is not being used anywhere. So the compiler continues with incrementing x without calling the function. Finally, when we print the value of val, at that time the function is called with the last value of x, which is 14, and gets a return value of 140 from the function.

So by default, the bound expression is not evaluated immediately when one of its elements changes. Instead, it is evaluated when the result of the expression is being consumed, and that's what is called lazy binding.

However, if you want to restore the pre-1.3 behavior of eager evaluation, you will have to define a trigger on the target variable. Let us modify the previous example to make the evaluation eager, restoring pre-JavaFX1.3 behavior; Listing 9-22 shows the code.

Example 9.22. Eager binding

var x = 10;
        var val = bind multiplyByTen(x) on replace {
        // Any code that may consume val, validate val
    };
    for (i in [1..4]) x ++;
    println("val: {val}");

    function multiplyByTen(y: Integer):Integer {
        println("function called {y}");
        y * 10;
    }

Output

function called 10
function called 11
function called 12
function called 13
function called 14
val: 140

In this example, we have used triggers (on replace), a feature that you'll learn about in Chapter 11. For now, just remember that the block after on replace is executed whenever the value of the variable val changes. When you compare Listings 9-21 and 9-22, the only difference is the addition of the on replace clause to the bound expression, and you can see the difference in the output—the function is invoked for every change of x. The trigger indicates that the output of the bound expression is likely to be consumed almost immediately when an element of the expression changes, and so the compiler employs eager binding in this case.

So wherever you need eager binding, define a trigger on the bound expression.

Lazy binding makes execution far more optimized by minimizing the unnecessary evaluation of bound expressions for every bit of change, and in the process offers much better performance for your applications.

Summary

Binding is one of the most powerful features of JavaFX; it has the potential to simplify your code drastically when used wisely. It is commonly used to keep your UI in sync with the back-end model easily, which otherwise would have to be coded explicitly through listeners. You will see more examples of this in Chapter 12, "Introduction to JavaFX Graphics."

The success of binding depends on how judiciously it's been used in the application. Although it is one of the most powerful features in JavaFX, it often comes with a price in terms of performance, and indiscriminate use of bind can significantly bring down the performance of your application. JavaFX 1.3+ has far more performance optimizations, such as lazy evaluation of bound expressions by default, but still striking a good balance between the bind usage and the potential performance trade-offs is critical to the success of your RIA.

Nonetheless, binding when used appropriately will significantly ease development and help you avoid writing a lot of unnecessary code that would otherwise be needed to keep your UI objects in sync with the changing data in the back-end.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset