none
DataGridView wirft InvalidOperationException wegen threadübergreifendem Vorgang nach Datenbindung RRS feed

  • Frage

  • Hallo,

    ich verwende C# im .NET- FW 3.5 und habe mein Grid über eine DataBindingSource an ein streng typisiertes DataSet mit streng typisierten DataTables gebunden. Über einen System.Threading.Timer wird n-Mal pro Sekunde der Inhalt einer Spalte überschrieben. Das funktioniert soweit einwandfrei und die neuen Werte werden ohne mein Zutun auch wunderbar im DataGridView angezeigt.

    Wenn ich im DataGridView auf die Spalte klicke, die wie oben beschrieben nahezu nonstop beschrieben wird, und versuche einen Wert einzugeben, dann erhalte ich die folgende Exception:

    "DataGridView-Ausnahme:

    System.InvalidOperationException: Ungültiger threadübergreifender Vorgang: Der Zugriff auf das Steuerelement erfolgte von einem anderen Thread als dem Thread, für den es erstellt wurde.

    bei (...)
    bei Systems.Windows.Forms.DataGridView.InitializeEditingControlValue(DatagridViewCellStyle& dataGridViewCellStyle, dataGridViewCell dataGridViewCell)

    Behandeln Sie das DataError-Ereignis, um dieses Standarddialogfeld zu ersetzen."

    Bevor jetzt jeder aufstöhnt und mir einen Link auf "invoke"-Artikel postet: Ich weiß, dass bei UI-Elementen nur der erzeugende Thread auf die Elemente zugreifen darf und ich anderenfalls mit Invoke den Originalthread dazu heran ziehen muss.

    In diesem Fall wundert es mich aber: Der "normale" WinForm-Thread (welcher überhaupt erst einmal das Fenster instantiiert) nimmt die Datenbindung von meinem DataBindingSource Objekt zu meinem DataSet und von meinem DataGridView zum DataBindingSource-Objekt vor:

     

    bindSource = new BindingSource(meinDataSet, "meineDataTable");  
    
    //prepare grid
    this.dataGridView1.AutoGenerateColumns = true;
    this.dataGridView1.AutoSize = true;
    this.dataGridView1.DataSource = bindSource;

    Der Timer schreibt direkt in die Instanz des DataSets. Ich habe noch keinen Code für das "User-Event" geschrieben (Zelle betreten, Zelle wird geändert, Zelle wurde geändert), daher weiß ich nicht, wo ich ansetzen soll, um diesen Fehler zu finden und zu beheben.

    1.) Woher kommt dieser Fehler?
    2.) Was kann/muss ich gegen diesen Fehler unternehmen?

    Danke im Voraus für eure Mithilfe

    Timo

    Donnerstag, 6. Januar 2011 14:14

Antworten

  • Hallo Timo,

    Das einfachste: Verwende einen Windows Forms Timer , denn der läuft auf dem UI Thread.

    Der System.Threading.Timer verwendet einen Worker Thread und Du müsstest
    Änderungen an Steuerlementen via Control.Invoke vornehmen, siehe auch:
    Gewusst wie: Threadsicheres Aufrufen von Windows Forms-Steuerelementen

    Ein ausführlicher Vergleich: Comparing the Timer Classes in the .NET Framework Class Library

    Gruß Elmar

    Donnerstag, 6. Januar 2011 15:04
    Beantworter
  • Hallo Timo,

    dafür ist der  Code-Schnipsel etwas zu kurz.
    Zeige mal den Code für Abruf und Zuweisung vollständig.

    Grundsätzlich gilt: Control.Invoke benötigt man, wenn man Steuerelemente, d. h. von Control abgeleitete Klassen,
    ansprechen will. Was für alle Eigenschaften gilt, die direkt oder indirekt auf das Windows-API zugreifen.
    Denn dort gilt (viel aus historischen Gründen), d. h. nur Zugriffe aus dem erzeugenden Thread zulässig sind.

    Für DataSet/DataTable und Zugriffsklassen wie TableAdapter gilt das nicht.
    Dort kannst Du nur andere Probleme bekommen, wenn Du aus einem anderen Thread Daten änderst,
    auf die der  Windows Thread gerade zugreifen will. Denn für so ziemlich alle Klassen gilt,
    das sie nicht thread-sicher sind, d. h. man sich um die Synchronisation selbst kümmern muss.

    Aber das ist schon ein anderes Thema.

    Gruß Elmar

     

    Montag, 10. Januar 2011 19:43
    Beantworter

Alle Antworten

  • Hallo Timo,

    Das einfachste: Verwende einen Windows Forms Timer , denn der läuft auf dem UI Thread.

    Der System.Threading.Timer verwendet einen Worker Thread und Du müsstest
    Änderungen an Steuerlementen via Control.Invoke vornehmen, siehe auch:
    Gewusst wie: Threadsicheres Aufrufen von Windows Forms-Steuerelementen

    Ein ausführlicher Vergleich: Comparing the Timer Classes in the .NET Framework Class Library

    Gruß Elmar

    Donnerstag, 6. Januar 2011 15:04
    Beantworter
  • Hallo Elmar,

     

    vielen Dank für deine Antwort. Ich habe jetzt den Timer durch einen Windows-Timer ersetzt und es funktioniert soweit.

    Was mich jetzt aber schon brennend interessieren würde: Wo kann ich in meinem Fall den Invoke-Befehl benutzen? Ich weiß, dass ich Invoke brauche, wenn ich mit einem fremden Thread auf UI-Elemente zugreifen will.

    Aber: Mein Grid ist per DataBindingSource an die DataTable gebunden. Der fremde Thread ändert nur die DataTable - die Aktualisierung des Grids erfolgt also implizit. Es gibt somit für mich augenscheinlich keinen Punkt, an welchem ich die Invoke-Methode des Grids anwenden könnte.

     

    Hier ist ein Code-Ausschnitt, wie ich die Quelle der DataBindingSource verändere (der Event-Code hat aktuell noch keine Aufgabe):

    meinDataSet
    .meineTabelle
     smpVarRow = this
    .meinDataSet.meineTabelle.FindByName(getVariableNameFromIndex(variableIndex));
    smpVarRow.BeginEdit();
    smpVarRow.Value = variableValue;
    smpVarRow.EndEdit();
    VariablesChanged(variableIndex, variableValue); //Event feuern und "Maske benachrichtigen"

     

    Die typisierte DataTableRow besitzt keine Invoke-Methode; ebensowenig das DataSet. Übersehe ich hier etwas?

    Danke und Gruß

    Timo

    Montag, 10. Januar 2011 11:07
  • Hallo Timo,

    Was tut VariablesChanged genau, wenn sie die "Maske benachrichtigt"?

    Gruß Elmar

    Montag, 10. Januar 2011 11:11
    Beantworter
  • Hallo Elmar,

    dieser Event-Code ist wirklich aktuell nur ein Dummy - die Funktion ist leer. Geplant ist für später, dass das Formular z.B. ein Textfeld aktualisiert, in welchem der Zeitstempel der letzten Aktualisierung vermerkt ist. Der Fehler tritt auch auf, wenn ich das Event im Formular nicht anmelde.

    Ich frage mich nur, wo ich das Invoke platzieren könnte. Das Event des Grids, welches die Threadverletzung meldet, ist nicht öffentlich.

    Hast du (oder jemand anderes) dazu eine Idee?

    Danke und Gruß

    Timo

    Montag, 10. Januar 2011 11:31
  • Hallo Timo,

    dafür ist der  Code-Schnipsel etwas zu kurz.
    Zeige mal den Code für Abruf und Zuweisung vollständig.

    Grundsätzlich gilt: Control.Invoke benötigt man, wenn man Steuerelemente, d. h. von Control abgeleitete Klassen,
    ansprechen will. Was für alle Eigenschaften gilt, die direkt oder indirekt auf das Windows-API zugreifen.
    Denn dort gilt (viel aus historischen Gründen), d. h. nur Zugriffe aus dem erzeugenden Thread zulässig sind.

    Für DataSet/DataTable und Zugriffsklassen wie TableAdapter gilt das nicht.
    Dort kannst Du nur andere Probleme bekommen, wenn Du aus einem anderen Thread Daten änderst,
    auf die der  Windows Thread gerade zugreifen will. Denn für so ziemlich alle Klassen gilt,
    das sie nicht thread-sicher sind, d. h. man sich um die Synchronisation selbst kümmern muss.

    Aber das ist schon ein anderes Thema.

    Gruß Elmar

     

    Montag, 10. Januar 2011 19:43
    Beantworter

  • Hallo Elmar (und alle anderen, die hier antworten können),

    sorry, dass ich so lange nichts von mir habe hören lassen - ich musste mich um viele andere Dinge kümmern.

    Hier nun der Codeblock, welcher die Threads erzeugt. Weiter unten befindet sich noch ein weiterer Block, der die Veränderung des DataSets beschreibt und ganz am Ende gibt es noch einen Block, der die Datenbindung des Grids an das DataSet beschreibt.


    public class clsXMLConnectorDummy : IConnector
    {
    private System.Windows.Forms.Timer tmrRetrieve;
    private Object lockGetValues = new Object();
    private Object lockStartThread = new Object();

    public clsXMLConnectorDummy()
    {
    //Prepare Timer
    AutoResetEvent autoEvent = new AutoResetEvent(false);
    tmrRetrieve = new System.Windows.Forms.Timer();
    tmrRetrieve.Interval = intTimerRequestInterval;
    tmrRetrieve.Tick += new EventHandler(tmrRetrieve_Tick);
    tmrRetrieve.Enabled = true;
    }

    private void tmrRetrieve_Tick(object sender, EventArgs e)
    {
    Application.DoEvents();
    lock (this.lockStartThread)
    {
    Thread t = new Thread(new ThreadStart(this.startTransaction));
    t.IsBackground = false;
    t.Start();
    GC.Collect();
    }
    //startTransaction();
    Application.DoEvents();
    }

    private void startTransaction()
    {
    lock (this.lockGetValues)
    {
    this.getAllVariableValues();
    }
    }


    In der Funktion "getAllVariableValues()" wird n-mal das Event "variableChanged(index, value)" ausgelöst. Dieses Event wird in dem folgenden Codeblock behandelt:
    public void DAL_VariableChanged(string variableIndex, string variableValue) 
    {
    //search for variable in DataSet and change value
    Variables.SimpleVariableRow smpVarRow = this.dsVariables.SimpleVariable.FindByIndex(variableIndex);
    if (smpVarRow != null)
    {
    lock (VariablesLock)
    {
    smpVarRow.BeginEdit();
    smpVarRow.Value = variableValue;
    smpVarRow.EndEdit();

    //signal that the datasource has changed
    if (VariablesChanged != null)
    {
    VariablesChanged(variableIndex, variableValue);
    }
    }
    }
    }

    Dieses Event "VariablesChanged" läuft ins Leere, da es noch nicht benötigt wird. Später möchte ich damit evtl. ein Label oder ähnliches aktualisieren (Zeitpunkt der letzten Variablenänderung etc.).


    Wie ist das Grid an die Datenquelle angebunden? Der folgende Codeblock im Formular verdeutlicht es:

    clsBindingSourceSafe bindSource = new clsBindingSourceSafe(clsWorkflow.BLL.VariableCollection, "SimpleVariable"); 

    this.dataGridView1.AutoGenerateColumns = true;
    this.dataGridView1.AutoSize = true;
    this.dataGridView1.DataSource = bindSource;


    Was nun passiert: In einem bestimmten Zyklus wird ein Thread gestartet, der ein Abrufen der Variablen von einem Server bewirkt. Immer dann, wenn eine Variablenänderung "bemerkt" wird, dann erzeugt dieser Fremdthread ein Event, dass die im zweiten Codeblock beschriebene Funktion aufruft, die die Werte im lokalen DataSet ändert.
    Ohne, dass weiter etwas passieren muss, aktualisiert nun das DataGrid seine Werte, da es die Änderungen über das BindingSource-Objekt mitbekommt.

    Wie zu erkennen ist, gibt es für mich keinen Einstiegspunkt, um das Event des Aktualisierens vom GUI-Thread durchführen zu lassen. Da pro Zyklus ca. 200 bis 500 Variablen abgerufen werden, dauert die Verarbeitung auch mal länger als die Zykluszeit.


    Hier meine Fragen:
    1.) Ist meine "Threading-Architektur" sinnvoll oder übersehe ich eine viel leichtere Variante um zyklisch über Multithreading eine Verarbeitung durchzuführen?
    2.) Gibt es eine Möglichkeit, dem Grid mitzuteilen, dass ich als User "während der automatischen Aktualisierung" im Grid klicken darf?
    3.) Falls 2.) nicht möglich ist: Kann ich der BindSource-Klasse mitteilen, dass sie für einen Zeitraum X nicht mehr auf das DataSet lauschen soll?

    Viele Grüße und vielen Dank im Voraus für die Unterstützung
    Timo
    • Bearbeitet Timo Theo Paschke Montag, 31. Januar 2011 15:47 Formatierung (diesmal alles plötzlich Plain-Text)
    Montag, 31. Januar 2011 15:43