Es gibt ein paar Aufgaben in der Welt der Programmierung, die seltsamerweise seit vielen Jahren auf eine elegante Lösung warten, obwohl es sich um wichtige Aufgaben handelt und sich immer wieder sehr intelligente Leute um eine Lösung bemühen. Die effiziente interaktive Anzeige eines grossen Datenbestandes, auf den man per SQL zugreift, scheint mir eine solche Aufgabe zu sein.
Konkret: Nehmen wird an, man hat eine Tabelle, die nicht zwangsläufig gross ist, aber doch gross werden kann, und will sie in einer DataGridView anzeigen. Das Ganze soll auch dann noch ordentlich funktionieren, wenn die Tabelle mal 1 Million Rows umfasst.
Auf meiner Suche nach einer Lösung für dieses Problem war ich zunächst optimistisch, weil die DataGridView vor einiger Zeit ein Feature namens VirtualMode dazugelernt hat. Vor diesem Feature hätte die View bei einem Einsatz von „DataBinding“ mal eben die Million Rows aus der Tabelle gelesen, was natürlich keine gute Idee ist. Mit VirtualMode kann man selbst die Daten häppchenweise laden, weil die View per CellValueNeeded jeweils bekanntgibt, welche Daten für die Anzeige gerade benötigt werden.
Das Lesen und Liefern der Daten aus einer SQL-Datenquelle in „Häppchen“ ist zwar nicht trivial, wie einschlägiger Demo-Code zeigt, aber machbar.
Was mir allerdings an dieser Lösung nicht gefallen wollte, war die Tatsache, dass die View auch im VirtualMode alle Zeilen und Zellen erzeugt, also im vorliegenden Beispiel 1 Million Zeilen. Es werden nur die Daten „virtualisiert“, das Grid ist aber immer in seiner ganzen monumentalen Grösse da und frisst Speicher.
Ich habe darum für die neue Programmierumgebung Triton der Megos, die gerade entsteht, und deren View auf grosse Tabellen einen Ansatz implementiert, den ich seltsamerweise so noch nirgendwie sonst gefunden habe. Ich will nicht behaupten, der Ansatz stelle die vielgesuchte Lösung für das hier diskutierte hartnäckige Problem dar, denn er hat auch seine Nachteile, aber ich denke, das Resultat ist ganz ordentlich:
Ich verwende eine DataGridView als reines Anzeige-Instrument: Kein DataBinding, kein VirtualMode, einfach eine Anzahl Zeilen, sagen wir als Beispiel einmal 30, die alle ganz sichtbar sind, so dass die View findet, vertikal gäbe es nichts zu rollen. Die View hat sozusagen keine Ahnung von den Daten, die sie anzeigt.
Ich lese Rows selbst aus der Tabelle mit Hilfe eines SqlDataReader. Um nach dem erstmaligen Öffnen und Anzeigen der View ihre 30 Zeilen mit den ersten 30 Rows aus den Daten zu füllen, führe ich ein SELECT * FROM Tabelle aus, hole mit Hilfe des Readers die ersten 30 Rows ab und fülle deren Daten in die entsprechenden Zellen.
Die SELECT-Anweisung ist schon richtig: Sie geht über die ganze Tabelle. Also kein SELECT TOP n oder ähnliches. Sorgfältige Tests haben gezeigt, dass eine solche Anweisung völlig harmlos ist, wenn die angegebene ORDER durch einen Index unterstützt wird. Weder wird der Datenbank-Server über Gebühr belastet, weil er versucht, die ganze Tabelle im Voraus zu lesen, noch werden Unmengen von Daten vorauseilend über das Netz gesendet.
Positioniert nun der Anwender die Selektion auf die letzte angezeigte Row und drückt Cursor Down, schiebe ich zunächst selbst die View „nach oben“, indem ich die Inhalte der Zellen umkopiere: Die Werte aus der 2. Zeile kommen in die 1. Zeile, die Werte aus der 3. Zeile kommen in die 2. Zeile, usw. Das mag Ihnen auf den ersten Blick vielleicht sehr seltsam vorkommen, denn natürlich schiebt im Normalfall die View völlig selbständig, aber hier kann sie es nicht, weil es eben keine 31. Zeile gibt, welche die View von sich aus in den sichtbaren Bereich hineinschieben könnte. Man muss es selber tun, was aber nicht weiter kompliziert ist.
Die Daten für die quasi „neu erscheinende“ Zeile beschaffe ich mir, indem ich per Reader noch eine Row mehr abhole. Der SqlDataReader hat ja – wie gesagt ohne schädliche Nebenwirkungen – brav auf mich gewartet, in einem „offenen“ Zustand, positioniert hinter Row 30.
Solange es also in der View abwärts geht, hole ich einfach weiter Rows ab mit Hilfe des Readers und schiebe Zelleninhalte selber nach oben.
Die Sache wird nur moderat komplizierter, wenn der Anwender etwas verlangt, was zu einer Repositionierung des Daten-Ausschnitts oder zumindest zu einem Richtungswechsel bei der Anzeige führt. Dann muss ich den Reader freigeben und einen neuen aufsetzen, in der Richtung, in der es bei der Anzeige gehen soll, rückwärts also mit Hilfe von DESC. Ich muss mir zudem die Schlüssel aller 30 in der View angezeigten Zeilen merken, damit ich bei einem Richtungswechsel die Schlüsselwerte für eine WHERE x < <key> bzw. WHERE x > <key>-Klausel zur Verfügung habe beim Öffnen des neuen Readers.
Was es auch noch braucht, ist eine vertikale Scrollbar, denn die View zeigt ja wie gesagt von sich aus keine solche an, weil alle ihre 30 Zeilen stets ganz sichtbar sind, und von mehr weiss sie nichts. Man muss sich also wiederum wie beim Umkopieren der Zellen-Werte um etwas kümmern, was die View sonst immer vollständig alleine macht, und alle 7 Scroll-Funktionen (Anfang, Ende, Zeile auf, Zeile ab, Seite auf, Seite ab, Positionieren) nachbauen.
Und zu guter Letzt muss man sogar beim Rollen die Selektion selber umpositionieren, denn vom Standpunkt der View aus wird nichts gerollt, weshalb sie auch die Selektion nicht bewegt.
Die resultierende Kombination DataGridView / VScrollBar / SqlDataReader braucht ein paar Seiten Code, um zu funktioneren, aber der Code ist über weite Strecken relativ einfach und durchsichtig. Auf irgendwelche komplizierten „Paging“-Konstrukte, stored procedures usw. kann man verzichten, und die Datenmenge in der Tabelle ist mehr oder weniger egal: Es dürfen auch 100 Millionen Records sein, wenn es sein muss.