Yunus Einsteinium
Yunus Einsteinium

Reputation: 1180

Display Sales information in TreeTableView

What I want to achieve is more or less having Sale object which when expanded to display SaleTransaction objects under that Sale object. Similar to want is displayed in this image from Code Project

enter image description here

where i can also do other CRUD functionality apart from just read(i.e Create, Update and delete).

I tried implementing it using TreeTableView like so:

 List<CreditSale> lstData = new ArrayList<CreditSale>(creditsaleservice.findAllCreditSales());

    TreeItem root = new TreeItem<>();
    for (CreditSaleTransaction cst : lstData.get(0).getCreditSaleTransaction()) {
        root.getChildren().addAll(new TreeItem<>(cst));
    }


    TreeTableColumn<CreditSaleTransaction, Product> productColumn = new TreeTableColumn<>("Product Name");

    productColumn.setPrefWidth(150);
    productColumn.setEditable(true);

    productColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<CreditSaleTransaction, Product> p) 
            -> new ReadOnlyObjectWrapper<>(p.getValue().getValue().getProduct()));


    TreeTableColumn<CreditSaleTransaction, Float>  quantityColumn = new TreeTableColumn<>("Quantity");
    quantityColumn.setPrefWidth(150);
    quantityColumn.setEditable(true);
    quantityColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<CreditSaleTransaction,Float> p)
            -> new ReadOnlyObjectWrapper<>(p.getValue().getValue().getAmount()));

    TreeTableColumn<CreditSaleTransaction, Float>  unitPColumn = new TreeTableColumn<>("Unit Price");
    unitPColumn.setPrefWidth(150);
    unitPColumn.setEditable(true);
    unitPColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<CreditSaleTransaction,Float> p)
            -> new ReadOnlyObjectWrapper<>(p.getValue().getValue().getUnitPrice()));

    TreeTableView<CreditSaleTransaction> treeTableView = new TreeTableView<>();
    treeTableView.setRoot(root);

    treeTableView.getColumns().addAll(productColumn,quantityColumn,unitPColumn);
    treeviewanchorpane.getChildren().add(treeTableView);

but nothing is displayed.

Upvotes: 0

Views: 871

Answers (1)

Peter
Peter

Reputation: 1612

It's possible to achieve a similar output with a TreeTableView provided you don't mind losing these inner headers:

Inner headers

Jewelsea has a good example of how to approach this here:


**Suggested approach for a custom implementation**

If the headers are important you can create a custom control to get the desired layout. My suggestion would be multiple TitledPane's within a container such as a VBox, with the outer item as the title and the inner items as the content within a TableView. TitledPane expects a String as the title, but with a combination of setGraphic and setContentDisplay , this can be changed to a Node allowing you to maintain a consistent layout

This approach would allow for multiple items (both inner and outer) to be visible at the same time.

In the event you only wish to be able to see the inner one at a time, an Accordian would be a suitable alternative to a VBox as it only allows one TitledPane to be expanded at any given time

Both TitledPane and Accordian are covered within this Oracle tutorial:

(I've included an example on how this approach could be implemented and an image displaying the output at the end of this answer)


**Monitoring changes**

JavaFX offers an ObservableList, which notifies listeners of any changes. Effective use of this would allow for the user interface to automatically reflect any changes.

For example, a ListChangeListener to detect when elements have been added/removed, which is covered here.

Note: If a child element within the ObservableList is modified (such as a price within a nested sales item), there may not be an update event. In this situation an extractor should be added to the list which specifies the additional values should be observed, firing the event updates if any of these values change

There are also a range of properties / bindings available, which are covered in the following pieces of documentation:

(The example contains examples of how to use the above)


Suggestions for implementing the CRUD functionality on `TableView`'s:
  • Creating a Button bar: You could place this under each TableView or the nestedView, using selected cells to determine when to enable the buttons and which objects to modify
  • Place an EventHandler on the TableView: See setOnEditCommit()
  • Adding custom cells to the TableView: There are several examples available on how to create custom TableCell's that delete the row when clicked.

    For example: Delete Rows using row buttons in Javafx TableView
  • Using ContextMenu's

Example:

I've added code comments where possible, but if there are any questions or suggested improvements please leave a comment

NestedTableView:

public class NestedTableView extends VBox {
    private ObservableList<Node> titledPanes = FXCollections.observableArrayList();
    private Region parent;

    public NestedTableView(Region parent, ObservableList<ProductBundle> bundlesToDisplay){
        this.parent = parent;
        VBox nestedView = new VBox();
        Bindings.bindContentBidirectional(nestedView.getChildren(), titledPanes);
        titledPanes.addAll(bundlesToDisplay.stream()
                .map(TablePane::new).collect(Collectors.toList()));
        getChildren().setAll(createHeader(), nestedView);
        getStylesheets().add("CSS/nestedTableViewStyles.css");
    }

    private HBox createHeader(){
        //Set up widths to align with the content headers beneath the header
        Label symbol = new Label("#");
        symbol.setPrefWidth(25); //Sum of the values used by the "arrow" region

        Label productId = new Label("Product Id");
        productId.prefWidthProperty().bind(parent.widthProperty().multiply(0.15));

        Label productName = new Label("Product Name");
        productName.prefWidthProperty().bind(parent.widthProperty().multiply(0.35)); //Give name extra space

        Label amount = new Label("Amount");
        amount.prefWidthProperty().bind(parent.widthProperty().multiply(0.15));

        Label date = new Label("Order Date");
        date.prefWidthProperty().bind(parent.widthProperty().multiply(0.15));

        Label quantityAvailable = new Label("#Available");
        quantityAvailable.prefWidthProperty().bind(parent.widthProperty().multiply(0.15));

        HBox header = new HBox(symbol, productId, productName, amount, date, quantityAvailable);
        header.getStyleClass().add("header");
        return header;
    }

    private class TablePane extends TitledPane {
        private ProductBundle productBundle;

        private HBox header;
        private TableView contentTableView;
        private MenuItem addToBundle, deleteBundle;

        public TablePane(ProductBundle productBundle){
            this.productBundle = productBundle;
            setupMenuItems();
            setupContentHeader();
            setGraphic(header);
            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
            if(!productBundle.getBundleItems().isEmpty()){
                createTableView();
                setContent(contentTableView);
            }

            //Only display the expandable "arrow" if there is content to display
            collapsibleProperty().bind(contentProperty().isNotNull());
            //If the "arrow" isn't displayed, pad the area to mimic the arrow being present to align headers
            header.paddingProperty().bind(
                    Bindings.when(collapsibleProperty()).then(Insets.EMPTY).otherwise(new Insets(0,0,0,15)));
            /*  For testing purposes. With more rows this will clutter the UI
                ToDo: add logic to determine how many panes to expand before the viewport has been filled
             */
            setExpanded(true);
        }

        private void setupMenuItems(){
            addToBundle = new MenuItem("Add to bundle");
            addToBundle.setOnAction(event -> {
                //ToDo: Add CRUD create logic here
                System.out.println("Add to bundle: " + productBundle.idProperty());
            });
            deleteBundle = new MenuItem("Delete bundle");
            deleteBundle.setOnAction(event -> {
                //ToDo: Add CRUD delete logic here
                System.out.println("Delete bundle: " + productBundle.idProperty());
            });
        }

        private void setupContentHeader(){
            header = new HBox();
            //Bind the content header to the root so that it aligned with the initial header
            header.prefWidthProperty().bind(parent.widthProperty());
            header.maxWidthProperty().bind(parent.widthProperty());

            /* Set up each TextField with widths to align with the TableView
               Each TextField is editable with the exception of id as it would be the primary key
               and amount as it's value is calculated from the sub items */
            TextField id = new TextField();
            id.setEditable(false);
            modifyTextFieldContextMenu(id);
            id.textProperty().bind(productBundle.idProperty());
            id.prefWidthProperty().bind(header.widthProperty().multiply(0.15));

            TextField name = new TextField();
            modifyTextFieldForCRUDFunctionality(name);
            name.textProperty().bindBidirectional(productBundle.nameProperty());
            name.prefWidthProperty().bind(header.widthProperty().multiply(0.35)); //Give name extra space

            TextField amount = new TextField();
            amount.setEditable(false);
            Bindings.bindBidirectional(amount.textProperty(), productBundle.amountProperty(),
                    new NumberStringConverter(NumberFormat.getCurrencyInstance(Locale.US)));
            amount.prefWidthProperty().bind(header.widthProperty().multiply(0.15));

            TextField date = new TextField();
            modifyTextFieldForCRUDFunctionality(date);
            date.textProperty().bind(productBundle.orderDateProperty());
            date.prefWidthProperty().bind(header.widthProperty().multiply(0.15));

            TextField quantityRemaining = new TextField();
            modifyTextFieldForCRUDFunctionality(quantityRemaining);
            //Only display a quantity if it's a valid value (to match example screen shot)
            quantityRemaining.textProperty().bind(
                    Bindings.when(productBundle.quantityAvailableProperty().greaterThan(0))
                            .then(productBundle.quantityAvailableProperty().asString()).otherwise("N/A"));
            quantityRemaining.prefWidthProperty().bind(header.widthProperty().multiply(0.15));

            header.getChildren().setAll(id, name, amount, date, quantityRemaining);
            header.getStyleClass().add("content-header");
        }

        private void modifyTextFieldContextMenu(TextField textField){
            TextFieldSkin skin = new TextFieldSkin(textField){
                @Override
                public void populateContextMenu(ContextMenu contextMenu) {
                    super.populateContextMenu(contextMenu);
                    contextMenu.getItems().add(0, addToBundle);
                    contextMenu.getItems().add(1, deleteBundle);
                    contextMenu.getItems().add(2, new SeparatorMenuItem());
                }
            };
            textField.setSkin(skin);
        }

        private void modifyTextFieldForCRUDFunctionality(TextField textField){
            textField.setEditable(true);
            textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
                private String previousText;
                @Override
                public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
                    String currentText = textField.getText();
                    if(newValue){
                        previousText = currentText;
                    }
                    //ToDo: Add CRUD update logic here
                    else if(!previousText.equals(currentText)){
                        System.out.println("Value has been changed from: " + previousText + " to: " + currentText);
                    }
                }
            });
        }

        private void createTableView(){
            TableColumn<BundleItem, String> idColumn = new TableColumn<>("#ID");
            idColumn.setCellValueFactory(param -> param.getValue().getItem().itemIdProperty());

            TableColumn<BundleItem, String> nameColumn = new TableColumn<>("Item");
            nameColumn.setCellValueFactory(param -> param.getValue().getItem().itemNameProperty());

            TableColumn<BundleItem, String> amountColumn = new TableColumn<>("Amount");
            amountColumn.setCellValueFactory(param -> param.getValue().getItem().amountProperty().asString("$%.2f"));

            TableColumn<BundleItem, Number> quantityColumn = new TableColumn<>("Qty");
            quantityColumn.setCellValueFactory(param -> param.getValue().quantityProperty());

            TableView<BundleItem> tableView = new TableView<>(productBundle.getBundleItems());
            tableView.setPadding(new Insets(10));
            //Equal column widths
            tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
            tableView.getColumns().setAll(idColumn, nameColumn, amountColumn, quantityColumn);
            //Only show visible shows
            tableView.setFixedCellSize(30);
            tableView.prefHeightProperty().bind(Bindings.size(productBundle.getBundleItems())
                    .multiply(tableView.getFixedCellSize()).add(tableView.getFixedCellSize()*1.5));
            contentTableView = tableView;
        }
    }
}

Sales related objects:

public class ProductBundle {
    private ObservableList<BundleItem> bundleItems = FXCollections.observableArrayList();
    private SimpleStringProperty productId, productName, orderDate;
    private SimpleDoubleProperty amount = new SimpleDoubleProperty();
    private SimpleIntegerProperty quantityAvailable;

    private ProductBundle(String productId, String productName, String orderDate, int quantityAvailable){
        this.productId = new SimpleStringProperty(productId);
        this.productName = new SimpleStringProperty(productName);
        this.orderDate = new SimpleStringProperty(orderDate);
        this.quantityAvailable = new SimpleIntegerProperty(quantityAvailable);
    }

    public ProductBundle(String productId, String productName, String orderDate,
                         int quantityAvailable, ObservableList<BundleItem> bundleItems){
        this(productId, productName, orderDate, quantityAvailable);
        //Setup an extractor to "Observe" changes on the amount/quantity of any items in the bundle
        this.bundleItems = FXCollections.observableArrayList(new Callback<BundleItem, Observable[]>() {
            @Override
            public Observable[] call(BundleItem param) {
                return new Observable[]{param.amountProperty(), param.quantityProperty()};
            }
        });
        this.bundleItems.addAll(bundleItems);
        //Calculate the total worth of this bundle
        amount.bind(Bindings.createDoubleBinding(()->
                bundleItems.stream().collect(Collectors.summingDouble(BundleItem::getAmount)), this.bundleItems)
                .multiply(quantityAvailable));
    }

    public ProductBundle(String productId, String productName, String orderDate,
                         int quantityAvailable, double amount){
        this(productId, productName, orderDate, quantityAvailable);
        this.amount.set(amount);
    }

    public ObservableList<BundleItem> getBundleItems(){
        return bundleItems;
    }

    public SimpleStringProperty idProperty(){
        return productId;
    }

    public SimpleStringProperty nameProperty(){
        return productName;
    }

    public SimpleIntegerProperty quantityAvailableProperty(){
        return quantityAvailable;
    }

    public SimpleStringProperty orderDateProperty(){
        return orderDate;
    }

    public SimpleDoubleProperty amountProperty(){
        return amount;
    }

    public double getAmount(){
        return amount.get();
    }
}

public class BundleItem {
    private Item item;
    private SimpleIntegerProperty quantity;
    private SimpleDoubleProperty amount = new SimpleDoubleProperty();

    public BundleItem(Item item, int quantity){
        this.item = item;
        this.quantity = new SimpleIntegerProperty(quantity);
        amount.bind(item.amountProperty().multiply(quantity));
    }

    public Item getItem(){
        return item;
    }

    public SimpleIntegerProperty quantityProperty(){
        return quantity;
    }

    public SimpleDoubleProperty amountProperty(){
        return amount;
    }

    public double getAmount(){
        return amount.get();
    }
}

public class Item {
    private SimpleStringProperty itemId, itemName;
    private SimpleDoubleProperty amount;

    public Item(String itemId, String itemName, double amount){
        this.itemId = new SimpleStringProperty(itemId);
        this.itemName = new SimpleStringProperty(itemName);
        this.amount = new SimpleDoubleProperty(amount);
    }

    public SimpleStringProperty itemIdProperty(){
        return itemId;
    }

    public SimpleStringProperty itemNameProperty(){
        return itemName;
    }

    public SimpleDoubleProperty amountProperty(){
        return amount;
    }

    public double getAmount(){
        return amount.get();
    }

    public void setAmount(double newValue){
        amount.set(newValue);
    }
}

nestedTableViewStyles.css:

.header {
    -fx-background-color: darkorange;
    -fx-pref-height: 30;
    -fx-padding: 5 0 0 0;
}

.header > .label {
    -fx-text-fill: white;
}

.header > .label, .content-header > .text-field {
    -fx-alignment: center;
    -fx-text-alignment: center;
}

.content-header > .text-field, .content-header > .text-field:focused {
    /* Make the TextField's display similar to a Label */
    -fx-background-color: transparent;
}

.content-header, .titled-pane > .title, .table-view {
    -fx-background-color: white;
}

.titled-pane > .title {
    -fx-border-color: lightgray;
    -fx-border-width: 0 0 1 0;
}

.table-view {
     -fx-table-cell-border-color: transparent;
}

.table-view .column-header-background {
    -fx-border-radius: 5 5 0 0;
    -fx-background-radius: 5 5 0 0;
}

.table-view .column-header-background, .table-row-cell {
    -fx-background-color: lightgray;
    -fx-border-color: gray;
    -fx-border-width: 0 0 1 0;
}

.table-view .column-header-background .label {
    -fx-background-color: lightgray;
    -fx-text-fill: black;
    -fx-font-weight: bold;
}

.table-view .column-header {
    -fx-background-color: transparent;
}

.table-column {
    -fx-alignment: center;
}

Usage:

public class NestedTableViewExample extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        ObservableList<ProductBundle> bundles =
                FXCollections.observableArrayList(
                        new ProductBundle("1001456", "Spring Season Gift", "02/14/2015", 1,
                                FXCollections.observableArrayList(
                                        new BundleItem(new Item("17890", "PS 3", 150.00), 1),
                                        new BundleItem(new Item("17891", "Heart shape ring", 100.00), 1)
                                )),
                        new ProductBundle("1001457", "Christmas Season Gift", "04/14/2015", 1,
                                FXCollections.observableArrayList(
                                        new BundleItem(new Item("17900", "Chocolate Giftbox", 150.00), 1),
                                        new BundleItem(new Item("17901", "Xbox 360", 199.00), 1)
                                )),
                        new ProductBundle("1001458", "Birthday Gift", "", 1, 200)
                );

        VBox root = new VBox();
        root.getChildren().setAll(new NestedTableView(root, bundles));
        Scene scene = new Scene(root, 500, 500);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Nested TableView example");
        primaryStage.show();
    }
}

**Output:**

Nested TableView Example

Upvotes: 2

Related Questions