7 Replies Latest reply: Dec 11, 2012 6:15 PM by jsmith RSS

    Can't update GridPane from Button Action

    936835
      I'm making a simple gui for Conway's Game of Life with a grid (GridPane) and a simple toolbar with Start/Stop buttons. My start button action looks like this:
      startButton.setOnAction(new EventHandler<ActionEvent>() {
              @Override
              public void handle(ActionEvent actionEvent) {
                  grid.change();
              }
          });
      So when i click it the grid updates ones as i expect. But i when i add something like a Service/Platform.runLater/Runnable inside it evaluates generations, grid model, but refuses to update the grid view. My Service code looks like this:
       
      private Service task = new Service(){
          @Override
          protected Task createTask() {
              System.out.println("Task created");
              return new Task<Void>() {
                  @Override
                  protected Void call() throws Exception {
                      for (int i = 0; i < 3; i++) {
                          System.out.println("calling");
                          Platform.runLater(new Runnable(){
                              @Override
                              public void run() {
                                  System.out.println("running");
                                 grid.changeGeneration();
                                  try {
                                      Thread.sleep(1500L);
                                  } catch (InterruptedException e) {
                                      e.printStackTrace();  
                                  }
                              }
                          });
                      }
                      return null;
                  }
              };
          }
      };
      It prints Task created calling calling calling running running running to the console, but doesn't update the GridPane after each running, only after everything is done it updates it (in this case after the third "running"). Where am i making a mistake and how to fix this. I don't want to use Swing =)

      Same question on [http://stackoverflow.com/questions/13820734/cant-update-gridpane]

      Edited by: Riddle on 11.12.2012 7:21
        • 1. Re: Can't update GridPane from Button Action
          James_D
          I think you need to call Thread.sleep(...) from outside the Platform.runLater(...). It's usually a very bad idea to sleep on the FX Application thread, but I think you logically want this anyway:
          private Service task = new Service(){
              @Override
              protected Task createTask() {
                  System.out.println("Task created");
                  return new Task<Void>() {
                      @Override
                      protected Void call() throws Exception {
                          for (int i = 0; i < 3; i++) {
                              System.out.println("calling");
                              Platform.runLater(new Runnable(){
                                  @Override
                                  public void run() {
                                      System.out.println("running");
                                     grid.changeGeneration();
                                  }
                              });
                              Thread.sleep(1500L);
                          }
                          return null;
                      }
                  };
              }
          };
          • 2. Re: Can't update GridPane from Button Action
            936835
            No, moving Thread.sleep() to call() method, doesn't change anything. And how then i can delay rendering grid between generations?
            • 3. Re: Can't update GridPane from Button Action
              James_D
              It'll pause because you invoke a delay between submitting the Runnables to the FX Application Thread.

              I can't answer why it's still coalescing those calls without knowing more about what your grid.changeGeneration() method does. Here's a very simple example of the same structure which updates a label three times, with the 1.5 second pause you want:
              import javafx.application.Application;
              import javafx.application.Platform;
              import javafx.concurrent.Service;
              import javafx.concurrent.Task;
              import javafx.event.ActionEvent;
              import javafx.event.EventHandler;
              import javafx.scene.Scene;
              import javafx.scene.control.Button;
              import javafx.scene.control.Label;
              import javafx.scene.layout.HBox;
              import javafx.stage.Stage;
              
              public class SimpleCounter extends Application {
              
                @Override
                public void start(Stage primaryStage) {
                  final HBox root = new HBox(5);
                  final Button start = new Button("Start");
                  final Label label = new Label("Press Start to start...");
                  root.getChildren().addAll(start, label);
                  final Service<Void> task = new Service<Void>() {
                    @Override
                    protected Task<Void> createTask() {
                      return new Task<Void>() {
                        @Override
                        protected Void call() throws Exception {
                          for (int i = 0; i < 3; i++) {
                            System.out.println("Calling");
                            final int count = i;
                            Platform.runLater(new Runnable() {
                              @Override
                              public void run() {
                                System.out.println("Running");
                                label.setText("Generation: " + count);
                              }
                            });
                            Thread.sleep(1500);
                          }
                          return null;
                        }
                      };
                    }
                  };
                  start.setOnAction(new EventHandler<ActionEvent>() {
                    @Override
                    public void handle(ActionEvent event) {
                      task.start();
                    }
                  });
              
                  Scene scene = new Scene(root);
                  primaryStage.setScene(scene);
                  primaryStage.show();
                }
              
                public static void main(String[] args) {
                  launch(args);
                }
              }
              • 4. Re: Can't update GridPane from Button Action
                936835
                public void changeGeneration() {
                        Cell[][] newGen = new Cell[rows][cols];
                        Cell cell;
                        for (int i = 0; i < newGen.length; i++) {
                            for (int j = 0; j < newGen.length; j++) {
                cell = new Cell(destiny(cells, i, j));
                newGen[i][j] = cell;
                add(cell, i, j);
                }
                }
                curGen = newGen;
                }
                My grid class is a subclass of GridPane
                
                So when i click button it's working, but not from other Runnable
                
                Edited by: Riddle on 11.12.2012 9:11                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    
                • 5. Re: Can't update GridPane from Button Action
                  James_D
                  Assuming the surrounding logic and the layout is working, I don't see why this doesn't do what you want. You could conceivably be flooding the FX Application thread with all those calls, but I can't imagine any implementation of a single generation of Life at any reasonable size would take any significant time. So no clue. I don't know if it's possible to reduce this to a concise, complete executable which shows the same problem.

                  One kind of unrelated thing you should be aware of, though: GridPane will allow multiple nodes to coexist in the same space in the grid. Their z-orders are determined by the order in which they're added. So it looks like you are piling cells on top of cells at each generation (you're calling add and never calling remove); doing this you're going to run out of memory, and possibly slow the rendering down, at some point.

                  I think my strategy for this would be to create a new GridPane instance for each generation, then replace it in your overall layout. This is nice for the threading too, because you can generate the entire new grid in an application thread, then just insert it into the layout in a Platform.runLater(...) call. This will at a minimum increase responsiveness, and possibly keeping the FX Application Thread so much cleaner will fix the problem you're seeing.
                  • 6. Re: Can't update GridPane from Button Action
                    936835
                    Thanks for the idea.
                    I've moved cells rendering (clear them and then add) in outer method and now it is working as it should.
                    • 7. Re: Can't update GridPane from Button Action
                      jsmith
                      Although the Server/Task/RunLater combination works, I'd be inclined to use a Timeline for this and not have to worry about any kind of potential thread safety issues:
                      import javafx.animation.*;
                      import javafx.application.Application;
                      import javafx.beans.property.*;
                      import javafx.event.*;
                      import javafx.geometry.Insets;
                      import javafx.scene.Scene;
                      import javafx.scene.control.*;
                      import javafx.scene.layout.HBox;
                      import javafx.stage.Stage;
                      import javafx.util.Duration;
                       
                      public class SimpleCounterTransition extends Application { 
                        final Label label = new Label("Press Start to start...");
                        final LongProperty generationCount = new SimpleLongProperty(0);
                        
                        @Override public void start(Stage primaryStage) {
                          final Button start = new Button("Start");
                      
                          final Timeline timeline = 
                              new Timeline(
                                  new KeyFrame(Duration.ZERO, new EventHandler<ActionEvent>() {
                                      @Override public void handle(ActionEvent t) {
                                          nextGeneration();
                                      }
                                  }),
                                  new KeyFrame(Duration.millis(1000), (KeyValue) null)
                              );
                          timeline.setCycleCount(Timeline.INDEFINITE);
                          
                          start.setOnAction(new EventHandler<ActionEvent>() {
                            @Override public void handle(ActionEvent event) {
                              timeline.play();
                              start.setDisable(true);
                            }
                          });
                       
                          final HBox root = new HBox(5);
                          root.setPadding(new Insets(5));   
                          root.getChildren().addAll(start, label);
                          primaryStage.setScene(new Scene(root));
                          primaryStage.show();
                        }
                        
                        private void nextGeneration() {
                          label.setText("Generation: " + generationCount.get());
                          generationCount.set(generationCount.get() + 1);
                        }
                       
                        public static void main(String[] args) { launch(args); }
                      }