Data binding, or the ability to create an immediate and direct relationship between two variables, is one of the most powerful features of the JavaFX Script programming language. This lesson begins with binding two simple variables and progresses to more sophisticated binding: between a variable and the outcome of a function or an expression. Once you understand the concept, Applying Data Binding to UI Objects, a lesson in Building GUI Applications with JavaFX, gives an example of how data binding can be a powerful tool when building JavaFX applications.
A replace trigger is a block of code that is attached to a variable — when the variable changes, the code is automatically executed. A practical application of a replace trigger is presented.
For more information on data binding, see Chapter 7. Binding in the JavaFX Script Programming Language Reference.
|
Contents
The bind keyword associates the value of a target variable with the value of a bound expression. The bound expression can be a simple value of some basic type, an object, the outcome of a function, or the outcome of an expression. The following sections present examples of each.
In most real-world programming situations, you will use data binding to keep an application's Graphical User Interface (GUI) in sync with its underlying data. (GUI programming is the topic of the Building GUI Applications with JavaFX; below we demonstrate the basic underlying mechanics with some non-visual examples.)
Let's start simple: the following script binds variable x to variable y, changes the value of x, then prints out the value of y. Because the variables are bound, the value of y automatically updates to the new value.
var x = 0;
def y = bind x;
x = 1;
println(y); // y now equals 1
x = 47;
println(y); // y now equals 47
|
Note that we have declared variable y as a def. This prevents any code from directly assigning a value to it (yet its value is allowed to change as the result of a bind). You should use this same convention when binding to an object (recall that we introduced Address in Using Objects):
var myStreet = "1 Main Street";
var myCity = "Santa Clara";
var myState = "CA";
var myZip = "95050";
def address = bind Address {
street: myStreet;
city: myCity;
state: myState;
zip: myZip;
};
println("address.street == {address.street}");
myStreet = "100 Maple Street";
println("address.street == {address.street}");
|
By changing the value of myStreet, the street variable inside the address object is affected:
address.street == 1 Main Street
address.street == 100 Maple Street
|
Note that changing the value of myStreet actually causes a new Address object to be created and then re-assigned to the address variable. To track changes without creating a new Address object, bind directly to the object's instance variables instead:
def address = bind Address {
street: bind myStreet;
city: bind myCity;
state: bind myState;
zip: bind myZip;
};
|
You can also omit the first bind (the one just before Address) if you are explicitly binding to the instance variables:
def address = Address {
street: bind myStreet;
city: bind myCity;
state: bind myState;
zip: bind myZip;
};
|
The previous lessons taught you about functions, but there is another distiction that you must also learn: bound functions vs. non-bound functions.
Consider the following function, which creates and returns a Point object:
var scale = 1.0;
bound function makePoint(xPos : Number, yPos : Number) : Point {
Point {
x: xPos * scale
y: yPos * scale
}
}
class Point {
var x : Number;
var y : Number;
}
|
This is known as a bound function, because it is preceded with the bound keyword.
Note: the bound keyword does not replace the bind keyword; the two are used in conjunction as described below.
Next let's add some code to invoke this function and test out the binding:
var scale = 1.0;
bound function makePoint(xPos : Number, yPos : Number) : Point {
Point {
x: xPos * scale
y: yPos * scale
}
}
class Point {
var x : Number;
var y : Number;
}
var myX = 3.0;
var myY = 3.0;
def pt = bind makePoint(myX, myY);
println(pt.x);
myX = 10.0;
println(pt.x);
scale = 2.0;
println(pt.x);
|
The output of this script is:
Let's analyze this script one section at a time.
The code:
var myX = 3.0;
var myY = 3.0;
def pt = bind makePoint(myX, myY);
println(pt.x);
|
initializes the script variables myX and myY to 3.0. These values are then passed as arguments to the makePoint function, which creates and returns a new Point object. The bind keyword, placed just before the invocation of makePoint, binds the newly created Point object (pt) to the outcome of the makePoint function.
Next, the code:
myX = 10.0;
println(pt.x);
|
changes the value of myX to 10.0 and prints out the value of pt.x. The output shows that pt.x is also now 10.0.
Lastly, the code:
scale = 2.0;
println(pt.x);
|
changes the value of scale and again prints out the value of pt.x. The value of pt.x is now 20.0. However, if we remove the bound keyword from this function (thereby making it a non-bound function), the output would be:
This is because non-bound functions only get re-invoked when one of their arguments change. Since scale is not a function argument, changing its value does not result in another function invocation.
You can also use bind with for expressions. To explore this, let's first define two sequences and print out the values of their items:
var seq1 = [1..10];
def seq2 = bind for (item in seq1) item*2;
printSeqs();
function printSeqs() {
println("First Sequence:");
for (i in seq1){println(i);}
println("Second Sequence:");
for (i in seq2){println(i);}
}
|
seq1 contains ten items (the numbers 1 through 10). seq2 also contains ten items; these items would have the same values as seq1, but we have applied the expression item*2 to each, so their values are doubled.
The output is therefore:
First Sequence:
1
2
3
4
5
6
7
8
9
10
Second Sequence:
2
4
6
8
10
12
14
16
18
20
|
We can bind the two sequences by placing the bind keyword just before the for keyword.
def seq2 = bind for (item in seq1) item*2;
|
The question now becomes: "if seq1 changes in some way, will all of the items — or just some of the items — in seq2 be affected?" We can test this out by inserting an item (the value 11) into the end of seq1, then printing the values of both sequences to see what changed:
var seq1 = [1..10];
def seq2 = bind for (item in seq1) item*2;
insert 11 into seq1;
printSeqs();
function printSeqs() {
println("First Sequence:");
for (i in seq1){println(i);}
println("Second Sequence:");
for (i in seq2){println(i);}
}
|
Output:
First Sequence:
1
2
3
4
5
6
7
8
9
10
11
Second Sequence:
2
4
6
8
10
12
14
16
18
20
22
|
The output shows that inserting 11 into the end of seq1 does not affect the first 10 items in seq2; a new item is automatically added to the end of seq2, with the value of 22.
Replace Triggers are arbitrary blocks of code that attach to variables and execute whenever the variable's value changes. The following example shows the basic syntax: it defines a password variable and attaches a replace trigger to it; when the password changes, the trigger prints out a message reporting its new value:
var password = "foo" on replace oldValue {
println("\nALERT! Password has changed!");
println("Old Value: {oldValue}");
println("New Value: {password}");
};
password = "bar";
|
The output of this example is:
ALERT! Password has changed!
Old Value:
New Value: foo
ALERT! Password has changed!
Old Value: foo
New Value: bar
|
The trigger in this example fires two times: first, when password is initialized to "foo", and then again when its value becomes "bar". Note that the oldValue variable stores the value of variable before the trigger was invoked. You can name this variable anything you want; we just happen to use oldValue because it is a descriptive name.
Do you have comments about this article? We welcome your participation in our community. Please keep your comments civil and on point. You may optionally provide your email address to be notified of replies - your information is not used for any other purpose. By submitting a comment, you agree to these Terms of Use.
|