Search code examples
scalascalafx

Erroneous data in some cells of tableview


New to scala and scalafx and having an issue with a tableview in a simple stock quote app [note: no prior javafx experience except in ways it is similar to things Android]

Problem: (see image) erroneous data displayed in change column where there should be none.

To create: Multiple scenarios, shown here is entry of new ticker symbol. It seems unpredictable how many cells are in error. Changing window size (say shorter then taller) generally creates more bad cells. Never happens if no change to windowsize and/or symbol list are made.

Code for the 'ticker' and 'change' columns below, though I suspect it is something in how I implemented the change column to do green/red text coloring.

(Note: data is updated in a single batch periodically which is why the new symbol does not immediately display quote data)

      val colTicker = new TableColumn[Quote, String] {
      editable = true
      text = "Ticker"
      prefWidth = 80
      alignmentInParent = scalafx.geometry.Pos.Center
      cellValueFactory = {
        _.value.ticker
      }
      cellFactory = _ => new TextFieldTableCell[Quote, String](new DefaultStringConverter())
      onEditCommit = (evt: CellEditEvent[Quote, String]) => {
        val quote: Quote = evt.rowValue
        val newTickerVal: String = evt.newValue.toUpperCase()
        val oldTickerVal: String = evt.oldValue
        // is it a valid ticker and not a dupe or is it blank (erase old ticker)?
        if ((isValidTicker(newTickerVal) || newTickerVal.length == 0) && !symbolList.contains(newTickerVal)) {
          // lock in the new value on the screen
          quote.ticker.set(newTickerVal)
          // if the new value is not empty add it to symbol list
          if (newTickerVal.length > 0) {
            symbolList.append(newTickerVal)
          }
          // now delete the old value
          symbolList -= oldTickerVal
          // sort and add another blank line
          characters.sortWith(_.ticker.getValueSafe < _.ticker.getValueSafe)
          if (oldTickerVal.length < 1) characters += Quote()
          // now need to update the data file
          putListToFile(dataFile, symbolList.sorted)
        } else {
          // bad ticker so keep the old one and don't update file
          quote.ticker.set(oldTickerVal)
          evt.getTableView.getColumns.get(0).setVisible(false)
          evt.getTableView.getColumns.get(0).setVisible(true)
          println("bad ticker, exiting symbol list: " + symbolList)
        }
      }

    }
    val colLast = new TableColumn[Quote, String] {
      editable = false

      text = "Last"
      cellValueFactory = {
        _.value.last
      }

      prefWidth = 80
      alignmentInParent = scalafx.geometry.Pos.Center
    }
    val colChange = new TableColumn[Quote, String] {
      editable = false
      text = "Change"
      cellFactory = {
        _ =>
          new TableCell[Quote, String] {
              item.onChange { (_, _, newChange) =>
                if (newChange != null) {
                  if (newChange.toString.contains("+")) textFill = Color.Green
                  else textFill = Color.Red
                  text = newChange
                }
              }
          }
      }
      cellValueFactory = {
        _.value.change
      }
      prefWidth = 80
      alignmentInParent = scalafx.geometry.Pos.Center
    }

enter image description here


Solution

  • JavaFX is reusing cells when rendering. This is especially noticeable when dynamically updating TableView content. Your cellFactory has to clear cell content when receiving and empty or null item: text and graphic need to be set to null. It may be sufficient to simply check for newChange == null

    cellFactory = { _ =>
      new TableCell[Quote, String] {
        item.onChange { (_, _, newChange) =>
          if (newChange == null) {
            text = null
            graphic = null
          else {
            if (newChange.toString.contains("+")) textFill = Color.Green
            else textFill = Color.Red
            text = newChange
          }
        }
      }
    }
    

    It that is nor reliable you will have to implement the cellFactory the JavaFX way by implementing javafx.scene.control.TableCell and overwriting method updateItem that is passing in the empty flag:

    cellFactory = {_ =>
      new javafx.scene.control.TableCell[Quote, String] {
        override def updateItem(item: String, empty: Boolean): Unit = {
          super.updateItem(item, empty) 
          if (item == null || empty) {
             text = null
             graphic = null
          }
          else {
            ...
          }
        }
      }
    }