JavaFX: Template for resizable CustomNode

Creating a CustomNode is very easy. A template is unnecessary. But when this CustomNode shall be Resizable things start to become a little bit more complicated.

Therefore I have created a template that can be used:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class MyCustomNode extends CustomNode, Resizable {
  override var width on replace { requestLayout() }
  override var height on replace { requestLayout() }
  override var layoutBounds = bind BoundingBox {
        width: this.width
        height: this.height
    }  

  init {
    children =[
      Rectangle {
        width: bind width
        height: bind height      
      }
    ]
  }

  override function getPrefWidth( number ) {
    //return your pref width - hard coded values are ok for most cases
  }

  override function getPrefHeight( number ) {
    //return your pref height - hard coded values are ok for most cases
  }

  /*
  //It is not absolutly necessary to override those methods, but strongly recommended.
  override function getMaxWidth() {
    //Return the max width
  }

  override function getMaxHeight() {
    //Return the max height
  }

  override function getMinWidth() {
    //Return the min width
  }

  override function getMinHeight() {
    //Return the min height
  }*/

}

There is a post containing a template for CustomControls here.

JavaFX: Making a CustomNode resizable

Creating a CustomNode is quite easy. There are no gotchas, extending CustomNode and adding some children is enough.
But of course that node is not resizable…

So this post shows what is necessary to create a CustomNode that extends Resizable. We follow the same setting: The custom node is placed within a Stack.

Step 1: Extending Resizable

Resizable enforces us to implement the getPrefWidth/Height methods. We just return some hard coded values and try to find out what happens:
The width/height of the CustomNode are set to the values returned by getPrefWidth/Height. But the boundsInLocal and layoutBounds don’t respect that. They still just depend on the children.

Step 2: Requesting Layout

The doc of Resizable tells us that we have to call requestLayout() every time the width/height of the node is changed. Doing this doesn’t make any difference in this context (within a Stack). But when placed in other layouts this is necessary!

1
2
 override var width on replace { requestLayout() }
  override var height on replace { requestLayout() }

Step 3: Connecting layoutBounds to width/height

As we have learned when looking at CustomControls it is a very good idea to bind the layoutBounds to the width/height. This is necessary to avoid difficult layout bugs that will only occur in some situations and are very hard to track down.

1
2
3
4
5
6
 override var layoutBounds = bind BoundingBox {
         minX: 0
         minY: 0
         width: this.width
         height: this.height
     }

Now the layoutBounds fit to the width/height of the Node. But of course the sizes of the rectangle(s) are still independent. And the boundInLocal don’t fit them neither.

Step 4: Children depend on width/height

Now we connect the width/height of the children to the width/height of our CustomNode.
Now the three values (width/height, layoutBounds, boundsInLocal) correspond correctly with each other.

Important: As mentioned before it is necessary that width/height always correspond with the layoutBounds. Always! But the boundsInLocal may be different from them (larger or smaller) but have to be influenced by them.

JavaFX: How to extend CustomNode properly

I have taken a look at custom controls before. Now it is time to examine how to extend CustomNodes.

In this post I take the same steps as before.

Empty CustomNode

Since 1.3 it is no longer necessary to implement any methods. So it is possible to create and use an empty class.
As expected the layoutBounds and boundsInLocal both have a width/height of 0.

CustomNode with a Rectangle

We add a Rectangle to our custom node. In the docs there are two possible ways described.

Overriding children var

The new, recommended way is to override the children var.
This way works as expected. The layoutBounds and boundsInLocal correspond to the bounds of the rectangle.

Overriding create()

This has been the old way to add children to a CustomNode. We just return the same rectangle.
And it works as expected.
(By the way: The default implementation of create() returns null).

Assigning children in init()/postinit()

But when there is a children var, why not simply assigning the children within the init method? So let’s try: Yes – works as it should. Bounds are calculated correctly. So I can’t tell you why the doc does not mention that way….

Adding two rectangles

No we add two rectangles as children that have different sizes and locations. The bounds contain both rectangles. This fact approves that a CustomNode has the same behavior as a Group.

Adding effects

Adding an effect to the rectangle

Adding an effect (e.g. DropShadow) to the Rectangle changes both bounds. So CustomNode has the same behavior as a Group.

Adding an effect to the CustomNode itself

Adding an effect (e.g. DropShadow) to the CustomNode itself only changes the boundsInLocal. The layoutBounds only depend on the children. While this is the expected (and documented) behavior, it is good to see that it has been implemented correctly…

New JavaFX default font (Amble Condensed) looks ugly!

Updates: Screenshots created on Mac/Windows added.

With the 1.3 relase of JavaFX there has been introduced a new default font: Amble Condensed.

The default font in 1.2 has been Dialog 12pt.
But that decision has some disadvantages – at least on Linux…. But one screen shot tells more than a thousand words:
fx-fonts

You can easily spot the difference…

When using Dialog a font size of 9 can easily be used. Maybe 8 if really necessary…  The smallest usable size with Amble seems to be 12 (that is the default) – maybe, if really necessary 11…

But even the default size of 12 is very problematic. I didn’t want to read long texts drawn in Amble 12…

Is there anybody that could run those tests on Windows and post a screen shot?
Windows Screenshot can be found down there…

Code for my demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.text.Text;
import javafx.scene.text.Font;
import javafx.scene.layout.VBox;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.layout.Stack;
import javafx.scene.shape.Rectangle;
import javafx.scene.Node;

def colors = [ Color.BLACK, Color.WHITE ];

Stage {
  title: "Application title"
  scene: Scene {
    width: 800
    height: 600
    content: [
      VBox {
        var stack:Stack;

        content: [
          for ( color in colors ) {
            stack = Stack {
              content: [
                Rectangle{
                  width: bind stack.width
                  height: bind stack.height
                  fill:color
                }

                HBox {
                  content: [
                    for ( font in [ Font.DEFAULT.name, "Dialog" ] ) {
                      VBox {
                        content: [
                          for ( size in [ 6..20 ] ) {
                            Text {
                              fill: colors[(indexof color +1) mod colors.size()]
                              font: Font {
                                size: size
                                name: font
                              }
                              content: "{font} with size { size }"
                            }
                          }
                        ]
                      }

                    }
                  ]
                }
              ]
 }
          }
        ]
      }
    ]
  }
}

println( "Default font: {Font.DEFAULT}" );

UPDATE:
John Tonkin send be a screen shot made using Windows:

default-font-windows

It doesn’t look any better…

And here is a shot created on a Mac by Antoine Mischler. Does it look different?
mac

Conclusion

The screenshots created on Linux and Windows look nearly the same (for “Amble Condensed”). The one created on the Mac looks a little bit better (look at 11pt)…
But be careful! The bounds are a different on Mac than on Windows/Linux (e.g. at 16pt)…

JavaFX Light Bulb with improved UI performance

Update: JNLP file has been fixed. Should work now (me == stupid)…

I am sure most of you have seen the nice demo posted by Mark Anro Silva.

While this is a very nice demo it has a performance problem related to the slider that is very common in JavaFX.
The effectOpacity of the LightBulbEffect is bound to the value of the slider. While this is the correct logic, we run into problems, if those values are updated too often.
A slider is able to create several updates/events every second. When expensive effects are calculated every time, we will get a UI that is not very responsive (and feels slow).

Therefore it is a good idea to not to bind expensive actions to UI triggers directly. Instead you could use a timeline to delay the action for some milliseconds.

For that purpose I have created a DelayedAction.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.cedarsoft.fx;

import javafx.animation.Timeline;
import javafx.animation.KeyFrame;

/**
 * A DeplayedAction.
 * This class can be used to delay actions. This is especially useful if used with sliders or other UI components
 * that update its values very fast.
 * When binding long running / processor intensive actions to those variables your application will start to feel
 * unresponsive.<p/>
 *
 * If maxDelay is set, the action is executed at least {@link #maxDelay} after the call of {@link #schedule()}.
 */

public class DelayedAction {

  /**
   * The action that is called after the delay has passed.
   */

  public-init var action: function(): Void;
  /**
   * The delay
   */

  public-init var delay: Duration = 50ms;
  /**
   * The maximum deplay the action is called after.
   * If the max delay
   */

  public-init var maxDelay: Duration;
  public-read def maxDelaySet = bind maxDelay > 0ms on replace {
    if ( not maxDelaySet ) {
      maxDeplayTimeLine.stop();
    }
  };
  /**
   * if set to true every call to {@link #schedule} will executed immediately.
   * Pending requests will be executed immediately.
   */

  public var executeImmediately: Boolean on replace {
    if ( executeImmediately and timeline.running or maxDeplayTimeLine.running ) {
      execute();
    }
  };
  def timeline: Timeline = Timeline {
      keyFrames: [
        KeyFrame {
          time: bind delay
          action: function () {
            execute();
          }
        }
      ]
    }
  def maxDeplayTimeLine: Timeline = Timeline {
      keyFrames: [
        KeyFrame {
          override var time= bind maxDelay on replace {
            maxDeplayTimeLine.evaluateKeyValues();
          }
          action: function () {
            execute();
          }
        }
      ]
    }

  /**
   * The action is called after the given delay. Every call to schedule restarts the delay.
   *
   */

  public function schedule() {
    if ( executeImmediately ) {
      execute();
      return ;
    }
    if ( maxDelaySet ) {
      maxDeplayTimeLine.play();
    }
    timeline.playFromStart();
  }

  /**
   * Executes the action immediately.
   */

  public function execute(): Void {
    maxDeplayTimeLine.stop();
    timeline.stop();
    action();
  }
}

That action is part of the fx.commons-library (snapshot can be downloaded from
http://nexus.cedarsoft.com/content/groups/public-snapshots/com/cedarsoft/fx/commons/1.0.0-SNAPSHOT/).

How to use DelayedAction

The usage of the DelayedAction is quite simple. Instead of directly binding the variable, you assign the var within a delayed action.

1
2
3
4
5
6
7
8
9
def lightOpacitySetAction: DelayedAction =  DelayedAction {
  delay: 50ms
  maxDelay: 500ms
  executeImmediately: true //this is only necessary for this demo. Will be changed to false as soon as the slider becomes visible
   action: function () {
    println( "---> SETTING NEW VALUE! {slider.value}" );
    lightOpacity2 = slider.value;
  }
};

That action is triggered if the value of the slider is changed. Therefore we introduce a temporary var.

1
2
3
var tmp = bind slider.value on replace{  
  lightOpacitySetAction.schedule();
}

Of course it is necessary to remove the binding of lightOpacity2 to slider.value.

Updated Demo

I have updated the demo application accordingly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
package com.cedarsoft.fx;

import javafx.scene.shape.QuadCurveTo;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.image.ImageView;
import javafx.scene.Group;
import javafx.scene.shape.Rectangle;
import javafx.scene.paint.Color;
import javafx.scene.effect.GaussianBlur;
import javafx.scene.effect.Glow;
import javafx.scene.shape.Path;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.control.Slider;
import javafx.scene.control.Label;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.CustomNode;
import javafx.animation.Timeline;
import javafx.animation.KeyFrame;
import javafx.animation.Interpolator;
import java.lang.UnsupportedOperationException;

function run(_ARGS:String[ ]){
  var buttonstext ="On" on replace {
    if ( buttonstext == "Off" ) {
      lightOpacity1 = .1;
      lightOpacity3 = .3;
      lightOpacity4 = 1;
      controlVisible = true;
      slider.value = .3;
      lightOpacitySetAction.executeImmediately = false;
    } else {
      lightOpacity1 = 0;
      lightOpacity3 = 0;
      lightOpacity4 = 0;
      controlVisible = false;
      lightOpacitySetAction.executeImmediately = true;
      slider.value = 0;
    }
  };

var lightOpacity1: Number = 0;
var lightOpacity2: Number = 0;
var lightOpacity3: Number = 0;
var lightOpacity4: Number = 0;
var lightbulbImg = ImageView {
     image: Image {
       url: "{__DIR__}resources/lightbulb.jpg"
    }
  }


def lightOpacitySetAction: DelayedAction =  DelayedAction {
  delay: 50ms
  maxDelay: 500ms
  executeImmediately: true
   action: function () {
    println( "---> SETTING NEW VALUE! {slider.value}" );
    lightOpacity2 = slider.value;
  }
};


var controlVisible = false;
var effects = Group {
     content: [
      Rectangle {
         width: 412
        height: 520
        fill: Color.YELLOW
        opacity: bind lightOpacity1
      }
      LightBulbEffect {  effectOpacity: bind lightOpacity2 }
      LightBulbEffect {  effectOpacity: bind lightOpacity3 }
      Rectangle {
         effect: GaussianBlur {
           radius: 10
          input: Glow { }
        }
        translateY: 178
        translateX: 165
        width: 65
        height: 8
        arcWidth: 8
        arcHeight: 8
        fill: Color.WHITE
        stroke: Color.ORANGE
        strokeWidth: 1.8
        opacity: bind lightOpacity4
      }
      Path {
         effect: GaussianBlur {
           radius: 1
          input: Glow { }
        }
        stroke: Color.WHITE
        strokeWidth: 1.8
        opacity: bind lightOpacity4
        elements: [
          MoveTo {  x: 172 y: 183 },
          LineTo {  x: 183 y: 182 },
          QuadCurveTo {
             controlX: 198 controlY: 183
            x: 213 y: 182
          }
          QuadCurveTo {
             controlX: 214 controlY: 183
            x: 225 y: 185
          }
        ]
      }
    ]
  }
var togglebutton = Button {
     translateY: 350
    translateX: 260
    text: bind buttonstext
    onMousePressed: function ( ev: MouseEvent ): Void {
      if ( buttonstext == "Off" ) {
        buttonstext = "On";
      } else {
        buttonstext = "Off";
      }
    }
  }
var slider: Slider = Slider {
    translateY: 420
    translateX: 128
    max: .4
    min: 0
    visible: bind controlVisible
  }

var tmp = bind slider.value on replace{  
  lightOpacitySetAction.schedule();
}

var controls = Group {
     content: [
      togglebutton,
      slider,
      Label {  translateX: 120 translateY: 419 text: "-" visible: bind controlVisible }
      Label {  translateX: 268 translateY: 419 text: "+" visible: bind controlVisible }
    ]
  }

Stage {
   title: "JavaFX Light Bulb"
  scene: Scene {
     width: 402
    height: 499
    fill: Color.BLACK
    content: [
      lightbulbImg,
      effects,
      controls
    ]
  }
}
}


public class LightBulbEffect extends CustomNode {

    public var effectOpacity: Number;

    override function create() {
        Path {
            translateX: -10
            translateY: -8
            effect: GaussianBlur {input: Glow {level: 1} radius: 63}
            fill: Color.YELLOW
            stroke: Color.YELLOW
            strokeWidth: 5
            opacity: bind effectOpacity
            elements: [
                MoveTo {x: 127 y: 132},
                QuadCurveTo {
                    controlX: 206 controlY: 48
                    x: 287 y: 132
                }
                QuadCurveTo {
                    controlX: 320 controlY: 180
                    x: 295 y: 230
                }
                QuadCurveTo {
                    controlX: 290 controlY: 240
                    x: 270 y: 280
                }
                QuadCurveTo {
                    controlX: 270 controlY: 295
                    x: 260 y: 320
                }
                LineTo {x: 153 y: 320}
                QuadCurveTo {
                    controlX: 143 controlY: 295
                    x: 143 y: 280
                }
                QuadCurveTo {
                    controlX: 118 controlY: 240
                    x: 115 y: 230
                }
                QuadCurveTo {
                    controlX: 95 controlY: 180
                    x: 127 y: 132
                }
            ]
        }
    }
}

JavaFX: Transparency and Linux

There have been a few posts regarding transparency under Linux. I have put that code together so that it can be used directly. This works for me:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public def osName = FX.getProperty( "javafx.os.name" );
public def IS_LINUX = osName.contains( "inux" );
public def IS_MAC = osName.contains( "mac" );
public def IS_WINDOWS = osName.contains( "indows" );
public def IS_SOLARIS = osName.contains( "olaris" );

/**
 * Is the version of the running JDK at least major.minor.micro_update?
 * In 1.6.0_18 macro=1,minor=6,micro=0,update=18
 */

public function jdkAtLeast( macro: Integer, minor: Integer, micro: Integer, update: Integer ): Boolean {
  def runtimeVersion = java.lang.System.getProperty( "java.runtime.version" );  
  def pattern = java.util.regex.Pattern.compile( "^(\\d)\\.(\\d)\\.(\\d)_(\\d+)-" );
  def matcher = pattern.matcher( runtimeVersion );
  if ( matcher.find() ) {
    def currentMacro = Integer.valueOf( matcher.group( 1 ) );
    def currentMinor = Integer.valueOf( matcher.group( 2 ) );
    def currentMicro = Integer.valueOf( matcher.group( 3 ) );
    def currentUpdate = Integer.valueOf( matcher.group( 4 ) );
    if ( currentMacro < macro or currentMinor < minor or currentMicro < micro or currentUpdate < update ) {
      return false;
    }
    }
  true
  }

public function fixTransparency() {
  if ( IS_LINUX and jdkAtLeast( 1, 6, 0, 14 ) ) {
    java.lang.System.setProperty( "javafx.allowTransparentStage", "true" );
  }
}

JavaFX: Bug in Timeline(?!)

The behavior of the Timeline has changed for performance reasons in 1.3. Now the sequence containing the KeyFrames is only recalculated/updated if you call evaluateKeyValues manually.

But also the behavior has changed in another way:
I have a Timeline that contains an optional KeyFrame. When looking at the docs the timeline should simply skip a KeyFrame if the time is negative:

KeyFrames having time < 0s will be ignored by the Timeline.

Exactly what I am looking for. But never believe the documentation:

1
2
3
4
5
6
7
8
9
10
  Timeline {
      keyFrames: [
        KeyFrame {
          time: -1ms
          action: function () {
            throw new UnsupportedOperationException("aaah!");
          }
        }
      ]
    }.playFromStart();

This small snippet throws the exception… Any ideas or comments?

JavaFX 1.3: Template for custom controls

Here is a small template that helps you to create your own custom control without any problem:

Just paste this code into your fx file and rename the classes using the IDE you prefer…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class MyOwnControl extends Control {
  init {
    if ( skin == null ) {      
      skin = MyOwnControlSkin {};
    }
  }
}

public class MyOwnControlSkin extends Skin {
  def myBehavior = bind behavior as MyOwnBehaviour;
  def myControl = bind control as MyOwnControl;    

  init {
    behavior = MyOwnBehaviour{};

    node =  Rectangle {
      width: bind control.width
      height: bind control.height      
    }
  }

  override function getPrefWidth( number ) {
    //return your pref width - hard coded values are ok for most cases
  }

  override function getPrefHeight( number ) {
    //return your pref height - hard coded values are ok for most cases
  }

  /*
  //It is not absolutly necessary to override those methods, but strongly recommended.
  override function getMaxWidth() {
    //Return the max width
  }

  override function getMaxHeight() {
    //Return the max height
  }

  override function getMinWidth() {
    //Return the min width
  }

  override function getMinHeight() {
    //Return the min height
  }*/


  override function contains( localX: Number, localY: Number ): Boolean {
    node.contains( localX, localY );
  }

  override function intersects( localX: Number, localY: Number, localWidth: Number, localHeight: Number ): Boolean {
    node.intersects( localX, localY, localWidth, localHeight );
  }
}

public class MyOwnBehaviour extends Behavior {
  def myControl = bind skin.control as MyOwnControl;
}

JavaFX: Custom controls in 1.3

The 1.3 release of JavaFX is a huge on. Many things have been improved. Also some incompatible API changes occured.

Creating Custom controls has been really hard in 1.2. I have created a template that did what it should. But with the 1.3 release this template does no longer work.

So here we go and look at the issues related to custom controls one more time…. It is the same setup: The custom control is placed within a Stack.

Empty control

We start with an empty control without a skin. The result is the same as in 1.2: Everything is 0.

Simple Control with and empty Skin

Now we create a control and assign an empty skin. This is done within the init-block since createNode no longer exists. This results in the same behavior as in 1.2: There are hard coded values for prefWidth/height that are queried by the stack, set as width/height of the custom control and reflected by the layoutBounds. The boundsInLocal are still (0/0/0/0).

The Stack implementation has been changed. In 1.2 the Stack resized all nodes that implement Resizable to fill the available space. The default behaviour in 1.3 is to use the prefWidth/height.
The old behavior can be achieved by overwriting getHFill()/getVFill.

Skin containing just a Rectangle

Skin with RectangleThe skin returns a Rectangle {widht:70 height:150}. The layoutBounds are not affected in any way. But the boundsInLocal are bound to the bounds of the node.

The result looks different than in 1.2: But just because the control is not resized! As a result the rectangle is drawn somewhere near the middle (but not in the middle). The position of the upper left border of the rectangle is placed by the Stack using the layoutBounds (drawn in red) as initial value for calculation.

So the result might be a little bit “dangerous”: For skins that have boundsInLocal that are near the hard coded values for prefWidth/height (and therefore width/height and therefore layoutBounds) the layout might be “good enough”. So the missing connection between width/height and boundsInLocal could be overlooked…

Skin with prefWidth/Height methods

The implementation of the prefWidth/Height methods is crucial – as written before. It has the expected results: The width/height of the control (and as result the layoutBounds) are changed accordingly by the container.

Connecting the width/height of the rectangle

As written before it is necessary to bind the width/height of the rectangle to the width/height of the control. When this is done the result looks as expected.

Conclusion

Creating custom controls in JavaFX is still quite difficult. And the documentation is still very poor. The template has to be changed slightly (due the removal of the create method). I will do this after the weekend.

The default of the Stack has been changed (and I like that). So it uses the prefWidth/Height per default. While this is the better behavior (imho) it might hide the fact that you have forgotten to implement the getPrefWidth/Height methods.

Please Oracle: Make those methods abstract asap!

JavaFX 1.3: Binding improved

Binding is one of the biggest strengths of JavaFX. I really like that.

And now – with release 1.3 – it seems to work much better. At least some of the simplest cases now work as expected.

The main difference is, that now bindings are lazy by default. They are not evaluated every time a var has changed. This is a big performance improvement for many cases.

Return top
 
12 visitors online now
12 guests, 0 members
Max visitors today: 19 at 06:08 am CEST
This month: 24 at 07-06-2010 06:16 pm CEST
This year: 54 at 05-11-2010 04:53 pm CEST
All time: 54 at 05-11-2010 04:53 pm CEST

Switch to our mobile site