7 Replies Latest reply: Feb 8, 2013 8:32 AM by James_D RSS

    TableColumn.setCellValueFactory()

    MoxeeMan
      I have the following abbreviated class that provides data for a TableView:

      public class DataObjectCropChart {

      private     int               bins;

      public SimpleIntegerProperty propBinsProperty() {
      return new SimpleIntegerProperty(bins);
      }
      }

      I want to assign a cell value factory to my table column. I've been down a number of rabbit trails but nothing works. I don't understand a lot about generics so I don't know what to do with following. It appears to me to follow all the patterns of API docs and what I find on Google searches but is an error:

           Callback<CellDataFeatures<DataObjectCropChart, SimpleIntegerProperty>, ObservableValue<SimpleIntegerProperty>> binsproperty =
                new Callback<CellDataFeatures<DataObjectCropChart, SimpleIntegerProperty>, ObservableValue<SimpleIntegerProperty>>() {
                @Override
                public ObservableValue<SimpleIntegerProperty> call(CellDataFeatures<DataObjectCropChart, SimpleIntegerProperty> p) {
                     return p.getValue().propBinsProperty();
                }
           };     

      The error is with the return. Although SimpleIntegerProperty is an ObservableValue, Netbeans reports this as an error because it is not an ObservableValue<SimpleIntegerProperty>. How do I create such a thing to pass to my cell factory? What is the best practice for this use case?

      Thanks much for direction.
        • 1. Re: TableColumn.setCellValueFactory()
          James_D
          It took me a while to figure out how all this fit together too. After some practice it all makes sense.

          Short answer: make your TableColumn a
          TableColumn<DataObjectCropChart, Number>
          and your cell value factory looks like
          Callback<CellDataFeatures<DataObjectCropChart, Number>, ObservableValue<Number>> binsproperty =
            new Callback<CellDataFeatures<DataObjectCropChart, Number>, ObservableValue<Number>>() {
              @Override
              public ObservableValue<Number> call(CellDataFeatures<DataObjectCropChart, Number> p) {
                return p.getValue().propBinsProperty();
              }
            };
          (There's an even easier way, but you have to read the long answer first.)


          Long answer:

          Start with the TableView. TableView is generic, so it's declared as a
          TableView<S>
          where S stands for "the type of object in each row". So in your example you should have something like
          TableView<DataObjectCropChart> table = new TableView<DataObjectCropChart>();
          To a
          TableView<S>
          you can add various
          TableColumn<S,T>
          where S is the same data type as the TableView, and T is the type of data to be displayed in the column. T may be different for each column in the table.

          The usual way a TableColumn operates is to bind to an Property which implements an ObservableValue of the correct type for the column. In your case, the property to which you want to bind is a SimpleIntegerProperty. From the [url http://docs.oracle.com/javafx/2/api/javafx/beans/property/SimpleIntegerProperty.html]API docs for SimpleIntegerProperty you'll see that
          SimpleIntegerProperty implements ObservableValue<Number>
          so your TableColumn here should be a
          TableColumn<DataObjectCropChart, Number>
          .

          The cellValueFactory associated with a TableColumn is an object which determines the value to display in that column from the value for a given row. That is, it creates a ObservableValue of type T from a value of type S. In order to allow you to write really general versions of these, the value is actually wrapped in a CellDataFeatures object which gives you access to a couple of other things (namely the TableView and TableColumn for which it's creating the value).

          So what this means is that the cellValueFactory you assign to a
          TableColumn<S,T>
          is a
          Callback<CellDataFeatures<S,T>, ObservableValue<T>>
          i.e. an object that maps, via it's call(...) method, a
          CellDataFeatures<S,T>
          to an
          ObservableValue<T>
          In your case, S is DataObjectCropChart, and T is Number. So you get
          Callback<CellDataFeatures<DataObjectCropChart, Number>, ObservableValue<Number>> binsproperty =
            new Callback<CellDataFeatures<DataObjectCropChart, Number>, ObservableValue<Number>>() {
              @Override
              public ObservableValue<Number> call(CellDataFeatures<DataObjectCropChart, Number> p) {
                return p.getValue().propBinsProperty();
              }
            };
          Since this is a really common scenario, there's actually a helper class (PropertyValueFactory) in the API for all this. With the TableView and TableColumn set up as above, you can just do
          myColumn.setCellValueFactory(new PropertyValueFactory("propBins"));
          A couple of things:

          SimpleIntegerProperty (and IntegerProperty) implement ObservableValue with a type of Number; they really should implement ObservableValue with a type of Integer in my opinion. This seems to be a (minor) flaw in the API design.

          Your DataObjectCropChart snippet is a little unusual. You're returning a new property each time it's requested. The benefit of using a property is that the table will automatically update if the value of the property is changed. Since you're getting a different instance each time, the table column won't be observing the correct property for changes. A more usual implementation looks like this:
          public class DataObjectCropChart {
            private final IntegerProperty bins = new SimpleIntegerProperty();
          
            public IntegerProperty propBinsProperty() {
              return bins ;
            }
          
            public int getPropBins() {
              return bins.get();
            }
           
            public void setPropBins(int bins) {
              this.bins.set(bins);
            }
          }
          Now you only have one instance of the property, and any changes to it are propagated from the table to the DataObjectCropChart and vice-versa.

          Hope this helps.
          • 2. Re: TableColumn.setCellValueFactory()
            MoxeeMan
            Thanks, James. I'll try the "Number" approach you outline. I have no problem with my TableView which consists presently of two columns. The String in the first column works well but I have to format numbers in the other and future columns; that's where I've hit the buzz-saw.

            The next challenge will be the TableCell which, it seems, generics strips all the methods off the object that arrives @ updateItem(T, boolean) and you can't do anything with it. Can't call a method, can't access a property and can't even cast it to something usable! That will be the next hurdle. If you can add that missing piece I may be off and running with my first fx table! The chart that goes with it is beautiful! And I recreate the property every time because the cell will never be updated -- only replaced by data from another crop year derived from a new set of crop chart objects and a totally different table.

            Thanks much for your help, James. I've spent many hours the last week studying generics and the more I learn the less I like them! Hopefully, some day I'll understand why people think they're the greatest thing since sliced bread (all the text book writers at least!). Until then I'm struggling a bit with all the fx use of them.
            • 3. Re: TableColumn.setCellValueFactory()
              James_D
              Where the cellValueFactory is an object that determines the value to display in a table column (give a value from any given row), the cellFactory determines how to display a value in a cell. So the cellValueFactory is purely about retrieving the correct data; the cellFactory specifies what to do with that data.

              Given a
              TableView<S> table ;
              and a
              TableColumn<S,T> column ;
              the column.setCellFactory(...) method needs to be passed a
              Callback<TableColumn<S,T>, TableCell<S,T>>
              i.e. an object that creates a TableCell of S and T from a TableColumn of the same types. In the TableCell implementation, you pretty much only deal with the data type (T). Assuming you have declared your tableview and tablecolumn with the correct types:
              TableView<DataObjectCropChart> table ;
              TableColumn<DataObjectCropChart, Number> column ;
              then the compiler "knows" what type to expect (Number in your example) in the TableCell.

              TableCell is a Control: however the way this all works is not exactly as you may first guess. The intuitive idea of a grid of individual TableCell instances, one for each cell in the table, is not how this is implemented as performance would be horrible. There is (typically, but it's not guaranteed) just one TableCell instance per column. When the table needs to be rendered to the scene graph, FX will call the table cell instance's various update methods, and then render that cell in the appropriate place in the table. If another cell needs to be rendered, the update...(...) methods are called again and the cell rendered in the next location. (Caveat: this may not be the exact implementation; it's just my intuitive understanding of what happens. Thinking about it this way has worked for me.)

              Since there isn't an individual table cell instance for each cell in the displayed table, you can't usefully invoke its methods. You don't know which physical cell in the table the table cell instance will next be used to render. The way to use the API is to provide a factory to the column which overrides the update methods, as appropriate, and calls methods to configure the display of the cell.

              Typically, you'll override updateItem(...). This method is called when the cell is being configured to display a new "item" to display in the column. updateItem(...) takes two parameters: the item to display (the result of calling getValue() on the ObservableValue supplied by your cellValueFactory) and a flag to determine if this is an "empty" cell (one outside the range of the data). In the updateItem(...) method you typically call setText(...), setGraphic(...) to configure some text and another node to display. But you could do a bunch of other stuff if you need; call getIndex() to find out which row you're in; call isSelected(...) to determine if the cell is currently selected. You could also set style classes if you need, etc etc.

              The default behavior is essentially to call toString() on the item and pass it to setText().

              Important: you must invoke the default behavior for any update...(...) method by calling super.update...(...).

              So a simple (but not realistic) example would look like:
              TableColumn<DataObjectCropChart, Number> priceColumn = ... ;
              final java.text.Format currencyFormat = java.text.NumberFormat.getCurrentInstance();
              priceColumn.setCellFactory(new Callback<TableColumn<DataObjectCropChart, Number>, TableCell<DataObjectCropChart, Number>>() {
                @Override
                public TableCell<DataObjectCropChart, Number> call(TableColumn<DataObjectCropChart, Number> col) { 
                  return new TableCell<DataObjectCropChart,Number>() {
                    @Override
                    public void updateItem(Number price, boolean empty) {
                      // Important: invoke default behavior. This does a bunch of important stuff
                      super.updateItem(price, empty);
              
                      // set the text to a formatted currency, and align it to the right.
                      setText(currentFormat.format(price.doubleValue());
                      setAlignment(Pos.CENTER_RIGHT);
                    }
                  };
                });
              (Code untested: may contain typos.)

              Without generics, incidentally, this becomes horrible. All your specific data types would just have to be replaced by Object. The compiler wouldn't be able to check anything, and you'd have to use downcasts all over the place to get the correct type. Something like
              priceColumn.setCellFactory(new Callback() {
                @Override
                public Object call(Object col) { 
                  return new TableCell() {
                    @Override
                    public void updateItem(Object item, boolean empty) {
                      // Important: invoke default behavior. This does a bunch of important stuff
                      super.updateItem(item, empty);
              
                      // No generics. Have to trust on faith that the item passed in was really a Number:
                      Number price = (Number) item ;
                      // set the text to a formatted currency, and align it to the right.
                      setText(currentFormat.format(price.doubleValue());
                      setAlignment(Pos.CENTER_RIGHT);
                    }
                  };
                });
              Note here the complier lets you get away with anything. You don't even have to (from the compiler's perspective) return a TableCell from the call(...) method. The fact that the value passed into updateItem(...) is really a Number comes from you having set up the cellValueFactory correctly. If you get anything wrong with the types here, you just get a ClassCastException at runtime, and debugging that can be painful. And of course if you don't test it correctly, there's always a chance something goes wrong much later on (i.e. after deployment...) that you didn't think of.

              So, I strongly recommend you use the generic version.
              • 4. Re: TableColumn.setCellValueFactory()
                MoxeeMan
                Thanks, James, for your explanations and patience.

                If what I'm reading about generics is correct for the following code you provided:

                return new TableCell<DataObjectCropChart,Number>() {
                @Override
                public void updateItem(Number price, boolean empty) {
                // Important: invoke default behavior. This does a bunch of important stuff
                super.updateItem(price, empty);

                // set the text to a formatted currency, and align it to the right.
                setText(currentFormat.format(price.doubleValue());
                setAlignment(Pos.CENTER_RIGHT);
                }
                };

                updateItem(Number price, boolean empty) is only going to know price is an Object....not a Number. At least in Netbeans that is the way things have been interpreted so far (this generics thing is stabbing me in the heart at this point!). So, if I understand what I've read about generics, I presume I'm going to have to get the value: int x = Integer.parseInt(price.toString());

                Rest assured I shall revise all the code per your suggestions this evening to see if Netbeans cooperates!

                Again, let me emphasize I appreciate your assistance getting my arms around this. table.getColumnModel().getColumn(1).setCellRenderer(new TableCellRendererTwoPlaces()) is so sweet!!!! :) Whatever was wrong with that!! ?
                • 5. Re: TableColumn.setCellValueFactory()
                  James_D
                  return new TableCell<DataObjectCropChart,Number>() {
                  @Override
                  public void updateItem(Number price, boolean empty) {
                  // Important: invoke default behavior. This does a bunch of important stuff
                  super.updateItem(price, empty);

                  // set the text to a formatted currency, and align it to the right.
                  setText(currentFormat.format(price.doubleValue());
                  setAlignment(Pos.CENTER_RIGHT);
                  }
                  };

                  updateItem(Number price, boolean empty) is only going to know price is an Object....not a Number.
                  No, completely the opposite. The compile time type of price is Number (that's how you declared the variable): therefore the compiler "knows" it's a Number. That's why I can call price.doubleValue() in the example. You can do
                  int x = price.intValue();
                  to get a safer (no NumberFormatException) representation as an int.

                  In fact, in the example, I could just have done
                  setText(currencyFormat.format(price));
                  (I just temporarily forgot how to use a Format...).

                  Without generics you have to go through steps like the one you show.

                  Make sure you have declared your TableView and TableColumn correctly: that's key. If you do
                  TableView table = .. ;
                  TableColumn column = ... ;
                  it won't work; you'll be forced to use Object types in your factories.

                  If you do
                  TableView<DataObjectCropChart> table = ...;
                  TableColumn<DataObjectCropChart, Number> column = ... ;
                  then you tell the compiler the types of things in your table and columns, so the types in your factories can be more specific.
                  • 6. Re: TableColumn.setCellValueFactory()
                    MoxeeMan
                    Thanks again, James.

                    It works fine in-line. I'm trying to create a re-usable class (because I'm lazy) to provide the values and cells. Will I get there? Who knows. But at least your in-line versions work and I don't know when, if ever, I'd have figured out the "Number" part of the value equation! You've been a great help! Now I'll get on about my business of setting the office abuzz with my beautiful fx charts and tables! :)
                    • 7. Re: TableColumn.setCellValueFactory()
                      James_D
                      Make sure you check out the classes provided by the API before you invest time in creating reusable implementations of your own; what you need may already be there. From the [url http://docs.oracle.com/javafx/2/api/index.html]API docs navigate to the javafx.scene.control.cell package and look at PropertyValueFactory for a cellValueFactory implementation, and the various TableCell implementations.