Laden...

Performantere Alternative um 200.000+ Einträge im DataView durchzuscrollen?

Erstellt von nicky vor 8 Jahren Letzter Beitrag vor 8 Jahren 1.881 Views
N
nicky Themenstarter:in
232 Beiträge seit 2011
vor 8 Jahren
Performantere Alternative um 200.000+ Einträge im DataView durchzuscrollen?

Hallo,

ich möchte mit meinem DataGrid 200.000+ Einträge aus einer DB durchscrollen können. Meine Anwendung ist nach dem MVVM Pattern aufgebaut und ich benutze demnach ObservableCollection als ItemSource.

Die Datenbankabfrage geht schnell, das ist kein Problem. Allerdings dauert es 15 Sekunden um die ObservableCollection zu erstellen. Sobald die Collection einmal erstellt ist, klappt das durchscrollen danke der Virtualization ganz gut.

Möglicherweise sollte ich nur den Ausschnitt aus der Datenbank auslesen, der auch angezeigt werden soll. Allerdings habe ich keine gute Lösung gefunden, bei der ich das DataGrid mit normaler Geschwindigkeit durchscrollen könnte. Mein Ansatz war die ersten 20 Einträge (Skip, Take) anzuzeigen und weitere Einträge per ScrollViewer.ScrollChanged hinzuzufügen. Allerdings würde ich dabei bei jedem Scrollen eine Datenbank-Abfrage starten?! Das kann doch auch nicht die Lösung sein.

Beispiele die ich im Netz gefunden habe verwenden alle die klassische Pagination mit den Previous und Next Buttons. Das ist für mich in der Form eigentlich keine Option.

Hat das jemand schon mal gemacht oder einen Lösungsansatz parat?

nicky

C
2.121 Beiträge seit 2010
vor 8 Jahren

Angenommen es passen 50 Datensätze auf eine Seite, dann sinds immer noch 4000 Seiten die gescrollt werden müssen. Bei einer Seite pro Sekunde braucht man dazu über eine Stunde. Dann hat man sich aber immer noch nichts davon angesehen.
Was ist der Nutzen von so einer Ansicht?

Ein Lösungsansatz wäre das nicht zu machen 😃
Sondern zu überlegen was ein Benutzer in dieser Ansicht tun will und wie er das erreichen kann. Mit Filtern oder Suchfeldern usw.

74 Beiträge seit 2014
vor 8 Jahren

Guten Morgen,

du könntest es auch so machen wie hier: http://www.codeproject.com/Articles/34405/WPF-Data-Virtualization

Da wird eine eigene Collection-Klasse erstellt, die ihre Items seitenweise hält und bei Bedarf Seiten nachlädt. Nicht benötigte Seiten werden dereferenziert und dem GC überlassen.

Grüße

3.003 Beiträge seit 2006
vor 8 Jahren

Zum einen ist die Anforderung, denke ich, falsch formuliert. Kein Anwender möchte durch 200.000 Einträge scrollen, sondern höchstwahrscheinlich interessieren bestimmte Einträge. chilic hat dazu ja alles gesagt.

Dennoch kann die Anzahl der gefilterten Ergebnisse natürlich auch immer noch so groß sein, dass Paging sinnvoll wird. In dem Fall könnte man den Prozess für den Benutzer scheinbar verschnellern, indem man

  • herausfindet, was der Benutzer vermutlich als nächstes tun wird (Seite vor/zurück? Mehrere Seiten vor? Weiteres Eingrenzen der Ergebnisse?)
  • die entsprechenden Ergebnisse im Hintergrund bereits bereitstellt

Software mit einer UI dreht 95% der Zeit Däumchen. Die Zeit kann man sich zu Nutze machen.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

N
nicky Themenstarter:in
232 Beiträge seit 2011
vor 8 Jahren

Die Anforderung ist natürlich nicht falsch formuliert. Der Anwender möchte durch 200k Einträge scrollen, sonst hätte ich es ja nicht geschrieben.

@Lando, vielen Dank - ich glaube das ist genau das richtige! 😁

3.003 Beiträge seit 2006
vor 8 Jahren

Die Anforderung ist natürlich nicht falsch formuliert. Der Anwender möchte durch 200k Einträge scrollen, sonst hätte ich es ja nicht geschrieben.

Denke, solche Anforderungen kennt jeder. Die richtige Formulierung lautet in den meisten Fällen "der Anwender glaubt, er möchte durch 200k Einträge scrollen". 😉

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

P
1.090 Beiträge seit 2011
vor 8 Jahren

Wenn du es nicht schon machst, Binde mal die ObservableCollection erst nach dem Befüllen.

Sollte man mal gelesen haben:

Clean Code Developer
Entwurfsmuster
Anti-Pattern

2.079 Beiträge seit 2012
vor 8 Jahren

Wenn du es nicht schon machst, Binde mal die ObservableCollection erst nach dem Befüllen.

... oder schreibe eine Ableitung davon, die AddRange hat.
In dieser AddRange-Methode wird zuerst das Werfen von Events unterdrückt, alle ELemente hinzu gefügt und dann einmal ein Event geworfen.

Das finde ich besser, denn dann muss man nicht darauf achten, immer vor der Inizialisierung des Controls zu befüllen.

N
nicky Themenstarter:in
232 Beiträge seit 2011
vor 8 Jahren

@Latino: Es kann ja gut sein, dass du dir keinen Anwendungsfall vorstellen kannst. Aber deine Kommentare sind im Rahmen dieser Diskussion (für mich) leider gehaltlos.

@Palin @Palladin007:

Interessante Gedanken, das würde ich gerne ausprobieren. Möglicherweise hapert es an meiner Implementation, denn ich habe keine Verbesserung erreichen können. Habt ihr da an sowas gedacht?

public class ObservableRangeCollection<T> : ObservableCollection<T>
{
    private bool _suppressNotification = false;

    public void AddRange(IEnumerable<T> list)
    {
        if (list == null)
            throw new ArgumentNullException("list");

        _suppressNotification = true;

        foreach (T item in list) Add(item);

        _suppressNotification = false;
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (!_suppressNotification) base.OnCollectionChanged(e);
    }
}
T
314 Beiträge seit 2013
vor 8 Jahren

Dauert es denn auch 15 Sekunden die Liste zu füllen, wenn die Liste nicht gebunden ist? Ansonsten zeigt doch mal noch mehr relevanten Code.

2.079 Beiträge seit 2012
vor 8 Jahren

Ja, ungefähr so würde ich das machen.
Dann würde ich für die NotifyCollectionChangedEventArgs aber als Action Add auswählen und die Items mit geben.
Das bietet bei der Nutzung - wenn Du das irgendwann mal brauchst - viele Möglichkeiten und vielleicht macht es auch einen Performance-Unterschied, dass Du nicht prinzipiell sagst, alles wurde geändert.

Allerdings muss ich den Kommentaren von LaTino zustimmen.
200k Einträge sind selbst für ganz besonders aggressive Scroller eine ordentliche Menge. Kannst ja mal bei Excel ausprobieren, wie lange es dauert, dort bis Zeile 200k zu scrollen.
Ich hab eine der Mäuse von Logitech, wo man das relativ schwere Mausrad lösen und dann damit sehr schnell scrollen kann. Bis 200k scrollen, da hätte ich keine Lust drauf 😄

Ein Filtern währe hier eine gute Lösung, was gleich noch ein tolle Feature ist.
Alternativ kannst Du sagen wie 10k Einträge laden und wenn der User bei Zeile 5000 angekommen ist, weitere 5000 Einträge laden. Das könnte - ja nach Größe der Sprünge - etwas ruckelig wirken, wenn man besonders schnell scrollt.

Oder Du lädst die Daten asynchron. Die werden dann hinzugefügt, während der User die Daten betrachtet. Das geht bei halbwegs sinnvoller Entwicklung so schnell, dass es nicht affällt und viel zu schnell viel zu viele Items geladen sind, bevor der User darüber hinaus scrollen kann.
Das Schwierigste wird da wohl eher sein, dass alles beteiligten Komponenten threadsafe sein müssen.

3.003 Beiträge seit 2006
vor 8 Jahren

@Latino: Es kann ja gut sein, dass du dir keinen Anwendungsfall vorstellen kannst. Aber deine Kommentare sind im Rahmen dieser Diskussion (für mich) leider gehaltlos.

Schade, aber nicht zu ändern. Beim Problemlösen schadet es nie, sich das Problem noch einmal genauer anzuschauen, aber dazu kann man niemanden zwingen.

Interessante Gedanken, das würde ich gerne ausprobieren. Möglicherweise hapert es an meiner Implementation, denn ich habe keine Verbesserung erreichen können.

Die Implementierung ist einigermaßen in Ordnung. Was sich mir nicht ganz erschließt, ist, ob du die Daten bereits teilweise oder immer noch vollständig einliest.

In letzterem Fall hast du durch die kleine Veränderung zwar eine Verbesserung erreicht, aber eine so minimale, dass sie nicht ins Gewicht fällt. Ein Durchlaufen einer Liste von 200k Einträgen dauert etwas, wobei die Kosten hauptsächlich beim Transformieren der Daten, die dir deine SQL-Abfrage liefert, anfallen dürften, und weniger beim Füllen der ObservableCollection. Wenn du den Zeitfresser mit Hilfe von etwas profiling identifiziert hast, kannst du an der Stelle ansetzen. Ich würde vermutlich zuerst 10.000 Datensätze laden, und sobald die da sind, die nächsten 10k im Hintergrund nachladen, bis die Liste voll ist. Dann bleibt eine Initial-Ladezeit von unter einer Sekunde, plus weitere kurze Wartezeiten, wenn der Anwender ein Hochleistungsscroller ist und die ersten 10k schneller wegscrollt, als die nächsten geladen sind.

Das schrieb ich allerdings auch bereits in meinem ersten Beitrag., scheint also keine Option für dich zu sein. Dann bleibt nur profiling und mikro-Optimierungen.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

W
872 Beiträge seit 2005
vor 8 Jahren

Wenn Du an fortgeschrittenen Techniken interessiert bist, dann würde ich mir mal den TailBlazer anschauen.
Das Scrollen und besonders die Suchbefehle gehen wirklich schnell.

3.003 Beiträge seit 2006
vor 8 Jahren

Kurzer Nachtrag, ich habe das Nachladen hier mal fix ausprobiert (Datenbank mit ~45 Millionen Einträgen zu je 40k), das initiale Laden dauert wie erwartet ca. eine Sekunde, danach (man sieht's an der Größe und Position des Scrollbalkens) kommen etwa alle Sekunde ein Haufen Einträge so lange dazu, bis mir mein Speicher vollgelaufen ist (was ein anderes Problem ist).


//viemodel-Ausschnitt
private bool _fetchDone;
private sqlWrapper _reader; //Kapselung eines Sql-Readers.
private ObservableCollection<ExampleEntry> _entryList;
public ObservableCollection<ExampleEntry> EntryCollection { get { return _entryList; } }


public void Initialize()
{
    _entryList = new ObservableCollection<ExampleEntry>();
    __reader = CreateReader<sqlEntry>(); //sqlEntry ist das pure SQL-Ergebnisobjekt (eine Zeile der DB)
    _reader.SelectAll(); //sql-Abfrage
    FetchCommand.Execute(null); //Befüllen der Liste im Hintergrund
}


private DelegatedBackgroundWorkerCommand FetchCommand //eigene Klasse, die drei Action-Objekte bekommt: eins wird vor der Haupt-Aktion, eins danach, und die Hauptaktion im Hintergrund ausgeführt
{
    get
    {
        var resultList = new List<ExampleEntry>(); 

            var result = new DelegatedBackgroundWorkerCommand(
            () => //diese Action() wird im Hintergrund ausgeführt
                {
                    for (int i = 0; i < 10000; i++) //nächsten 10000 Einträge holen
                    {
                        if (!_reader.Next())
                        {
                            _fetchDone = true;
                            break;
                        }
                        resultList.Add(new ExampleEntry(_reader.record)); //<- das hier ist das, was Zeit kostet
                    }
                },
            () => //diese Action wird ausgeführt, wenn die Hintergrund-Operation beendet ist
                {
                    resultList.ForEach(_entryList.Add);
                    if(!_fetchDone) FetchCommand.Execute(null);
                    ;
                }
                , null, null);
        return result;
    }
}

Wie gesagt, das habe ich jetzt auf die Schnelle zusammengehackt, wobei der Großteil des Aufwands war, die richtige Tabelle zu finden und deren Einträge in ein Objekt "ExampleEntry" zu mappen. Aber es erfülllt die Anforderung, und die UI bleibt responsiv.

Das "ziehen" des Scrollbalkens mit der Maus ist jetzt natürlich broken, weil immer noch Einträge dazukommen.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)