Reusable Row and Cell Factories for TableViews that generate ContextMenus

Introduction

In the previous post on this topic, I showed how to write cell and row factories for TableViews that showed ContextMenus. That approach works well, but if you have multiple tables in your UI which require context menus, you’ll find that there’s a fair amount of boilerplate code that has to be repeated, replicating the cell and/or row factory machinery and managing the context menus each time. In this post, I’ll show how to factor that boilerplate code out into reusable factory classes that are flexible and reasonably easy to use.

Considerations

In creating our cell factory and row factory, we need to think a little about some use cases. The only thing these factories will do will be to add a context menu to the TableRow or TableCell. There are plenty of other uses for a TableView’s cellFactory and rowFactory, and we want to be able to use these in such a way that we don’t prohibit the user from having other cell factory or row factory implementations. In order to do this, we’ll implement our factories using a wrapper pattern, so the user can optionally pass in another factory which will be used as a base.

Our main aim is to minimize the amount of code that has to be written for each table. The bare minimum will be to create a set of MenuItems: we want to keep anything above and beyond that to a minimum. The problem is that these menu items will have functionality that depends on the item being displayed in the row or cell. The approach we will take is to let the user specify a Callback that takes the appropriate value and generates a List<MenuItem> that will be used to populate the context menu.

For the row factory, given a TableView<T>, each row displays an object of type T, and the callback the user provides will be a Callback<T, List<MenuItem>>. This gives the user access to the item displayed in the row when they are generating the menu items.

For the cell factory, things are slightly more complex. Given a TableView<S>, each column is represented by a TableColumn<S,T>, where S is the type of each item in a table row, and T is the type of the property displayed in the column. The cell factory generates TableCell<S,T>. In generating the MenuItems, the user might want not only to access the value in the cell, of type T, but may also want to be able to access the value in the row, of type S. In order to do this, we’ll ask the user to provide a Callback<TableCell<S,T>, List<MenuItem>>. This gives the user access to the table cell when generating the MenuItems. From this, they can call getItem() to get the cell value, or getTableRow().getItem() to get the value for the row.

Implementing the wrapper

Let’s look first at how we implement the wrapper. For the row factory, we’ll define the following class:

public class ContextMenuTableRowFactory<T> implements Callback<TableView<T>, TableRow<T>>

In order to provide the wrapper functionality, we’ll define a field of type Callback<TableView<T>, TableRow<T>>, which we will allow to be null. In the implementation of the call(…) method, we will invoke the call(…) method on the wrapped field (if it is not null), or create a default TableRow (if it is):

public class ContextMenuTableRowFactory&lt;T&gt; implements 
    Callback&lt;TableView&lt;T&gt;, TableRow&lt;T&gt;&gt; {
  // ...
  private final Callback&lt;TableView&lt;T&gt;, TableRow&lt;T&gt;&gt; rowFactory ;
  // ...
  @Override
  public TableRow&lt;T&gt; call(TableView&lt;T&gt; table) {
    final TableRow&lt;T&gt; row ;
    if (rowFactory == null) {
      row = new TableRow&lt;T&gt;();
    } else {
      row = rowFactory.call(table);
    }
    // configure row with context menu
    return row ;
  }
  // ...
}</pre>
The cell factory has a similar pattern:
<pre>public class ContextMenuTableCellFactory&lt;S,T&gt; implements 
    Callback&lt;TableColumn&lt;S,T&gt;, TableCell&lt;S,T&gt;&gt; {
  // ...
  private final Callback&lt;TableColumn&lt;S,T&gt;, TableCell&lt;S,T&gt; cellFactory ;
  // ...
  @Override
  public TableCell&lt;S,T&gt; call(TableColumn&lt;S,T&gt; tableColumn) {
    final TableCell&lt;S,T&gt; cell ;
    if (cellFactory == null) {
      cell = new TableCell&lt;S,T&gt;() {
        @Override
        public void updateItem(T item, boolean empty) {
          super.updateItem(item, empty);
          if (empty || item==null) {
            setText(null);
          } else {
            setText(item.toString());
          }
        }
      };
    } else {
      cell = cellFactory.call(tableColumn);
    }
    // configure cell with context menu
    return cell ;
  }
  // ...
}

Notice in this case that we provide some basic display behavior when creating the default table cell.

Configuring the context menu

In order to create the context menu for the row factory, the user will provide a List of MenuItems. Since these MenuItems will have behavior that depends on a particular item in the row, they need access to that item in order to supply the MenuItems. We will define a field of type Callback<T, List<MenuItem>> which the user will initialize via the constructor call. To configure the context menu in the call(…) method, we retrieve the item from the table row, pass it to the provided callback, and populate a context menu with the resulting list of MenuItems. As in the previous post we also copy any menu items attached to a context menu defined on the table into this context menu.

Our row factory now looks like this:

public class ContextMenuTableRowFactory<T> implements
    Callback<TableView<T>, TableRow<T>> {
  private final Callback<TableView<T>, TableRow<T>> rowFactory ;
  private final Callback<T, List<MenuItem>> menuItemFactory ;

  // constructors:
  public ContextMenuTableRowFactory(
      Callback<TableView<T>, TableRow<T>> rowFactory,
      Callback<T, List<MenuItem>> menuItemFactory) {
    this.rowFactory = rowFactory ;
    this.menuItemFactory = menuItemFactory ;
  }

  public ContextMenuTableRowFactory(
      Callback<T, List<MenuItem>> menuItemFactory) {
    this(null, menuItemFactory);
  }

  @Override
  public TableRow<T> call(TableView<T> table) {
    final TableRow<T> row ;
    // initialized as above
    ContextMenu menu = createContextMenu(row);
    // ...
    return row ;
  }

  private ContextMenu createContextMenu(TableRow<T> row) {
    ContextMenu menu = new ContextMenu();
    ContextMenu tableMenu = row.getTableView().getContextMenu();
    if (tableMenu != null) {
      menu.getItems().addAll(tableMenu.getItems());
      menu.getItems().add(new SeparatorMenuItem());
    }
    menu.getItems().addAll(menuItemFactory.call(row.getItem()));
    return menu ;
  }
}

The last detail is that we need to make sure that we update the MenuItems if the item displayed in the row changes (remember, the menu items’ functionality depends on the row item). We achieve this by listening to the row’s item property and updating the context menu. While we’re there, we’ll make sure the context menu only appears on rows whose item is not null:

public TableRow<T> call(TableView<T> table) {
  final TableRow<T> row ;
  // ...
  row.itemProperty().addListener(new ChangeListener<T>() {
    @Override
    public void changed(ObservableValue<? extends T> obs, 
        T oldValue, T newValue) {
      if (newValue==null) {
        row.setContextMenu(null);
      } else {
        row.setContextMenu(createContextMenu(row));
      }
    }
  });
  return row ;
}

The code for the cell factory is fairly similar. The main difference is that our menuItemFactory is a Callback<TableCell<S,T>, List<MenuItem>>, and we pass the cell itself into its call(…) method. One other small difference is that we check both the enclosing TableRow and the TableView for context menus from which to “borrow” menu items:

public class ContextMenuTableCellFactory<S,T> implements 
    Callback<TableColumn<S,T>, TableCell<S,T>> {
  private final Callback<TableColumn<S,T>, TableCell<S,T>> cellFactory;
  private final Callback<TableCell<S,T>, List<MenuItem>> menuItemFactory ;
  // ...
  private ContextMenu createContextMenu(TableCell<S,T> cell) {
    ContextMenu menu = new ContextMenu();
    TableRow<?> row = cell.getTableRow();
    if (row != null) {
      ContextMenu rowMenu = row.getContextMenu();
      if (rowMenu == null) {
        TableView<S> table = cell.getTableView();
        ContextMenu tableMenu = table.getContextMenu();
        if (tableMenu != null) {
          menu.getItems().addAll(tableMenu.getItems());
          menu.getItems().add(new SeparatorMenuItem());
        }
      } else {
        menu.getItems().addAll(rowMenu.getItems());
        menu.getItems().add(new SeparatorMenuItem());
      }
    }
    menu.getItems().addAll(menuItemFactory.call(cell);
    return menu ;
  }
}

Using the factory classes

To use the factory classes, we just have to create a callback that generates a List of MenuItems and pass it to the factory’s constructor. Here’s an example, using the usual TableView<Person> example from the Oracle tutorial.

final TableView<Person> table = new TableView<>();
// ...
final TableColumn<Person, String> emailCol = new TableColumn<>("Email");
// ...
final Callback<Person, List<MenuItem>> rowMenuItemFactory
    = new Callback<Person, List<MenuItem>>() {
  @Override
  public List<MenuItem> call(final Person person) {
    final MenuItem edit = new MenuItem("Edit");
    edit.setOnAction(...) ;
    final MenuItem delete = new MenuItem("Delete");
    delete.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent event) {
        table.getItems().remove(person);
      }
    }
    return Arrays.asList(edit, delete);
  }
};
table.setRowFactory(new ContextMenuTableRowFactory(rowMenuItemFactory));

final Callback<TableCell<Person, String>, List<MenuItem>> emailCellMenuItemFactory
    = new Callback<TableCell<Person, String>, List<MenuItem>>() {
  @Override
  public List<MenuItem> call(final TableCell<Person, String> cell) {
    MenuItem email = new MenuItem("Email");
    email.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent event) {
        // TODO: implement email here
        String emailAdd = cell.getItem();
        Person person = cell.getTableRow().getItem();
        System.out.println("Email "+person+" at "+emailAdd);
      }
    });
    return Collections.singletonList(email);
  }
};
emailCol.setCellFactory(new ContextMenuTableCellFactory(emailCellMenuItemFactory));

Note how the bulk of the code in this last listing is creating the MenuItems and their event handlers. There is a small amount of boilerplate code in wrapping these in a callback, but this is minimized further in Java 8, where the Callback can be expressed as a lambda expression.

Full code for this post, including the row and cell factories, and a full sample with menu items attached for the TableView, TableRow, and TableCell contexts is posted at github.

Leave a Reply

Your email address will not be published. Required fields are marked *