| 

.NET C# Java Javascript Exception

3
Hallo,

als erstes muss ich sagen, daß ich wirklich lange im Netz und hier gesucht habe, aber leider keine Antwort auf meine Frage gefunden habe.

Ich habe hier ein Form mit einem DataGridView. Die BindingSource des Datagridviews fülle ich mit einem Backgroundworker und möchte nun auf einem Dialogfensterchen anzeigen, wie weit das Einlesen schon gekommen ist.

Form:
Private Sub LoadDataBgwDoWork(sender As System.Object, _
e As System.ComponentModel.DoWorkEventArgs) _
Handles LoadDataBGW.DoWork

_blv.DataSource.Clear()
'''...
' Fortschritt mitteilen
While (s < _designOrders.Count())
LoadDataBGW.ReportProgress(CInt((s * 1.0) / _designOrders.Count() * 100.0), _designOrders.Skip(s).Take(Pagesize).ToList())
s += Pagesize
End While

End Sub

Private Sub LoadDataBGWProgressChanged(sender As System.Object, _
e As System.ComponentModel.ProgressChangedEventArgs) _
Handles LoadDataBGW.ProgressChanged

Try
For Each o In DirectCast(e.UserState, List(Of OrderDataGridViewModelRow))
_blv.DataSource.Add(o)
Next
_waitDialog.Progress = e.ProgressPercentage

Catch ex As Exception
Console.WriteLine(ex)
End Try

End Sub


WarteDialog:
Public Property Progress As Integer
Get
Return ProgressBar1.Value
End Get
Set(value As Integer)
If ProgressBar1.Value <> value _
AndAlso value >= ProgressBar1.Minimum _
AndAlso value <= ProgressBar1.Maximum Then

ProgressBar1.Value = value
End If
End Set
End Property


Nun passiert folgendes, der Wartedialog wird angezeigt, er arbeitet wie wild und der Progressbar bewegt sich kein Stück. Und dann ist er auch schon fertig und der Dialog wird wieder ausgeblendet.

Wenn ich in die Schleife eine Thread.Sleep(1000) einbaue, klappt es wunderbar, aber ich will ja, daß die Daten schnell geladen werden und der Vorgang nicht noch zusätzlich verlangsamt wird. Es hat also mit der Aktualisierung der Anzeige zu tun. Aber weder ein .Refresh, noch ein .Update oder ein .DoEvents() haben den gewünschten Erfolg gebracht.

Hat noch jemand eine Idee?

LG,

BaNDiT
09.05.2012
TiMeBaNDiT76 294 2 7
Hast du WorkerReportsProgress = true gesetzt?
mrmee 09.05.2012
5 Antworten
1
Hm, nach Lesen Deines aktualisierten Code-Beispiels: wenn ich das richtig verstehe, hast Du im DowWork-Handler die zu ladenden Daten komplett auf einen Schlag verfügbar - gekapselt durch das OrderDataGridViewModel (falls das VM IEnumerable oder sowas implementiert: die Count()- und Skip()-Aufrufe dürften jeden Versuch von Lazy Loading torpedieren). Danach sollen diese Daten zeilenweise in die BindingSource übertragen werden, und für diesen Prozeß möchtest Du eine Fortschrittsanzeige.

Richtig?

Dann denke ich, dass das mit dem BGW so nicht funktioniert oder dessen Idee zuwider läuft. das Befüllen einer BindingSource ist eine UI-Aufgabe - nicht umsonst brauchst Du ein Invoke(). Der BGW ist aber dafür da, langlaufende Aufgaben in einen separaten Nicht-UI-Thread zu verschieben und derweil das UI am Leben zu halten und optional darüber zu informieren, was da so im Hintergrund vor sich geht. Damit das einen Sinn ergibt, muß aber die zeitintensive Arbeit im Background-Thread erfolgen. Du blockierst Dir aber den UI-Thread durch das zeilenweise Befüllen der BindingSource, während der Worker-Thread kaum etwas zu tun bekommt.

Wenn das Befüllen tatsächlich so lange dauert, dass Du eine Fortschrittsanzeige dafür brauchst, mußt Du zwei parallele Aufgabe im UI-Thread koordinieren und Dir bewußt sein, daß es dadurch tendenziell etwas langsamer wird. Der BGW dürfte dann eher nicht das richtige Werkzeug dafür sein, eine normale Schleife wäre dann wohl besser, bei der alle n Durchläufe die ProgressPercentage erhöht wird.
Ich würde aber erst mal versuchen, die Daten auf einen Schlag einzufüllen (_blw.DataSource = _designOrders, statt Add()). Das sollte eigentlich nicht so ewig dauern, und wenn doch, dann würde mal nachschauen, wieso.
10.05.2012
Matthias Hlawatsch 13,0k 3 9
+1, thumbs up
Floyd 10.05.2012
Ok ... Problem verstanden ... vielleicht bin ich es ja auch falsch angegangen. Den Progressbar habe ich eingebaut, weil ich ein DataGridView mit Hilfe von DataBinding befülle. Das reine Zeichnen des DataGrid dauert gefühlt eine Ewigkeit, obwohl vielleicht 100 Einträge drin sind. Das Abfragen und ViewModel erzeugen geht schnell, daß sagt zumindest der Profiler.

In dem DataGridView sind, neben Text und Datum, auch ein paar kleine Piktogramme. Könnten die es vielleicht sein? Gibt es irgendwo eine super Erklärung, wie man Databinding RICHTIG verwendet?
TiMeBaNDiT76 10.05.2012
Versuch mal "BindingSource.RaiseListChangedEvent" auf FALSE zu setzten.
Floyd 10.05.2012
2
Das hat so leider nicht geholfen ... ich habe aber inzwischen entdeckt, daß die LANGSAMKEIT nicht direkt aus der Bindung stammen, sondern durch meine Abfragen resultieren und einen Join mit Linq, der sich leider so nicht vermeiden läßt.

Ich möchte mich trotzdem sehr herzlich bei euch allen Bedanken. Durch eure Anregungen habe ich viele neue Ideen bekommen und auch umgesetzt.
TiMeBaNDiT76 11.05.2012
Dann schreib dir eine Stored Procedure und verlager das SQL dort hinein, statt das ganze SQL dynamisch zu erzeugen. Das hat zudem noch andere Vorteile.

1. die Compile-Zeit für das Statment fällt weg
2. du hast direkten Einfluss auf das Statment und kannst spezielle Optimierungen verwenden (z.B. Index Hints)
Floyd 11.05.2012
Ja, darüber habe ich auch schon nachgedacht ... mein Problem. Die Abfrage geht über mehrere Schemata auf einem MySql Server und das kann EF irgendwie nicht richtig. Zumindest nicht in Version 4.x
TiMeBaNDiT76 11.05.2012
Aber eine SP sollte es können, und damit das Problem aus dem EF in die Datenbank verlagern.

Ich bin generell weniger einer Fan von Dynamischen SQL, als mehr einer davon SP und FUNCS als Datenzugriff zu nehmen damit man den Datenzugriff optimal an jeweilige Datenbank anpassen zu können und auch Änderungen am Datenmodel transparent für die Clients machen zu können.
Floyd 11.05.2012
Ja ...

um an dieser Stelle weiter zu diskutieren, sollten wir vielleicht einen neuen Thread aufmachen, wie z.B. Dyn.SQL vs. Stored Procedures oder so ;-) ... würde mich auch interessieren, wie man das geschickt umsetzen kann.
TiMeBaNDiT76 11.05.2012
Dann schau dir einmal http://weblogs.asp.net/scottgu/archive/2007/08/16/linq-to-sql-part-6-retrieving-data-using-stored-procedures.aspx an und wenn du Fragen bzw. Diskussionstoff hast, stehen wir dir gern zur Seite ;)
Floyd 11.05.2012
Das werde ich machen. Danke.
TiMeBaNDiT76 11.05.2012
0
Aus dem kleinen Stück was du gepostet hast kann man leider einige sachen nicht erkennen.

Ein zwei typische Fehler sind:



  • WorkerReportsProgress fehlt
    LoadDataBGW.WorkerReportsProgress = True

  • Invoke in den UI-Thread fehlt
    Als C#-Code:
    if(this.InvokeRequired) {
    this.Invoke(new MethodInvoker(() => LoadDataBGWProgressChanged(sender, args)));
    return;
    }


    Private Sub LoadDataBgwDoWork(sender As System.Object, _
    e As System.ComponentModel.DoWorkEventArgs) _
    Handles LoadDataBGW.DoWork

    _blv.DataSource.Clear()
    '''...
    ' Fortschritt mitteilen
    While (s < _designOrders.Count())

    ' \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/ \/
    DirectCast(sender, System.ComponentModel.BackgroundWorker).ReportProgress(CInt((s * 1.0) / _designOrders.Count() * 100.0), _designOrders.Skip(s).Take(Pagesize).ToList())

    s += Pagesize
    End While

    End Sub

    Private Sub LoadDataBGWProgressChanged(sender As System.Object, _
    e As System.ComponentModel.ProgressChangedEventArgs) _
    Handles LoadDataBGW.ProgressChanged

    '\/ C# Code den du in VB.Net übersetzten musst
    '\/ sorgt dafür das du in den UI-Thread wechselst
    if(this.InvokeRequired) {
    this.Invoke(new MethodInvoker(() => LoadDataBGWProgressChanged(sender, args)));
    return;
    }

    Try
    For Each o In DirectCast(e.UserState, List(Of OrderDataGridViewModelRow))
    _blv.DataSource.Add(o)
    Next
    _waitDialog.Progress = e.ProgressPercentage

    Catch ex As Exception
    Console.WriteLine(ex)
    End Try

    End Sub



EDIT, Neue These:

Das Problem könnte sein, das der UI-Thread mit dem befüllen der DataGridView beschäftigt ist (also dem was du im DoWork-Handler machst) und nicht dazu kommt die ProgressChanged-Events die von ReportProgress ausgelöst werden abzuarbeiten.
Das würde auch erklären warum ein Thread-Sleep(1000) das Problem beseitigt.

Eine Möglichkeit wäre ReportProgress nur jede Sekunden oder nach erreichen bestimmte Meilensteine auszulösen und im Anschluss mit Thread.Sleep(100) dem UI-Thread die möglichkeit zu geben das Event abzuarbeiten.

Private Sub LoadDataBgwDoWork(sender As System.Object, _
e As System.ComponentModel.DoWorkEventArgs) _
Handles LoadDataBGW.DoWork

Dim lastUpdate as DateTime = DateTime.Min
_blv.DataSource.Clear()

'''...
' Fortschritt mitteilen
While (s < _designOrders.Count())
'ReportProgress nur einmal pro Sekunden auslösen
if (DateTime.Now - lastUpdate).TotalMilliseconds < 1000 then
DirectCast(sender, System.ComponentModel.BackgroundWorker).ReportProgress(CInt((s * 1.0) / _designOrders.Count() * 100.0), _designOrders.Skip(s).Take(Pagesize).ToList())
lastUpdate = DateTime.Now
Thread.Sleep(100); '100 Millisekunden warten um den UI-Thread die Möglichkeit zu geben das Event abzuarbeiten
end if
s += Pagesize
End While

End Sub
09.05.2012
Floyd 14,5k 3 9
Floyd 14,5k 3 9
Hi,

beide angesprochene Themen sind es nicht.

1. WorkerReportsProgress
Habe ich natürlich gesetzt.

2. Das Invoke brauche ich nur, wenn ich es von einer externen Klasse aufrufe. Da ich mich aber im eigenen Thread befinde funktioniert es so. Wenn es im Debugger durchgehe, funktioniert es auch.

Wie schon gesagt, ich bekomme keine Fehlermeldung. Der Thread, welcher die Daten bearbeitet scheint alle Ressourcen des Rechners zu sperren. Wenn ich ein Thread.Sleep(1000) einbaue geht es.
TiMeBaNDiT76 09.05.2012
Ich will den Vorgang aber nicht unnötig verlängern, indem immer wieder ein Warten eingebaut wird. Es muss doch auch anders gehen.
TiMeBaNDiT76 09.05.2012
2
Ich glaube auch nicht, dass es am Thread-Wechsel liegt. Der BGW soll es einem ja gerade abnehmen, über InvokeRequired und dergleichen nachdenken zu müssen, und ProgressChanged ist für das Aktualisieren des UI gedacht. Zitat MSDN:
Im DoWork-Ereignishandler dürfen keine Benutzeroberflächenobjekte bearbeitet werden. Verwenden Sie stattdessen zum Kommunizieren mit der Benutzeroberfläche das ProgressChanged-Ereignis und das RunWorkerCompleted-Ereignis.
Matthias Hlawatsch 09.05.2012
Hmm, guter Einwand
Floyd 09.05.2012
Hab meinen Beitrag um ne neue These erweitert
Floyd 09.05.2012
Die Idee ist natürlich nicht schlecht. Werde ich morgen einmal ausprobieren.
TiMeBaNDiT76 09.05.2012
@Floyd: er befüllt doch die DataSource gerade *nicht* in DoWork, sondern in ProgressChanged?
Matthias Hlawatsch 09.05.2012
Stimmt, ich schein heute nicht auf der Höhe zu sein, aber unabhängig davon, wissen wir beide nicht was er dort macht wo nur ein "''..." steht.
Floyd 09.05.2012
Aber da stellt sich mir doch die Frage warum man das im ProgressChanged macht und nicht im DoWork ?
Floyd 09.05.2012
1
@Floyd: Ich denke, dass ein Befüllen der DataSource in DoWork nicht funktioniert, weil das ein Aktualisieren des UI zur Folge hätte, was in DoWork nicht erlaubt ist (es sei denn, es wird mit Invoke() gekapselt, und das will man ja eigentlich mit dem BGW gerade vermeiden). Das ist halt die Crux beim Databinding :-(
Ansonsten: ja, mich interessiert auch, was an den ausdgeblendeten Stellen passiert ;-)
Matthias Hlawatsch 09.05.2012
@Matthias: Von der Logik her ist ja die DoWork Methode dafür da.
Einen echten Grund um auf Invoke in der DoWork-Methode zu verzichten sehe ich aber nicht.

Aber ich muss auch zugeben das ich mit DataGridViews und DataSource nicht wirklich auskenne, da mein Schwerpunkt mehr im Bereich der Webentwicklung, BuisnessLogiken und API-Design liegt.
Floyd 09.05.2012
Kleine Anmerkung, ich hab TotalMilliseconds < 1000 geschrieben was natürlich falsch ist. TotalMilliseconds > 1000 ist richtiger.
Floyd 10.05.2012
0
In welche Schleife hast Du denn den Sleep eingebaut? In DoWork oder in ProgressChanged?

Was ich merkwürdig finde: Du benutzt ProgressChanged für das Befüllen der DataSource. Nach meinem Verständnis ist ProgressChanged aber nur für das Aktualisieren der Fortschrittsanzeige gedacht.

Was ist denn wirklich langsam? Das Einlesen der Daten oder das Befüllen der DataSource? Könntest Du nicht auch erst mal alle Daten einlesen, dabei über den Fortschritt berichten und dann in einer Aktion die DataSource befüllen, und zwar in RunWorkerCompleted? Ich kann zwar nicht erklären, warum es zu der Blockade kommt (und vielleicht fehlen dazu auch ein paar wichtige Infos in Deiner Frage), aber ich glaube, so haben sie sich das bei Microsoft gedacht.

Übrigens: _blv.DataSource.Clear() würde ich aus dem DoWork-Handler herausnehmen und vor dem Aufruf von RunWorkerAsync (oder auch erst in RunWorkerCompleted, wenn Du meinem Vorschlag folgst) ausführen. Das zieht ja auch eine UI-Aktualisierung nach sich...
09.05.2012
Matthias Hlawatsch 13,0k 3 9
Du stellst dir scheinbar die selbe Frage :)
Floyd 09.05.2012
Hallo,

also Du hast schon recht, ich habe die ReportProgress Methode verwendet, weil sie nicht im Thread des Backgroundworkers läuft und damit ein Update der UI möglich ist. Ich aktualisiere den Progressbar und füge Elemente zur BindingSource hinzu.

Das was ich mit "..." markiert hatte, war für den Ablauf nicht wichtig.

Meint ihr, wenn ich mir die Mühe mache und das Hinzufügen von Elementen in eine Methode auslagere, welche ich dann mit INVOKE aufrufe wird es funktionieren?

Dann würde ich im ReportProgress nur noch den Progressbar aktualisieren.

Der Sleep war in der DoWork Methode.
TiMeBaNDiT76 09.05.2012
Ja, das denke ich. Und ist von der Logik her auch besser.
Floyd 10.05.2012
0
Hi,

ich habe das mit dem Invoke probiert, komme aber zum gleichen Ergebnis. Ich poste diesmal den ganzen Code, damit ihr nicht wieder nach dem "..." fragt ;-)

Private Sub LoadDataBgwDoWork(sender As System.Object, _
e As System.ComponentModel.DoWorkEventArgs) _
Handles LoadDataBGW.DoWork

Dim init = DirectCast(e.Argument, Boolean)
_blv.DataSource.Clear()

' Rechte prüfen
If Context.Instance.IsSuperVisor Then
_designOrders = New OrderDataGridViewModel(init)
Else
_designOrders = New OrderDataGridViewModel(init, Context.Instance.CurrentUser.PersonenID)
End If

'ListView erstellen
Dim s = 0

' Fortschritt mitteilen
While (s < _designOrders.Count())
FillData(_designOrders.Skip(s).Take(Pagesize).ToList())
LoadDataBGW.ReportProgress(CInt((s * 1.0) / _designOrders.Count() * 100.0), Nothing)
s += Pagesize
End While

End Sub

Delegate Sub DelegateFillData(list As IEnumerable(Of OrderDataGridViewModelRow))

''' <summary>
''' Füllt das DataGrid mit Elementen
''' </summary>
''' <param name="list"></param>
''' <remarks></remarks>
Private Sub FillData(list As IEnumerable(Of OrderDataGridViewModelRow))

If _dgvJobs.InvokeRequired Then

Dim fill As DelegateFillData = AddressOf FillData
Invoke(fill, New Object() {list})

Else

Try

For Each item In list
_blv.DataSource.Add(item)
Next

Catch ex As Exception

Throw ex

End Try

End If

End Sub

Private Sub LoadDataBGWProgressChanged(sender As System.Object, _
e As System.ComponentModel.ProgressChangedEventArgs) _
Handles LoadDataBGW.ProgressChanged

_waitDialog.Progress = e.ProgressPercentage

End Sub

Private Sub LoadDataBGWRunWorkerCompleted(sender As System.Object, _
e As System.ComponentModel.RunWorkerCompletedEventArgs) _
Handles LoadDataBGW.RunWorkerCompleted

If Not Context.Instance.IsSuperVisor Then
EditReleasesToolStripMenuItem.Visible = False
End If

' Nur die offenen Aufträge anzeigen
'OpenOrdersCheckBox.Checked = True

If _onlyOpenOrdersLoaded Then
_blv.Refresh()
Else
_blv.ApplyFilter(AddressOf ApplyFilter)
End If


Cursor = Cursors.Arrow
_waitDialog.Hide()

End Sub


Habt ihr noch eine Idee?
10.05.2012
TiMeBaNDiT76 294 2 7
1
Ist OrderDataGridViewModel IQueryable oder woher hat das ViewModel die Count() Methode? Bei IEnumerable kann der Aufruf der Count() Methode nicht optimiert bzw. verzögert ausgeführt werden, was dazu führt, dass jeder Aufruf von Count die Items komplett iteriert. Das mag nicht zwingend mit Deinem Aktualisierungsproblem zusammenhängen, würde aber die Loadperformance bei der Häufigkeit der Aufrufe signifikant steigern.
ffordermaier 10.05.2012
Nunja, wenn ich nochmal drüber nachdenke, ist IEnumerable eher unwahrscheinlich. War halt ein spontaner Gedanke...
ffordermaier 10.05.2012
Es ist eine GenericList ... aber ich habe die Klammern mal weg gemacht, damit er das Property und nicht die Linq-Methode verwendet. Wobei es in VB ja wohl irgendwie egal ist, ob man Klammern setzt, wenn mann keine Parameter hat. Ich weiß also nicht genau, ob er das Property oder die Methode aufruft.
TiMeBaNDiT76 10.05.2012
0
versuch mal das hier:

Dim lastUpdate as DateTime = DateTime.Min

While (s < _designOrders.Count())
FillData(_designOrders.Skip(s).Take(Pagesize).ToList())

'ReportProgress nur einmal pro Sekunden auslösen
if (DateTime.Now - lastUpdate).TotalMilliseconds > 1000 then '>1000 nicht <1000
DirectCast(sender, System.ComponentModel.BackgroundWorker).ReportProgress(CInt((s * 1.0) / _designOrders.Count() * 100.0), Nothing)
lastUpdate = DateTime.Now
Thread.Sleep(100)
end if
s += Pagesize
End While


Um den UI-Thread Zeit zu geben das Event auch abzuarbeiten.
Das Delegate ist im Prinzip nur ein Funktionszeiger. Das Invoke beschäftigt weiterhin den UI-Thread, also musst du selbigem Zeit einräumen das Event auch abzuarbeiten.
10.05.2012
Floyd 14,5k 3 9
Floyd 14,5k 3 9

Stelle deine .net-Frage jetzt!