0

I am trying to add a button into a GUI app in JFX for dealing with a database of customers and appointments. I want to put two buttons in each row of the table that will open other views when clocked.

Below is what I currently have (which does not work). The compiler doesn't complain about this code but I get the following ClassCastException at runtime (which did not seem to be an issue in the place I saw ActionButtonTableCell originally used for a case analogous to this one; How to add button in JavaFX table view).

Here is the stack trace:

Exception in thread "JavaFX Application Thread" java.lang.ClassCastException: class javafx.scene.control.TableColumn$CellDataFeatures cannot be cast to class javafx.scene.control.TableColumn (javafx.scene.control.TableColumn$CellDataFeatures and javafx.scene.control.TableColumn are in module javafx.controls of loader 'app')
    at javafx.controls/javafx.scene.control.TableColumn.getCellObservableValue(TableColumn.java:593)
    at javafx.controls/javafx.scene.control.TableColumn.getCellObservableValue(TableColumn.java:578)
    at javafx.controls/javafx.scene.control.TableCell.updateItem(TableCell.java:646)
    at javafx.controls/javafx.scene.control.TableCell.indexChanged(TableCell.java:469)
    at javafx.controls/javafx.scene.control.IndexedCell.updateIndex(IndexedCell.java:120)
    at javafx.controls/javafx.scene.control.skin.TableSkinUtils.resizeColumnToFitContent(TableSkinUtils.java:119)
    at javafx.controls/javafx.scene.control.skin.TableSkinUtils.resizeColumnToFitContent(TableSkinUtils.java:86)
    at javafx.controls/javafx.scene.control.skin.TableColumnHeader.doColumnAutoSize(TableColumnHeader.java:573)
    at javafx.controls/javafx.scene.control.skin.TableColumnHeader.updateScene(TableColumnHeader.java:516)
    at javafx.controls/javafx.scene.control.skin.TableColumnHeader.lambda$new$0(TableColumnHeader.java:159)
    at javafx.controls/com.sun.javafx.scene.control.LambdaMultiplePropertyChangeListenerHandler.lambda$new$1(LambdaMultiplePropertyChangeListenerHandler.java:49)
    at javafx.base/javafx.beans.value.WeakChangeListener.changed(WeakChangeListener.java:86)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
    at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
    at javafx.base/javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:74)
    at javafx.base/javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
    at javafx.graphics/javafx.scene.Node$ReadOnlyObjectWrapperManualFire.fireSuperValueChangedEvent(Node.java:1054)
    at javafx.graphics/javafx.scene.Node.invalidatedScenes(Node.java:1114)
    at javafx.graphics/javafx.scene.Node.setScenes(Node.java:1152)
    at javafx.graphics/javafx.scene.Parent$2.onChanged(Parent.java:369)
    at javafx.base/com.sun.javafx.collections.TrackableObservableList.lambda$new$0(TrackableObservableList.java:45)
    at javafx.base/com.sun.javafx.collections.ListListenerHelper$Generic.fireValueChangedEvent(ListListenerHelper.java:329)
    at javafx.base/com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:73)
    at javafx.base/javafx.collections.ObservableListBase.fireChange(ObservableListBase.java:233)
    at javafx.base/javafx.collections.ListChangeBuilder.commit(ListChangeBuilder.java:482)
    at javafx.base/javafx.collections.ListChangeBuilder.endChange(ListChangeBuilder.java:541)
    at javafx.base/javafx.collections.ObservableListBase.endChange(ObservableListBase.java:205)
    at javafx.base/javafx.collections.ModifiableObservableListBase.setAll(ModifiableObservableListBase.java:90)
    at javafx.base/com.sun.javafx.collections.VetoableListDecorator.setAll(VetoableListDecorator.java:116)
    at javafx.controls/javafx.scene.control.skin.NestedTableColumnHeader.updateContent(NestedTableColumnHeader.java:578)
    at javafx.controls/javafx.scene.control.skin.NestedTableColumnHeader.updateTableColumnHeaders(NestedTableColumnHeader.java:504)
    at javafx.controls/javafx.scene.control.skin.NestedTableColumnHeader.checkState(NestedTableColumnHeader.java:638)
    at javafx.controls/javafx.scene.control.skin.NestedTableColumnHeader.computePrefHeight(NestedTableColumnHeader.java:345)
    at javafx.graphics/javafx.scene.Parent.prefHeight(Parent.java:1031)
    at javafx.graphics/javafx.scene.layout.Region.prefHeight(Region.java:1559)
    at javafx.controls/javafx.scene.control.skin.TableHeaderRow.computePrefHeight(TableHeaderRow.java:376)
    at javafx.controls/javafx.scene.control.skin.TableHeaderRow.computeMinHeight(TableHeaderRow.java:369)
    at javafx.graphics/javafx.scene.Parent.minHeight(Parent.java:1059)
    at javafx.graphics/javafx.scene.layout.Region.minHeight(Region.java:1525)
    at javafx.controls/javafx.scene.control.SkinBase.computeMinHeight(SkinBase.java:311)
    at javafx.controls/javafx.scene.control.Control.computeMinHeight(Control.java:512)
    at javafx.graphics/javafx.scene.Parent.minHeight(Parent.java:1059)
    at javafx.graphics/javafx.scene.layout.Region.minHeight(Region.java:1525)
    at javafx.graphics/javafx.scene.layout.Region.computeChildPrefAreaHeight(Region.java:1980)
    at javafx.graphics/javafx.scene.layout.GridPane.computePrefHeights(GridPane.java:1436)
    at javafx.graphics/javafx.scene.layout.GridPane.computePrefHeight(GridPane.java:1265)
    at javafx.graphics/javafx.scene.Parent.prefHeight(Parent.java:1031)
    at javafx.graphics/javafx.scene.layout.Region.prefHeight(Region.java:1559)
    at javafx.graphics/javafx.scene.Scene.getPreferredHeight(Scene.java:1811)
    at javafx.graphics/javafx.scene.Scene.resizeRootToPreferredSize(Scene.java:1776)
    at javafx.graphics/javafx.scene.Scene.preferredSize(Scene.java:1747)
    at javafx.graphics/javafx.scene.Scene$2.preferredSize(Scene.java:393)
    at javafx.graphics/com.sun.javafx.scene.SceneHelper.preferredSize(SceneHelper.java:66)
    at javafx.graphics/javafx.stage.Window$SceneModel.invalidated(Window.java:814)
    at javafx.base/javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
    at javafx.base/javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:147)
    at javafx.graphics/javafx.stage.Window.setScene(Window.java:770)
    at javafx.graphics/javafx.stage.Stage.setScene(Stage.java:266)
    at app.jfx.CustomerList.<init>(CustomerList.java:79)
    at app.jfx.CustomerList.<init>(CustomerList.java:19)
    at app.jfx.LoggedInView$1.handle(LoggedInView.java:44)
    at app.jfx.LoggedInView$1.handle(LoggedInView.java:41)
    at javafx.base/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
    at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
    at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
    at javafx.base/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
    at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
    at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at javafx.base/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    at javafx.base/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)
    at javafx.base/javafx.event.Event.fireEvent(Event.java:198)
    at javafx.graphics/javafx.scene.Node.fireEvent(Node.java:8879)
    at javafx.controls/javafx.scene.control.Button.fire(Button.java:200)
    at javafx.controls/com.sun.javafx.scene.control.behavior.ButtonBehavior.mouseReleased(ButtonBehavior.java:206)
    at javafx.controls/com.sun.javafx.scene.control.inputmap.InputMap.handle(InputMap.java:274)
    at javafx.base/com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
    at javafx.base/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
    at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
    at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
    at javafx.base/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
    at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
    at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at javafx.base/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    at javafx.base/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
    at javafx.base/javafx.event.Event.fireEvent(Event.java:198)
    at javafx.graphics/javafx.scene.Scene$MouseHandler.process(Scene.java:3851)
    at javafx.graphics/javafx.scene.Scene$MouseHandler.access$1200(Scene.java:3579)
    at javafx.graphics/javafx.scene.Scene.processMouseEvent(Scene.java:1849)
    at javafx.graphics/javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2588)
    at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:397)
    at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:295)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$2(GlassViewEventHandler.java:434)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:390)
    at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:433)
    at javafx.graphics/com.sun.glass.ui.View.handleMouseEvent(View.java:556)
    at javafx.graphics/com.sun.glass.ui.View.notifyMouse(View.java:942)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:174)
    at java.base/java.lang.Thread.run(Thread.java:833)

I just want the buttons to display as part of the table, but the exception results in the entire table (apart from the column headers) failing to display any data.

Customer class looks like this (Model is an application-specific class that just reduces code duplication for several properties that have the same name across other classes):

public class Customer extends Model {
    //primary key    
    private int customerId;
    //customer data
    private String customerName; 
    private String address;    
    private String postalCode;
    private String phone;
    //foreign key to FirstLevelDivision
    private int divisionId;
    
    //all-arg constructor

    //getters and setters
}

The button is meant to be added with ActionButtonTableCell, which looks like this:

package app.jfx;
import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;
import java.util.function.Function;

public class ActionButtonTableCell<S> extends TableCell<S, Button> {
    private final Button actionButton;

    public ActionButtonTableCell(String label, Function< S, S> function) {
        this.getStyleClass().add("action-button-table-cell");
        this.actionButton = new Button(label);
        this.actionButton.setOnAction((ActionEvent e) -> {
            function.apply(getCurrentItem());        });
            this.actionButton.setMaxWidth(Double.MAX_VALUE);
        }
    public S getCurrentItem() {
        return (S) getTableView().getItems().get(getIndex());
    }

    public static <S> Callback<TableColumn<S, Button>, TableCell<S, Button>> forTableColumn(String label, Function< S, S> function) {
        return param -> new ActionButtonTableCell<>(label, function);
    }
    @Override
    public void updateItem(Button item, boolean empty) {
        super.updateItem(item, empty);
        if (empty) {
            setGraphic(null);
        } else {
            setGraphic(actionButton);
        }
    }
}

It appears in CustomerList, which looks like this:

package app.jfx;
import app.JDBC;
import app.model.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.GridPane;import javafx.stage.Stage;
import javafx.util.Callback;
import java.util.List;
import java.util.function.Function;

public class CustomerList extends Scene {
    private User user;
    public CustomerList(Stage stage) {
        this(stage, null, new GridPane());
    }    public CustomerList(Stage stage, User user) {
        this(stage, user, new GridPane());
    }
    private CustomerList(Stage stage, User user, GridPane gridPane) {
        super(gridPane);
        Label label = new Label("Customer Records");
        TableView tableView = new TableView();
        Button back = new Button("Back");
        back.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent actionEvent) {
                new LoggedInView(stage, user);
            }
        });
        gridPane.add(label, 0, 0);
        gridPane.add(tableView, 0, 1);
        gridPane.add(back, 0, 2);
        List<Customer> customers = JDBC.getCustomers();
        customers.forEach( customer -> tableView.getItems().add(customer));

        //define columns
        TableColumn<Customer, Integer> customerId = new TableColumn<>("Customer ID");
        TableColumn<Customer, String> customerName = new TableColumn<>("Customer Name");
        TableColumn<Customer, String> address = new TableColumn<>("Address");
        TableColumn<Customer, String> postalCode = new TableColumn<>("Postal Code");
        TableColumn<Customer, String> phone = new TableColumn<>("Phone");
        TableColumn<Customer, Integer> divisionId = new TableColumn<>("Division ID");
        TableColumn viewCol = new TableColumn("");
        TableColumn deleteCol = new TableColumn("");

        //set cell values
        customerId.setCellValueFactory(new PropertyValueFactory<>("customerId"));
        customerName.setCellValueFactory(new PropertyValueFactory<>("customerName"));
        address.setCellValueFactory(new PropertyValueFactory<>("address"));
        postalCode.setCellValueFactory(new PropertyValueFactory<>("postalCode"));
        phone.setCellValueFactory(new PropertyValueFactory<>("phone"));
        divisionId.setCellValueFactory(new PropertyValueFactory<>("divisionId"));
        viewCol.setCellValueFactory(ActionButtonTableCell.<Customer>forTableColumn("View/Edit", (Customer c) -> {
            new CustomerEditView(stage, user, c.getCustomerId());
            return c;
        }));
        deleteCol.setCellValueFactory(ActionButtonTableCell.<Customer>forTableColumn("Delete", (Customer c) -> {
            Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to delete this customer?");
            confirm.show();
            JDBC.deleteCustomer(c.getCustomerId());
            new CustomerList(stage, user);
            return c;        }));

        //render table
        tableView.getColumns().add(customerId);
        tableView.getColumns().add(customerName);
        tableView.getColumns().add(address);
        tableView.getColumns().add(postalCode);
        tableView.getColumns().add(phone);
        tableView.getColumns().add(divisionId);
        tableView.getColumns().add(viewCol);
        tableView.getColumns().add(deleteCol);
        stage.setScene(this);    }}

Adding in deleteCol is where the breaking started, yielding the exception above. If I specify viewCol and deleteCol as TableColumn<Customer, Button>, the compiler warns that setCellValueFactory requires a Callback<CellDataFeatures<Customer, Button>, ObservableValue<Button>> and I've provided a Callback<TableColumn<Customer, Button>, TableCell<Customer, Button> instead (but not if the types for TableColumn are not specified). Altering the return type of the ActionButtonTableCell method used breaks the @Override. What can I do to make this cast work (or make it unnecessary)? Is there another way to add the buttons in?

James_D
  • 201,275
  • 16
  • 291
  • 322
Gray
  • 3
  • 2
  • 2
    Looks like you are confusing `cellFactory` and `cellValueFactory`. Also, don’t use raw types. – James_D May 14 '23 at 17:35
  • 3
    This is something that’s usually necessary in web UIs, but in a desktop UI, a much better design is to have *one* delete button which is not in the table at all. When the user presses that button, the selected rows in the table are deleted. You can even enable the button based on the selection: `deleteButton.disableProperty().bind(table.getSelectionModel().selectedItemProperty().isNull())` – VGR May 14 '23 at 21:20

1 Answers1

4

There are a number of issues with your code. The primary problem is that you seem to be confusing the cellFactory (which is used to generate cells to display in the table column) with the cellValueFactory (which is used to provide the value that is displayed by the cells). In this case, the cell doesn't really need a value (it only relies on the value for the entire row), so you don't need a cellValueFactory (though you could use one to return the value for the row).

You should also never use raw types (such as TableView and TableColumn) without type parameters.

Since your action implementations never use the value they return, it seems to make more sense to use a Consumer<S> to represent them, instead of a Function<S,S> which seems to arbitrarily return the value passed as a parameter.

And finally, the types for TableView, TableColumn, TableCell, etc., should be types of data, not types of UI control. So you should never have, e.g., TableCell<S, Button>. Button is not a data type.

Your table cell implementation can look like

public class ActionButtonTableCell<S, T> extends TableCell<S, T> {
    private final Button actionButton;

    public ActionButtonTableCell(String label, Consumer<S> function) {
        this.getStyleClass().add("action-button-table-cell");
        this.actionButton = new Button(label);
        this.actionButton.setOnAction(e -> function.accept(getCurrentItem());
        this.actionButton.setMaxWidth(Double.MAX_VALUE);
    }
    public S getCurrentItem() {
        // No need for a cast here:
        return getTableView().getItems().get(getIndex());
    }

    public static <S, T> Callback<TableColumn<S, T>, TableCell<S, T>> forTableColumn(String label, Consumer<S> function) {
        return param -> new ActionButtonTableCell<>(label, function);
    }
    @Override
    public void updateItem(T item, boolean empty) {
        super.updateItem(item, empty);
        if (empty) {
            setGraphic(null);
        } else {
            setGraphic(actionButton);
        }
    }
}

And now your application code should be something like

    TableView<Customer> tableView = new TableView<>();
    // ...
    TableColumn<Customer, Void> viewCol = new TableColumn<>("");
    TableColumn deleteCol<Customer, Void> = new TableColumn<>("");

    // ...

    // note setCellFactory, not setCellValueFactory:
    viewCol.setCellFactory(ActionButtonTableCell.<Customer, Void>forTableColumn("View/Edit", c -> 
        new CustomerEditView(stage, user, c.getCustomerId())));
    deleteCol.setCellFactory(ActionButtonTableCell.<Customer, Void>forTableColumn("Delete", c -> {
        Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to delete this customer?");
        confirm.show();
        JDBC.deleteCustomer(c.getCustomerId());
        new CustomerList(stage, user);
    }));

Generally, you might consider other user experience options here. As mentioned in a comment on the original post, a better approach might be to have a single edit button and a single delete button outside of the table which just operates on the selected item.

I would definitely suggest not having a column for every action if you have multiple actions to perform. Just for the sake of demonstrating the other approach I alluded to above (the cell value factory returning the value for the row), here is an example of an action-based table column which uses that approach and supports multiple actions (buttons) in the same column. This is written in more of a fluid-API style:

import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.layout.HBox;

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

public class ActionColumn<S> {

    private final List<Action<S>> actions = new ArrayList<>();
    private String title ;

    private ActionColumn() {}

    public static <S> ActionColumn<S> forType(Class<S> type) {
        return new ActionColumn<>();
    }

    public ActionColumn<S> withTitle(String title) {
        this.title = title ;
        return this;
    }

    public ActionColumn<S> withAction(String name, Consumer<S> action) {
        actions.add(new Action<>(name, action));
        return this;
    }


    public TableColumn<S, ?> build() {
        TableColumn<S, S> column = new TableColumn<>(title);
        column.setCellValueFactory(data -> new SimpleObjectProperty<>(data.getValue()));
        column.setCellFactory(tc -> new ActionColumnCell<>(actions));
        return column;
    }

    private record Action<S>(String name, Consumer<S> action) {}

    private static class ActionColumnCell<S> extends TableCell<S, S> {
        private final HBox buttons ;
        private ActionColumnCell(List<Action<S>> actions) {
            buttons = new HBox(2);
            actions.stream()
                    .map(this::createButton)
                    .forEach(buttons.getChildren()::add);
        }

        private Button createButton(Action<S> action) {
            Button button = new Button(action.name());
            button.setOnAction(e -> action.action().accept(getItem()));
            return button;
        }

        @Override
        protected void updateItem(S item, boolean empty) {
            super.updateItem(item, empty);
            setGraphic(empty ? null : buttons);
        }
    }
}

Usage is something like:

TableColumn<Item, ?> actionColumn = ActionColumn.forType(Item.class)
    .withTitle("Action")
    .withAction("Edit", this::editItem)
    .withAction("Delete", table.getItems()::remove)
    .build();

Here is a complete working example:

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class HelloApplication extends Application {
    @Override
    public void start(Stage stage) {
        ObservableList<Item> items = FXCollections.observableArrayList();
        for (int i = 1 ; i <= 20 ; i++) items.add(new Item("Item "+i));
        TableView<Item> table = new TableView<>(items);

        TableColumn<Item, String> itemColumn = new TableColumn<>("Item");
        itemColumn.setCellValueFactory(data -> data.getValue().nameProperty());
        table.getColumns().add(itemColumn);


        table.getColumns().add(
                ActionColumn.forType(Item.class).withTitle("Action")
                        .withAction("Edit", this::editItem)
                        .withAction("Delete", items::remove)
                        .build()
        );

        BorderPane root = new BorderPane(table);
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }

    private void editItem(Item item) {
        new TextInputDialog(item.getName()).showAndWait()
            .ifPresent(item::setName);
    }

    public static class Item {
        private final StringProperty name = new SimpleStringProperty();

        public String getName() {
            return name.get();
        }

        public StringProperty nameProperty() {
            return name;
        }

        public void setName(String name) {
            this.name.set(name);
        }

        public Item(String name) {
            setName(name);
        }
    }

    public static void main(String[] args) {
        launch();
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322