2016-04-29 13 views
1

Angenommen, ich zeige sehr lange Tabelle mit TableView. Handbuch sagt, TableViewJavaFX schlechtes Design: Zeilenidentität in beobachtbaren Listen hinter dem TableView?

ausgelegt ist, eine unbegrenzte Anzahl von Datenzeilen

So, da Millionen von Zeilen, zu visualisieren, das RAM nicht passen, ich würde einige Caching einführen. Dies bedeutet, dass ObservableList#get() verschiedene Instanzen für denselben Zeilenindex zurückgeben darf.

Ist das wahr?

Was ist mit dem Gegenteil? Kann ich dieselbe Instanz für alle Zeilenindizes mit unterschiedlichen Daten zurückgeben?

Ich bemerkte, dass dies ein Problem mit der Zeilenbearbeitung impliziert. In welchem ​​Moment sollte ich Daten an den Laden weitergeben? Sieht aus wie TableView ruft nie ObservableList#set(), aber nur ermittelte Instanz mutiert.

Wo abzufangen?

UPDATE

vorstellen, auch in diesem sehr großen Tisch auf Server-Seite wurde aktualisiert. Angenommen, eine Million Datensätze wurden hinzugefügt.

Die einzige Möglichkeit, darüber zu berichten - ist durch Auslösen beobachtbarer Liste Zusatzereignis, während ein Zusatzereignis enthält auch einen Verweis auf alle hinzugefügten Zeilen. Was ist Unsinn - warum senden Daten, die nicht Ereignis angezeigt wird?

+1

Nein, eine beobachtbare Liste darf nicht verschiedene Instanzen für den gleichen Index zurückgeben - schließlich ist es immer noch eine Liste. Ich denke, Sie sind verwirrt mit der Knotenvirtualisierung - das 'TableView' hat nur genug Knoten, um seine sichtbaren Bereiche zu füllen, und verwendet diese Knoten erneut, um die Elemente in der 'ObservableList' zu zeigen. Ich bin nicht vertraut genug mit den inneren Funktionen, um eine vollständige Antwort zu liefern, aber das scheint wie eine gute lesen: http://fxexperience.com/2009/09/ui-virtualization/ – Itai

+0

Unfähigkeit, verschiedene Instanzen zurückgeben widerspricht der Anspruch 'TableView 'kann mit unbegrenzter Liste arbeiten. –

+0

Beachten Sie, dass TableView eine beobachtbare Liste anzeigt. Es spielt keine Rolle, wie viele visuelle Knoten TableView hat. Ich spreche über die Anzahl der Zeilen von Backend-Daten. Angenommen, Sie haben 100.000.000 Zeilen und Sie werden verstehen, dass es unmöglich ist, immer die gleiche Instanz für jeden Knoten zurückzugeben. –

Antwort

4

denke ich die Absicht der Aussage in der Javadocs, dass Sie zitieren

ausgelegt ist, eine unbegrenzte Anzahl von Datenzeilen

gemeint zu visualisieren ist zu implizieren, dass die TableView nicht erlegt (zusätzliche) Einschränkungen für die Größe der Tabellendaten: Mit anderen Worten, die Ansicht ist bei einem konstanten Speicherverbrauch im Wesentlichen skalierbar. Dies wird durch die "Virtualisierung" erreicht: Die Tabellenansicht erzeugt Zellen nur für die sichtbaren Daten und verwendet sie für verschiedene Elemente in der Unterstützungsliste, wie z. B. der Benutzer scrollt. Da in einer typischen Anwendung die Zellen (die graphisch sind) viel mehr Speicher als die Daten verbrauchen, stellt dies eine große Leistungseinsparung dar und ermöglicht so viele Zeilen in der Tabelle, wie der Benutzer sie möglicherweise handhaben könnte.

Es gibt natürlich noch andere Einschränkungen für die Tabellendatengröße, die nicht von der Tabellenansicht auferlegt werden. Das Modell (d. H. Die beobachtbare Liste) muss die Daten speichern, und folglich werden Speicherbeschränkungen (in der Standardimplementierung) eine Beschränkung für die Anzahl der Zeilen in der Tabelle auferlegen. Sie könnten eine Cache-Liste (siehe unten) implementieren, um den Speicherbedarf bei Bedarf zu reduzieren. Und wie @fabian in den Kommentaren unter der Frage darauf hinweist, wird die Benutzererfahrung wahrscheinlich lange vor dem Erreichen dieses Punkts Einschränkungen auferlegen (ich würde die Verwendung von Paginierung oder eine Art Filterung empfehlen).

Ihre Frage nach der Identität von Elementen aus der Liste abgerufen ist relevant in einer Caching-Implementierung: es läuft grundsätzlich nach unten, ob eine Liste Umsetzung verpflichtet ist, list.get(i) == list.get(i) zu gewährleisten, oder ob es genug nur ist list.get(i).equals(list.get(i)) zu gewährleisten.Soweit ich weiß, erwartet TableView nur Letzteres, daher sollte eine Implementierung von ObservableList, die eine relativ kleine Anzahl von Elementen zwischenspeichert und sie nach Bedarf neu erstellt, funktionieren.

Für Proof of Concept, hier ist eine Implementierung einer unmodifiable Caching beobachtbaren Liste:

import java.util.LinkedList; 
import java.util.function.IntFunction; 
import java.util.stream.Collectors; 
import java.util.stream.IntStream; 

import javafx.collections.ObservableListBase; 

public class CachedObservableList<T> extends ObservableListBase<T> { 

    private final int maxCacheSize ; 
    private int cacheStartIndex ; 
    private int actualSize ; 
    private final IntFunction<T> generator ; 

    private final LinkedList<T> cache ; 

    public CachedObservableList(int maxCacheSize, int size, IntFunction<T> generator) { 
     this.maxCacheSize = maxCacheSize ; 
     this.generator = generator ; 

     this.cache = new LinkedList<T>(); 

     this.actualSize = size ; 
    } 

    @Override 
    public T get(int index) { 

     int debugCacheStart = cacheStartIndex ; 
     int debugCacheSize = cache.size(); 

     if (index < cacheStartIndex) { 
      // evict from end of cache: 
      int numToEvict = cacheStartIndex + cache.size() - (index + maxCacheSize); 
      if (numToEvict < 0) { 
       numToEvict = 0 ; 
      } 
      if (numToEvict > cache.size()) { 
       numToEvict = cache.size(); 
      } 
      cache.subList(cache.size() - numToEvict, cache.size()).clear(); 

      // create new elements: 
      int numElementsToCreate = cacheStartIndex - index ; 
      if (numElementsToCreate > maxCacheSize) { 
       numElementsToCreate = maxCacheSize ; 
      } 
      cache.addAll(0, 
        IntStream.range(index, index + numElementsToCreate) 
        .mapToObj(generator) 
        .collect(Collectors.toList())); 

      cacheStartIndex = index ; 

     } else if (index >= cacheStartIndex + cache.size()) { 
      // evict from beginning of cache: 
      int numToEvict = index - cacheStartIndex - maxCacheSize + 1 ; 
      if (numToEvict < 0) { 
       numToEvict = 0 ; 
      } 
      if (numToEvict >= cache.size()) { 
       numToEvict = cache.size(); 
      } 

      cache.subList(0, numToEvict).clear(); 

      // create new elements: 

      int numElementsToCreate = index - cacheStartIndex - numToEvict - cache.size() + 1; 
      if (numElementsToCreate > maxCacheSize) { 
       numElementsToCreate = maxCacheSize ; 
      } 

      cache.addAll(
        IntStream.range(index - numElementsToCreate + 1, index + 1) 
        .mapToObj(generator) 
        .collect(Collectors.toList())); 

      cacheStartIndex = index - cache.size() + 1 ; 
     } 

     try { 
      T t = cache.get(index - cacheStartIndex); 
      assert(generator.apply(index).equals(t)); 
      return t ; 
     } catch (Throwable exc) { 
      System.err.println("Exception retrieving index "+index+": cache start was "+debugCacheStart+", cache size was "+debugCacheSize); 
      throw exc ; 
     } 

    } 

    @Override 
    public int size() { 
     return actualSize ; 
    } 

} 

Und hier ist ein kurzes Beispiel unter Verwendung es, dass 100 Millionen Zeilen in der Tabelle hat. Offensichtlich ist dies aus Sicht der Benutzererfahrung unbrauchbar, aber es scheint perfekt zu funktionieren (selbst wenn Sie die Cachegröße so ändern, dass sie kleiner ist als die Anzahl der angezeigten Zellen).

import java.util.Objects; 

import javafx.application.Application; 
import javafx.beans.property.SimpleStringProperty; 
import javafx.beans.property.StringProperty; 
import javafx.scene.Scene; 
import javafx.scene.control.TableColumn; 
import javafx.scene.control.TableView; 
import javafx.stage.Stage; 

public class CachedTableView extends Application { 

    @Override 
    public void start(Stage primaryStage) { 
     CachedObservableList<Item> data = new CachedObservableList<>(100, 100_000_000, i -> new Item(String.format("Item %,d",i))); 

     TableView<Item> table = new TableView<>(); 
     table.setItems(data); 

     TableColumn<Item, String> itemCol = new TableColumn<>("Item"); 
     itemCol.setCellValueFactory(cellData -> cellData.getValue().nameProperty()); 
     itemCol.setMinWidth(300); 
     table.getColumns().add(itemCol); 

     Scene scene = new Scene(table, 600, 600); 
     primaryStage.setScene(scene); 
     primaryStage.show(); 
    } 

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


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

     public final StringProperty nameProperty() { 
      return this.name; 
     } 


     public final String getName() { 
      return this.nameProperty().get(); 
     } 


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

     @Override 
     public boolean equals(Object o) { 
      if (o.getClass() != Item.class) { 
       return false ; 
      } 
      return Objects.equals(getName(), ((Item)o).getName()); 
     } 
    } 


    public static void main(String[] args) { 
     launch(args); 
    } 
} 

Es gibt offensichtlich noch viel mehr zu tun, wenn Sie die Liste so implementieren möchten, dass sie änderbar ist; Beginnen Sie damit, genau darüber nachzudenken, welches Verhalten Sie für set(index, element) benötigen würden, wenn index nicht im Cache ist ... und dann Unterklasse ModifiableObservableListBase.

Zur Bearbeitung:

Ich bemerkte, dass dies ein Problem mit Zeilen Bearbeitung impliziert. In welchem ​​Moment sollte ich Daten an den Laden weitergeben? Es sieht so aus, als ob TableView niemals ObservableList # set() aufruft, sondern nur die erhaltene Instanz mutiert.

Sie haben drei Möglichkeiten, die ich sehen kann:

Wenn Ihre Domain-Objekte JavaFX Eigenschaften verwenden, dann ist das Standardverhalten die Eigenschaft zu aktualisieren, wenn sich verpflichtet Bearbeitung. Sie können Listener mit den Eigenschaften registrieren und den Sicherungsspeicher aktualisieren, wenn sie sich ändern.

Alternativ können Sie einen onEditCommit Handler mit der TableColumn registrieren; Dies wird benachrichtigt, wenn eine Änderung in der Tabelle festgeschrieben wird, und Sie können den Speicher damit aktualisieren. Beachten Sie, dass dies das standardmäßige Bearbeitungs-Commit-Verhalten ersetzt. Daher müssen Sie auch die Eigenschaft aktualisieren. Dies gibt Ihnen die Möglichkeit, ein Veto gegen die Aktualisierung der zwischengespeicherten Eigenschaft einzulegen, wenn die Aktualisierung des Speichers aus irgendeinem Grund fehlschlägt. Dies ist wahrscheinlich die gewünschte Option.

Drittens, wenn Sie die Bearbeitungszellen selbst implementieren, anstatt Standardimplementierungen wie TextFieldTableCell zu verwenden, können Sie Methoden auf dem Modell direkt über die Steuerelemente in der Zelle aufrufen. Dies ist wahrscheinlich nicht wünschenswert, da es gegen die Standardentwurfsmuster verstößt und die üblichen Bearbeitungsbenachrichtigungen vermeidet, die in der Tabellenansicht eingebaut sind, aber es kann in einigen Fällen eine nützliche Option sein.

Stellen Sie sich auch vor, diese sehr große Tabelle wurde auf Serverseite aktualisiert. Angenommen, eine Million Datensätze wurden hinzugefügt.

Die einzige Möglichkeit, darüber zu berichten - ist durch Auslösen beobachtbarer Liste Zusatzereignis, während ein Zusatzereignis enthält auch einen Verweis auf alle hinzugefügten Zeilen.

Das stimmt nicht, soweit ich das beurteilen kann.ListChangeListener.Change hat eine getAddedSublist() Methode, aber die API-Dokumentation für diesen Zustand gibt es

eine Unterliste Ansicht der Liste, die nur die Elemente enthält

hinzugefügt

so sollte es einfach getItems().sublist(change.getFrom(), change.getTo()) zurückzukehren. Natürlich gibt dies einfach eine Unterlistenansicht der zwischengespeicherten Listenimplementierung zurück, sodass die Objekte nicht erstellt werden, es sei denn, Sie haben sie explizit angefordert. (Beachten Sie, dass getRemoved() könnte möglicherweise mehr Probleme verursachen, aber es sollte eine gewisse Art und Weise um, dass zu arbeiten sein.)

schließlich diesen Kreis zu bringen, während die beobachtbaren Liste Umsetzungsarbeiten hier zu tun ist, um die Elemente der Zwischenspeicherung und Herstellung das Modell "unlimited" in der Anzahl der Zeilen, die es unterstützen kann (bis zu Integer.MAX_VALUE), wäre es nicht möglich, dies in der Tabellenansicht zu verwenden, wenn die Tabellenansicht keine "Virtualisierung" implementiert hätte. Eine nicht virtualisierte Implementierung der Tabellenansicht würde Zellen für jedes Element in der Liste erstellen (dh es würde get(i) für aufrufen, für jede Zelle eine Zelle erstellen), die Zellen in einer Bildlauffensterimplementierung platzieren und der Speicherverbrauch würde sogar in die Höhe schießen mit dem Caching in der Liste. So bedeutet "unbegrenzt" in den Javadocs wirklich, dass jedes Limit auf die Implementierung des Modells verschoben wird.