Hauke Hauke - 7 months ago 50
Java Question

Java 8 method references with javafx

I just developed a JavaFX applications this twenty different pages. Each page has a table and I wanted to place a context menu on each table.

Basically its always the same code for placing the context menu to the table but I am hoping that method references can help here a little bit.

This is the actual code snippet:

resultTable.setRowFactory(new Callback<TableView<InterfaceModel>, TableRow<InterfaceModel>>() {
@Override
public TableRow<InterfaceModel> call(TableView<InterfaceModel> tableView) {
final TableRow<InterfaceModel> row = new TableRow<InterfaceModel>();

final ContextMenu rowMenu = new ContextMenu();
MenuItem editItem = new MenuItem("EDIT");
editItem.setOnAction(event -> {
// action if edit was selected
});


And I want something like that:

ContextMenuHelper helper = new ContextMenuHelper(resultTable);
helper.addItem("Edit", [referenceToAMethod]);
helper.addItem("Item 2", [referenceToADifferentMethod]);


What I mean is that this helper creates the context menu. All this helper needs is the label for the entry and a method to call after selection of this entry.

Is that possible with the method-refereces from java 8?

Thanks,
Hauke

Answer

If you just want to define a method for creating a MenuItem, then it's easy enough: you just need to decide on the functional interface you will need for the parameter that takes the method reference (or lambda, etc). E.g. if the method signature takes no parameters and has void return type, you could use Runnable:

public MenuItem createItem(String text, Runnable handler) {
    MenuItem item = new MenuItem(text);
    item.setOnAction(e -> handler.run());
}

You probably want the menu item event handler to have access to the table item in the row, in which case it would need a reference to the row:

public <T> MenuItem createItem(String text, TableRow<T> row, Consumer<T> handler) {
    MenuItem item = new MenuItem(text);
    item.setOnAction(e -> handler.accept(row.getItem()));
}

Then you can do

TableView<InterfaceModel> table = new TableView<>();
ContextMenuHelper helper = new ContextMenuHelper();
table.setRowFactory(t -> {
    TableRow<InterfaceModel> row = new TableRow<>();
    ContextMenu menu = new ContextMenu();
    row.setContextMenu(menu);
    menu.getItems().addItem(helper.createItem("Edit", row, this::edit));
    // etc...
});

with

private void edit(InterfaceModel model) {
    // ...
}

What you didn't actually ask, but I'm sort of guessing you really want, is for the "helper" class to actually set the row factory and create all the menus, etc. This is a bit harder to structure, because you need to entirely build the context menu inside the row factory, so you need to know all the menu items before you can actually set the row factory. For this, you probably want to consider a builder pattern:

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.util.Callback;

public class TableRowContextMenuBuilder<T> {
    private final List<MenuItemConfig<T>> items ;
    private boolean built ;

    public TableRowContextMenuBuilder() {
        this.items = new ArrayList<>();
    }


    public static <T> TableRowContextMenuBuilder<T> create(Class<T> type) {
        return new TableRowContextMenuBuilder<>();
    }

    public TableRowContextMenuBuilder<T> addItem(String text, Consumer<T> handler) {
        if (built) {
            throw new IllegalStateException("Row factory is already built: cannot add new items");
        }
        items.add(new MenuItemConfig<T>(text, handler));
        return this ;
    }

    public TableRowContextMenuBuilder<T> addItem(String text, Runnable handler) {
        return addItem(text, t -> handler.run());
    }

    public Callback<TableView<T>, TableRow<T>> build() {
        if (built) {
            throw new IllegalStateException("Cannot build row factory more than once");
        }
        built = true ;
        return t -> {
            TableRow<T> row = new TableRow<>();
            ContextMenu menu = new ContextMenu();
            row.setContextMenu(menu);
            items.stream()
                .map(config -> config.asMenuItem(row))
                .forEach(menu.getItems()::add);
            return row ;
        };
    }

    public void buildForTable(TableView<T> table) {
        table.setRowFactory(build());
    }


    private static class MenuItemConfig<T> {
        private final String text ;
        private final Consumer<T> handler ;
        MenuItemConfig(String text, Consumer<T> handler) {
            this.text = text;
            this.handler = handler;
        }
        MenuItem asMenuItem(TableRow<T> row) {
            MenuItem item = new MenuItem(text);
            item.setOnAction(e -> handler.accept(row.getItem()));
            return item ;
        }
    }
}

And now you can do

TableView<InterfaceModel> table = new TableView<>();
TableViewContextMenuBuilder.create(InterfaceModel.class)
    .menuBuilder.addItem("Edit", this::edit);
    .menuBuilder.addItem("Item 2", this::handleOtherItem);
    // ...
    .buildForTable(table);

with the appropriate methods defined:

private void edit(InterfaceModel model) { /* ... */}
private void handleOtherItem(InterfaceModel model) { /* ... */}
Comments