Changing JavaFX table row styling using property binding

In my current project, we had the problem that JavaFX rows in TableView had to update their style based on a property. That property would not be visible for the user in any column. In our case, the client application would receive updates from the server and change row statuses to active/inactive in TableView.

There are many guides on how to create custom row factory that updates row styles, like rterp’s excellent example. When using visible properties this is pretty straightforward since updateItem() -method is always called when the property changes.

My first solution was to create columns that have their visibility set to false. Unfortunately, JavaFX does not call updateItem() -method if the column is not visible. Next, I created a column with zero width, but that left a narrow empty space. (Though you could probably get rid of it by messing around with CSS.)

One often suggested solution is to force the whole table to refresh by changing column visibility on and off. That solution did not play well with the FlashingTableCell which I created earlier, and it is also a bit sub-optimal since it calls updateItem() -method on every row/cell.

((TableColumn) getTableView().getColumns().get(0)).setVisible(false);
((TableColumn) getTableView().getColumns().get(0)).setVisible(true);

Eventually, I ended up making a binding between row- and item-level properties and updating the binding every time the item changed.

import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TableRow;

/**
 * Item change makes binding between table item and table row, to force
 * UpdateItem call when active property changes and We don't
 * have to force the update it's refresh with visibility true/false
 * trick. ex.
 *
 *  ((TableColumn) getTableView().getColumns().get(0)).setVisible(false);
 *  ((TableColumn) getTableView().getColumns().get(0)).setVisible(true);
 * 
 * Binding created is a weak reference so garbage collection should work properly.
 * 
 * @author jaakkju
 * @param 
 */
public class ToggleTableRow<T extends AbstractToggleTableItem> extends TableRow<T> {
    
    private final SimpleBooleanProperty active = new SimpleBooleanProperty();
    
    /* Current Item which is bound to this table row */
    private T currentItem = null;
    
    public ToggleTableRow() {
        super();

        active.addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
            
            /* If item is the same we know that the update came 
                from actual property change and not from the row reuse */
            
            if (currentItem != null && currentItem == getItem()) {
                updateItem(getItem(), isEmpty());
            }
        });

        /*
            JavaFX reuses rows in the same way as it reuses table cells, 
            item behind the row changes ex. if row if scrolled so that it is not visible.
         */
        itemProperty().addListener((ObservableValue<? extends T> observable, T oldValue, T newValue) -> {
            
            /* When the item changes, we unbind the
                old property and start listening to the new */
            
            active.unbind();
            
            if (newValue != null) {
                active.bind(newValue.activeProperty());
                
                /* We change current item only after
                    binding since since it trickers change in the properties */
                currentItem = newValue;
            }
        });
    }

    @Override
    final protected void updateItem(T item, boolean empty) {
        super.updateItem(item, empty);
        
        /* Setting disabled sets row's pseudoclass to disabled, we can use that
           value to assign inactive styling to the row. */
        
        setDisable(item != null && !item.isActive());
        setEditable(item != null && !item.isActive());
    }
}

Table items in the solution extend this simple abstract class with active boolean property.

import javafx.beans.property.SimpleBooleanProperty;
 
/**
 * Simple abstract class that has active boolean property
 * 
 * @author jaakkju
 */
abstract public class AbstractToggleTableItem {
 
    private final SimpleBooleanProperty active = new SimpleBooleanProperty(true);
 
    public AbstractToggleTableItem(boolean active) {
        this.active.set(active);
    }
 
    public SimpleBooleanProperty activeProperty() {
        return active;
    }
 
    public void setActive(boolean active) {
        this.active.set(active);
    }
 
    public boolean isActive() {
        return active.get();
    }
}

CSS styling using pseudoclasses.

.table-row-cell:disabled {
    -fx-background-color: #c1c1c1;
    -fx-control-inner-background: #c1c1c1;
}

 

Complete example with basic table implementation is available here.

Creating flashing table cell in JavaFX 8

In my current project with JavaFX, we need cells to flash when their value has been updated so that a user would notice the change. Although this sounds like an easy piece of code to write, it took three tries to get this right.

The first versions that I wrote used CSS manipulation and timer which updated the opacity value. This created a huge overhead since CSS handling in JavaFX is not effective and should be avoided in cell updateItem method if possible.

Eventually, I ended up with a solution that uses StackPane with BorderPane as a background and FadeTransition to handle the animation. Hopefully it helps if you are building something similar.

Conversation and examples can be found from Oracle forum:
https://community.oracle.com/thread/2312537

 

import java.util.Comparator;

import javafx.animation.FadeTransition;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.util.Duration;

import com.sun.javafx.scene.control.skin.LabeledText;

/**
 * 
 * @author jaakkju
 * @param <S>
 * @param <T>
 */
public class FlashingTableCell<S, T> extends TableCell<S, T> {

    private static final Color INCREASE_HIGHLIGHT_COLOR = Color.rgb(0, 255, 0, 0.8);
    private static final Color DECREASE_HIGHLIGHT_COLOR = Color.rgb(255, 0, 0, 0.8);
    private static final Color HIGHLIGHT_COLOR = Color.rgb(255, 255, 0, 0.8);
    private static final Duration HIGHLIGHT_TIME = Duration.millis(300);
    
    private final Background bgIncrease = new Background(new BackgroundFill(INCREASE_HIGHLIGHT_COLOR, CornerRadii.EMPTY, Insets.EMPTY));
    private final Background bgDecrease = new Background(new BackgroundFill(DECREASE_HIGHLIGHT_COLOR, CornerRadii.EMPTY, Insets.EMPTY));
    private final Background bgChange = new Background(new BackgroundFill(HIGHLIGHT_COLOR, CornerRadii.EMPTY, Insets.EMPTY));

    private final BorderPane background = new BorderPane();
    private final LabeledText lblText = new LabeledText(this);
    private final FadeTransition animation = new FadeTransition(HIGHLIGHT_TIME, background);

    private final StackPane container = new StackPane();

    private T prevValue;
    private S prevItem;

    final private Comparator<T> comparator;


    public FlashingTableCell(Comparator<T> comparator, Pos alignment) {
        super();
        this.comparator = comparator;

        lblText.textProperty().bindBidirectional(textProperty());
        this.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);

        setPadding(Insets.EMPTY);
        container.getChildren().addAll(background, lblText);
        container.setAlignment(alignment);
        setGraphic(container);
    }

    @Override
    protected void updateItem(T value, boolean empty) {
        super.updateItem(value, empty);

        S currentItem = getTableRow() != null && getTableRow().getItem() != null ? (S) getTableRow().getItem() : null;

        /*
         * We check that the value has been updated and that the row model/item
         * under the cell is the same. JavaFX table reuses cells so item is not
         * always the same!
         */
        boolean valueChanged = (prevValue == null && value != null)
                || (value != null && (prevValue.hashCode() != value.hashCode()));
        boolean sameItem = currentItem != null && prevItem != null && currentItem == prevItem;

        if (valueChanged && sameItem) {

            if (comparator != null) {
                int compare = comparator.compare(value, prevValue);
                if (compare > 0) {
                    background.setBackground(bgIncrease);
                } else if (compare < 0) {
                    background.setBackground(bgDecrease);
                }
            } else {
                background.setBackground(bgChange);
            }

            animation.setFromValue(1);
            animation.setToValue(0);
            animation.setCycleCount(1);
            animation.setAutoReverse(false);
            animation.playFromStart();
        }

        prevValue = value;
        prevItem = currentItem;
    }
}