This discussion is archived
2 Replies Latest reply: Jan 19, 2013 11:53 AM by KonradZuse RSS

What exactly is the "cell factory?"

KonradZuse Explorer
Currently Being Moderated
So I've been seeing this term a lot on the forums, as well as http://docs.oracle.com/javafx/2/ui_controls/table-view.htm and on the ensemble, but I'm not 100% sure what it is.... It seems like just a method to set data into your tables, like the table model? Anyone have a more in depth explanation than the one in the docs, I would appreciate it!!!

Thanks,

~KZ
  • 1. Re: What exactly is the "cell factory?"
    jsmith Guru
    Currently Being Moderated
    Cell factories create cells. A cell is a Labeled Node which contains some extra properties and methods for maintaining an editing and selection state and a link back to a cell value. Cells are used in a few places in JavaFX, for example in ListViews and TableViews, as well as TreeTables and ComboBoxes. The Cell is the visual representation (Node) which corresponds to a backing data item. The trick is that there is not necessarily a static one to one correspondence between cells and data values.

    Let's take an example. Here is an empty ListView in a Scene. When I run the app, it displays the ListView at it's preferred height, with 17 rows.
    import javafx.application.Application;
    import javafx.scene.Scene;
    import javafx.scene.control.ListView;
    import javafx.scene.layout.*;
    import javafx.stage.Stage;
    
    public class ListViewSample extends Application {
      @Override public void start(Stage stage) {
        ListView listView = new ListView();
    
        VBox layout = new VBox();
        VBox.setVgrow(listView, Priority.ALWAYS);
        layout.getChildren().addAll(listView);
        stage.setScene(new Scene(layout));
        stage.show();
      }
    
      public static void main(String[] args) { launch(args); }
    }
    Each one of those 17 rows is empty. No Cell Factory has been set, yet you can see alternating light and dark shaded rows. Each one of these rows in the ListView corresponds to a Cell and each cell has been generated by the default ListView cell factory. When I drag the stage's bottom border to increase the size of the stage, the list view increases in size. When I drag the stage's bottom border to decrease the size of the stage, the list view decreases in size. When the list view increases in size, more rows are visible. Each of the new cells for the larger list view are generated by the cell factory on an as needed basis; i.e. the cells were not created when the app was first run but only created as there was a greater visible area available to the ListView in which the ListView could display more cells.

    Now everything is pretty boring so far. Let's add some data, using the following line of code:
    listView.setItems(FXCollections.observableArrayList("apple", "orange", "pear"));
    Now you will see the strings "apple", "orange" and "pear" rendered in the first three cells of the ListView again by using the default cell factory for the ListView. Again this is pretty boring.

    What we will do now is add some mutators which will change the observable list backing the list view in response to some user actions:
    import javafx.application.Application;
    import javafx.collections.FXCollections;
    import javafx.event.*;
    import javafx.scene.Scene;
    import javafx.scene.control.*;
    import javafx.scene.layout.*;
    import javafx.stage.Stage;
    
    import java.util.Collections;
    import java.util.Comparator;
    
    public class ListViewSample extends Application {
      @Override public void start(Stage stage) {
        final ListView<String> listView = new ListView<>();
        listView.setItems(FXCollections.observableArrayList("apple", "orange", "pear"));
    
        ListViewSorter listViewSorter = new ListViewSorter(listView).invoke();
    
        VBox layout = new VBox(10);
        VBox.setVgrow(listView, Priority.ALWAYS);
        listView.setMinHeight(0);
        layout.getChildren().addAll(
            listView,
            HBoxBuilder
                .create()
                .spacing(10)
                .children(
                    guavaCreator(listView),
                    listViewSorter.getSorter(),
                    listViewSorter.getReverser()
                )
                .build()
        );
    
        stage.setScene(new Scene(layout));
        stage.show();
      }
    
      private Button guavaCreator(final ListView<String> listView) {
        final Button guavatron = new Button("Add Guava");
        guavatron.setOnAction(new EventHandler<ActionEvent>() {
          @Override public void handle(ActionEvent actionEvent) {
            listView.getItems().add("guava");
            guavatron.setDisable(true);
          }
        });
        return guavatron;
      }
    
      public static void main(String[] args) { launch(args); }
    
      private class ListViewSorter {
        private final ListView<String> listView;
        private Button sorter;
        private Button reverser;
    
        public ListViewSorter(ListView<String> listView) {
          this.listView = listView;
        }
    
        public Button getSorter() {
          return sorter;
        }
    
        public Button getReverser() {
          return reverser;
        }
    
        public ListViewSorter invoke() {
          sorter = new Button("Sort");
          sorter.setOnAction(new EventHandler<ActionEvent>() {
            @Override public void handle(ActionEvent actionEvent) {
              Collections.sort(listView.getItems());
            }
          });
    
          final Comparator<String> REVERSE_SORT = new Comparator<String>() {
            @Override  public int compare(String s1, String s2) {
              return -1 * s1.compareTo(s2);
            }
          };
    
          reverser = new Button("Reverse Sort");
          reverser.setOnAction(new EventHandler<ActionEvent>() {
            @Override public void handle(ActionEvent actionEvent) {
              Collections.sort(listView.getItems(), REVERSE_SORT);
            }
          });
          return this;
        }
      }
    }
    OK, now we have some extra buttons, the "Add guava" button will create a new item ("guava"), the "Sort" and "Reverse Sort", buttons will change the sort order of the backing list. Now to understand what happens behind the scenes when we use these buttons, let's take a look at the source code for the default list cell factory.
    new ListCell() {
       @Override public void updateItem(Object item, boolean empty) {
         super.updateItem(item, empty);
    
         if (empty) {
           setText(null);
           setGraphic(null);
         } else if (item instanceof Node) {
           setText(null);
           Node currentNode = getGraphic();
           Node newNode = (Node) item;
           if (currentNode == null || ! currentNode.equals(newNode)) {
             setGraphic(newNode);
           }
         } else {
           setText(item == null ? "null" : item.toString());
           setGraphic(null);
         }
       }
     };
    This code is doing one of three things. If the list cell is empty, it sets the text and graphic to null, so you end up with a blank cell (the alternating light and dark grey bars are generated by the ListCell's parent setting differing style classes on alternate cells). If the item is a node, it sets the graphic to the node - this is the mechanism which allow you to place nodes directly in the backing list for the ListView and have the ListView display them OK. Otherwise a toString is called on the item to set the item's text (this is the case which is occurring for our simple example of Strings in the backing list).

    Now the important thing to note about the ListCell implementation is that the clever logic of translating the backing item for the cell to a visual representation is occurring in an updateItem call. This updateItem method is invoked by the JavaFX system on the ListCell whenever the backing item for the cell has been invalidated, for example the item has been edited, a new item added, or the items in the list have been reordered.

    So when somebody presses, the "Add Guava" button, a new ListCell is not created, instead updateItem is called on an already existing empty cell. This is because when we started the application, there was space for 17 rows, so 17 cells were already created, it is just that most of them were empty because we only had 3 items in the backing list for the ListView.

    Now, if we press one of the sort buttons to reorder the backing list, it will cause the existing list cells to become invalidated, and updateItem will be called on each of the cells according to the change permutations in the ObservableList. Note that as each item is updated, a new Labeled display node for the item is not created, instead the setText method is invoked which changes the text for the existing Labeled.

    There are a couple of extra cases to understand. Our backing list currently maxes out at 4 items. Let's say we drag the bottom of our stage up so that the available space for the ListView was made really small (e.g. only 2 rows high). In this case, you will two rows (cells) and a scrollbar you can use to scroll up and down. As you scroll up and down it seems that some rows are scrolling off the screen and some are scrolling on the screen. What is actually happening though is that the same two cells are remaining on screen and their contents being continually updated and replaced as backing items come in and out of view. This is the magic of how the ListView is able to achieve it's efficiency when dealing with potentially very large collections or collections where not all of the required data is available on the client at the current time. Instead of creating visual cells for all of the possible items which can be placed in the list, instead the ListView creates cells only for the visible items and updates the content of those cells on an as needed basis. This concept is known in the List Cell creators jargon as a Virtual Flow in a Virtualized control.

    OK, so that was a little more interesting, but there have been a lot of words so far, and no custom cell factory. This was partly on purpose - there is lot you can do with the default cell factory without needing to create your own custom cell factory.

    But sometimes you do actually want to create your own cell factory when you want fine control over the look or behaviour of the cells.

    Let's say you want to show each item in the list with a capitalized friendly name "Apple", "Orange" and "Pear" and an icon - a picture of the corresponding fruit. To do this you would create a cell factory - something that can produce the visual representation of these things from the corresponding data values.
    import javafx.application.Application;
    import javafx.collections.*;
    import javafx.scene.Scene;
    import javafx.scene.control.*;
    import javafx.scene.image.*;
    import javafx.scene.layout.*;
    import javafx.stage.Stage;
    import javafx.util.Callback;
    
    public class ListViewCustomCellFactorySample extends Application {
      ObservableMap<String, Image> iconMap = FXCollections.observableHashMap();
    
      @Override public void init() {
        iconMap.put(
          "apple",  
          new Image(
            "http://uhallnyu.files.wordpress.com/2011/11/green-apple.jpg", 
            0, 32, true, true
          )
        );
        iconMap.put(
          "orange", 
          new Image(
            "http://i.i.com.com/cnwk.1d/i/tim/2011/03/10/orange_iStock_000001331357X_540x405.jpg",
            0, 32, true, true
          )
        );
        iconMap.put(
          "pear",   
          new Image(
            "http://smoothiejuicerecipes.com/pear.jpg", 
            0, 32, true, true
          )
        );
      }
    
      @Override public void start(Stage stage) {
        final ListView<String> listView = new ListView<>();
        listView.setItems(FXCollections.observableArrayList("apple", "orange", "pear"));
    
        listView.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
          @Override public ListCell<String> call(ListView<String> stringListView) {
            return new LabeledIconListCell();
          }
        });
    
        VBox layout = new VBox(10);
        VBox.setVgrow(listView, Priority.ALWAYS);
        listView.setMinHeight(0);
        layout.getChildren().addAll(
            listView
        );
        stage.setScene(new Scene(layout));
        stage.show();
      }
    
      public static void main(String[] args) { launch(args); }
    
      private class LabeledIconListCell extends ListCell<String> {
        @Override protected void updateItem(String item, boolean empty) {
          super.updateItem(item, empty);
    
          if (item != null) {
            String friendlyText = item.toString();
            if (item.length() > 0) {
              friendlyText = item.substring(0, 1).toUpperCase() + item.substring(1);
            }
            setText(friendlyText);
    
            setGraphic(
                StackPaneBuilder
                    .create()
                    .prefWidth(55)
                    .children(
                        new ImageView(
                            iconMap.get(item)
                        )
                    )
                    .build()
            );
          } else {
            setText("");
            setGraphic(null);
          }
        }
      }
    }
    Here what the cell factory has done is to check what value of the backing item for the cell is whenever that item has been updated, and set some customized label text and graphic representation for the cell.

    As a minor point, for efficiency, and because there are only a few of them, the required images are loaded and scaled up front so that they don't been to be reloaded every time the cell is updated with a different value (which if the image loading was within the cell's updateItem call could mean that the same image could potentially get loaded multiple times.

    My personal take on this is that it is powerful but complicated. Often people will gravitate towards using the complex ListView and TableView APIs when they don't necessarily need all of the functionality and virtualization efficiency capabilities that the virtualized controls offer. In many cases, simple layout mechanisms such VBoxes and Grids can be a better choice. However, if you have a need for the virtualized functions, then it's good to know that things like ListView and TableView are there if you can work out how to use them well in your case.

    Also note that JavaFX 2.2+ has numerous convenience methods for creating different kinds of cells which you may be able to use in standard cases to avoid some of the overhead in creating your own, for example the CheckBoxListCell, the ComboBoxListCell and the TextFieldListCell. And there are many more such simplifying higher level abstractions in the DataFX library.

    One other point worth observing is that if you have a list of mutatable objects, for example Person objects with a changable last name field, then you need to make the object an Observable object with an invalidation implementation if you want the updateItem call in the cell factory to be invoked automatically whenever the object is mutated.

    A cell factory and a cell value factory are different things, but that is probably a topic for a different post.

    I realize this was a round about and lengthy explanation - hopefully it served some purpose and helped to explain some of the mysteries of cell factories.

    http://docs.oracle.com/javafx/2/api/javafx/scene/control/Cell.html
    http://www.javafxdata.org
    http://docs.oracle.com/javafx/2/ui_controls/list-view.htm
  • 2. Re: What exactly is the "cell factory?"
    KonradZuse Explorer
    Currently Being Moderated
    Damn you went really hard on this one, thanks! So it seems it's like I was thinking, just a way to edit the data in the cells, and every table HAS a DEFAULT cell factory? From the API it seemed like it was something seperate, but maybe they meant a custom cell factory with the check box.


    Basically all I need is to be able to have column headers as people's names, and then under it have their score(for a game I'm creating). Nothing really fancy. I've used Swing Tables before without needing a table model, so it seems like it's about the same with regards to how the FX table is used. Graphics seem awesome, but I don't think they are needed.... MAYBE later....

    I appreciate your time spent on this Q, I will run your code later when I have some time :).

    Also I was curious, why did you use a map for the images?

    I have only used Stacks, Queues, Trees, LinkedLists and Arrays for Collections(Data Structures) but I've been curious about hash maps and the few other cases, but those seem rarely used(At least from what I've seen). Why are we using a hash map for the images?


    Thanks again for everything!!!!

Legend

  • Correct Answers - 10 points
  • Helpful Answers - 5 points