2

I'm developing an application using JavaFx in which I'm creating dynamic TextFields inside a GridPane and there is a Button which is initially disabled like this:

enter image description here

So what I want is if Column 1 TextFields values are less than Column 3 TextFields values, button should be enable like this:

enter image description here

But let say if any of Column 3 TextField value become less than Column 1 TextField value of same row, it should disable button and show that specific TextField border in red color and when hover mouse over that field should display some warning:

enter image description here

I'm creating TextField like this:

public static GridPane table(int rows){
            GridPane table = new GridPane();

            for(int i=0; i<rows; i++){
            TextField textField1 = new JFXTextField();
            textField1.setAlignment(Pos.CENTER);
            TextField textField2 = new JFXTextField();
            textField2.setAlignment(Pos.CENTER);
            TextField textField3 = new JFXTextField();
            textField3.setAlignment(Pos.CENTER);

            //add them to the GridPane
            table.add(textField1, 0, i+1);
            table.add(textField2, 1, i+1);
            table.add(textField3, 2, i+1);
         }
        return table;
    }

After that I'm creating another method to return component from table at specific row and column like this:

public static Node getComponent (int row, int column, GridPane table) {
         for (Node component : table.getChildren()) { // loop through every node in the table
             if(GridPane.getRowIndex(component) == row && 
                             GridPane.getColumnIndex(component) == column) {
                 return component;
             }
         }

         return null;
     }

I tried to do like this but it's not working (Here I'm converting values into string and comparing just for the check):

private boolean isTextEqual(GridPane table, Button button){

        for(Node node : table.getChildren()){ 
            if(node instanceof TextField){
                for(int i=1 ; i<=ComboBox().getValue(); i++){
                    String str = ((TextField)DynamicGridpanes.getComponent (i, 0, table)).getText();
                    ((TextField)DynamicGridpanes.getComponent (i, 2, table)).textProperty().addListener((obs, old, newV)->{ 
                    if(newV.toString()==str){
                        button.setDisable(false);
                    }
                    else{
                        button.setDisable(true);
                    }
                 });
                    }
                }
            }

        return true;
    }
Junaid
  • 664
  • 5
  • 18
  • 35
  • Do you still have the `DatePicker` and `CheckBox` in your table? – Yahya Jun 01 '17 at 16:08
  • Yes @Yahya I've. I posted it like to simplify the question – Junaid Jun 01 '17 at 16:10
  • So basically you have THREE columns of `TextFields` (followed by each other) and them the `CheckBoxs` then the `DatePickers` ? And is every `TextField` Columns in different `GridPane`.. how many `GridPane` do you have? – Yahya Jun 01 '17 at 16:12
  • 1
    `if(newV.toString()==str)` is not the correct way to compare Strings. See https://stackoverflow.com/questions/513832/how-do-i-compare-strings-in-java. – VGR Jun 01 '17 at 16:19
  • Yes @Yahya they all are in one GridPane like in this sequence TextField->CheckBox->DatePicker->TextField – Junaid Jun 01 '17 at 16:23

2 Answers2

1

You can create the bindings that do the validation when you create the text fields. This will avoid the need to navigate through the grid pane's child nodes, which doesn't seem very robust.

Declare an array of boolean bindings (there will be one for each row):

private BooleanBinding[] rowValidationBindings ;

Then you can do

public static GridPane table(int rows){
    GridPane table = new GridPane();

    rowValidationBindings = new BooleanBinding[rows];

    for(int i=0; i<rows; i++){
        TextField textField1 = new JFXTextField();
        textField1.setAlignment(Pos.CENTER);
        TextField textField2 = new JFXTextField();
        textField2.setAlignment(Pos.CENTER);
        TextField textField3 = new JFXTextField();
        textField3.setAlignment(Pos.CENTER);

        rowValidationBindings[i] = Bindings.createBooleanBinding(
            () -> {
                if (textField1.getText().matches("\\d+") &&
                    textField3.getText().matches("\\d+")) {
                    int value1 = Integer.parseInt(textField1.getText());
                    int value3 = Integer.parseInt(textFIeld3.getText());
                    return value3 > value1 ;
                } else {
                    return false ;
                }
            }, textField1.textProperty(), textField2.textProperty()
        );

        //add them to the GridPane
        table.add(textField1, 0, i+1);
        table.add(textField2, 1, i+1);
        table.add(textField3, 2, i+1);
    }

    button.disableProperty().bind(Bindings.createBooleanBinding(
        () -> ! Stream.of(rowValidationBindings).allMatch(BooleanBinding::get),
        rowValidationBindings
    ));

    return table;
}

You can also add the styling to the text field directly in the for loop:

textField3.styleProperty().bind(Bindings
    .when(rowValidationBindings[i])
    .then("")
    .otherwise("-fx-border-color: red")); // or whatever you are using for style

and for tooltips:

Tooltip tooltip = new Tooltip();
tooltip.textProperty().bind(Bindings.concat("Value must be greater than ",textField1.textProperty()));
textField3.tooltipProperty().bind(Bindings
    .when(rowValidationBindings[i])
    .then((Tooltip)null)
    .otherwise(tooltip));
James_D
  • 201,275
  • 16
  • 291
  • 322
  • To be honest, I would hugely refactor this code, e.g. create a class that represented each row, with methods for adding to a grid pane and retrieving the values. Then just expose an `ObservableBooleanValue valid` from that class (along with methods for extracting the values). Then you can keep a `List` of instances of that class, and it would be easy to create the binding for the button. – James_D Jun 01 '17 at 16:39
  • Can you please elaborate a little more what do you mean by this "create a class that represented each row" – Junaid Jun 02 '17 at 07:32
  • @Junaid well you don't have to do that, it would just simplify your code. I just meant create a class with three text fields, a field for "valid" and the methods you need (e.g. `void addToGridPaneRow(int row)`, `String getText1()`, etc). But the solution posted should work. – James_D Jun 02 '17 at 09:05
  • Here `button.disableProperty().bind(Bindings.createBooleanBinding( () -> ! Stream.of(rowValidationBindings).allMatch(BooleanBinding::get)), rowValidationBindings );` eclipse is throwing this error `The method bind(ObservableValue extends Boolean>) in the type Property is not applicable for the arguments (BooleanBinding, BooleanBinding[])` – Junaid Jun 07 '17 at 14:10
  • There is just a missing `)`: I fixed it. – James_D Jun 07 '17 at 14:13
  • Thanks. and another thing If I want to get rid of `public static Node getComponent()` and I use your example so than how can I get values from TextFields ? – Junaid Jun 07 '17 at 14:42
  • Either retain the method, or you could put the text fields in an array when you create them, or you could refactor it as described above. – James_D Jun 07 '17 at 14:43
  • Wouldn't this whole thing be better with a `TableView`? – James_D Jun 07 '17 at 14:44
  • I've no idea of `TableView` I just started learning JavaFx. Idk `TableView` will fulfill the purpose or not. – Junaid Jun 07 '17 at 14:47
  • I'm trying to understand your code, can you please explain me what this part of code is doing `Stream.of(rowValidationBindings).allMatch(BooleanBinding::get), rowValidationBindings` – Junaid Jun 07 '17 at 15:17
  • @Junaid Please read the Javadocs for the methods that are called. – James_D Jun 07 '17 at 15:25
  • Last thing I want to ask, If I've two separate GridPanes and I want to apply validation on both of the GridPane's nodes like in your above example. So lets say both GridPane's nodes should've numbers and not alphabets and they should not be empty and bind it with the button's disableProperty. So disable the button if any node from any of both GridPane violates the rule. How can I achieve this. At the moment I'm iterating through eachnode and using this method private static boolean isAllFilled(GridPane tableA, `GridPane tableB)`. – Junaid Jun 07 '17 at 15:50
1

Actually it's not that easy to do what you want, because the code you have needs to be refactored (the code is not meant to do such advanced requirements but it's fine for the basic requirements you have). However, you can do something like this:

First, define a global variable to be updated with the last row index of the invalid TextField (From here you shall conclude that this will change the border color for ONE invalid TextField at a time):

public static int textFieldIndex = -1;

Now with the help of the method you already have getComponent (int row, int column, GridPane table), create another static method to check if ALL TextFields have Valid Values at one time:

/**
* This method to check at run time with every change in any TextField 
* if the corresponding TextField has a valid value(i.e contains number and
* the first TextField value is less than the second)
* @param table
* @param numRows
*/
private static boolean hasValidValue(GridPane table, int numRows){
   // cycle through every row in the table
   // and compare every two TextFields
   for(int i=0; i<numRows; i++){
      try{ // try because user may enters a non-number input (to avoid crash)
         // the first TextField is always at column index 0 , the second at column index 3
         if(Integer.parseInt(((TextField)(getComponent (i, 0, table))).getText())>
            Integer.parseInt(((TextField)(getComponent (i, 3, table))).getText())){
            // before returning false
            textFieldIndex = i; // update at which row the TextField is less
            return false;
          }
       }catch(NumberFormatException e){ // if it contains invalid input(non-digit)
            return false;
       }
   }
   return true; 
}

Now you need to use the above method in the validateTable() method and do some adjustments:

// pass the comboBox.getValue() to the third parameter
private void validateTable(GridPane table, Button button, int numRows) {

   for(Node textField : table.getChildren()){ 
      if(textField instanceof TextField){
         ((TextField)textField).textProperty().addListener((obs, old, newV)->{
           // first of all remove the red border from the invalid TextField (if any)
          // we know that via textFieldIndex which should be -1 if there is no lesser
          // actually it's a pain 
          if(textFieldIndex!=-1){
            ((TextField) getComponent(textFieldIndex, 3, table)).setStyle("");
          }
          if(isAllFilled(table)){ // if all filled ( you already have this method)
             if(hasValidValue(table,numRows)){ // check for validity
                button.setDisable(false); // then make the button active again
             }
             else{// if it's not a valid value
                  // re-style the TextField which has lesser value
                 ((TextField) getComponent(textFieldIndex, 3, table)).
                                        setStyle("-fx-border-color: red;");
                  button.setDisable(true); 
             }
          }
          else{
               button.setDisable(true);
          }
       });
     }  
   }
}

Now in your tabPane ChangeListener add the third para to the method (because you already have it you need just to add the value of ComboBox:

tabPane.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Tab>(){
   ....
   ....
   ....
   // I think you have here anchorPane not containerB in the original code
   validateTable((GridPane) containerB.getChildren().get(0), test, comboBox.getValue());
}

Test

Test

Yahya
  • 13,349
  • 6
  • 30
  • 42
  • @Junaid Is everything ok? – Yahya Jun 01 '17 at 21:29
  • Idk @Yahya but "hasValidValue" is always returning true – Junaid Jun 02 '17 at 07:32
  • Yes @Yahya it's working now but it's showing red border one at a time like if there are more values in second column lesser then first column it's not showing red border for all of them, so can we achieve something like this ? it's not that much big issue but If is implementable it'll be better. – Junaid Jun 02 '17 at 09:22
  • @Junaid My friend I mentioned at the beginning of my answer *"From here you shall conclude that this will change the border color for ONE invalid TextField at a time"*, and you can see how it works in the `gif` I provided. The thing is the program validating against **3** things together (if empty `TextField`, if contains a number and if the third column contains a lesser value than the corresponding one in the first column) so to achieve what you want, you need to make all `TextFields` red bordered until the user inserts something ***fully*** valid. But as I (I'll continue in second comment) – Yahya Jun 02 '17 at 14:10
  • @Junaid But as I see it, it's still ok because it will point to the invalid `TextField` to be fixed by user one by one. so if the user has two invalid `TextFields`, it will deactivate the button and ask him to fix the first `TextField` then when he does that, it asks him to fix the second. Exactly as it's shown in the `gif` image above :) – Yahya Jun 02 '17 at 14:12
  • Yes @Yahya you're right it fulfills the requirement, thanks my friend for always helping and guiding me :) – Junaid Jun 02 '17 at 21:48
  • [Question](https://stackoverflow.com/questions/44413649/javafx-how-to-update-text-of-dynimically-created-textfields-inside-gridpane) Yahya can you please look into this. – Junaid Jun 07 '17 at 13:14