WinForms: Multithreaded BindingList

20. Oktober 2010

Mit einer BindingList<T> bzw. einer IBindingList kann in WinForms leicht ein Two-Way-DataBinding an ein DataGridView ermöglicht werden, indem man dessen DataSource-Property auf die BindingList setzt. Vorteil dabei: die Anzeige des DataGridView wird stets aktuell gehalten mit den Elementen in der BindingList, zusätzlich auch mit den Daten innerhalb der einzelnen Elemente, wenn diese INotifyPropertyChanged implementieren.

Doch die DataBinding-Magie hat ein Ende, wenn die BindingList in einem zweiten Thread parallel zum UI-Thread aktualisiert bzw. verändert wird. Die Änderung im zweiten Thread wird über das ListChanged-Event der BindingList an das DataGridView publiziert, das aber im UI-Thread läuft und daher ein Cross-Thread-Problem bekommt:

CrossThreadError

Fehlermeldung: "System.InvalidOperationException – Ungültiger threadübergreifender Vorgang: Der Zugriff auf das Steuerelement ‚xyz‘ erfolgte von einem anderen Thread als dem Thread, für den es erstellt wurde."
Bzw. auf Englisch: "System.InvalidOperationException – Cross-thread operation not valid: Control ‚xyz‘ accessed from a thread other than the thread it was created on."

Da die Aktualisierung des Grids automatisch erfolgt, hat man als Entwickler keine Chance in den Prozess einzugreifen und z.B. mit InvokeRequired zu prüfen, ob die Operation im UI-Thread stattfinden muss.

Abhilfe schafft eine eigene BindingList, die sich des Threading-Problems mit Hilfe der SynchronizationContext-Klasse annimmt und Updates so im UI-Thread auslöst. Folgende ThreadedBindingList (Modifikation einer Lösung von Stack Overflow) setzt dies um:

   1: public class ThreadedBindingList<T> : BindingList<T>

   2: {

   3:     private readonly SynchronizationContext m_syncContext = SynchronizationContext.Current;

   4:  

   5:     protected override void OnAddingNew(AddingNewEventArgs e)

   6:     {

   7:         if (m_syncContext == null)

   8:             BaseAddingNew(e);

   9:         else

  10:             m_syncContext.Send(state => BaseAddingNew(e), null);

  11:     }

  12:  

  13:     protected override void OnListChanged(ListChangedEventArgs e)

  14:     {

  15:         if (m_syncContext == null)

  16:             base.OnListChanged(e);

  17:         else

  18:             m_syncContext.Send(state => BaseListChanged(e), null);

  19:     }

  20:  

  21:     private void BaseAddingNew(AddingNewEventArgs e)

  22:     {

  23:         base.OnAddingNew(e);

  24:     }

  25:  

  26:     private void BaseListChanged(ListChangedEventArgs e)

  27:     {

  28:         base.OnListChanged(e);

  29:     }

  30: } 

Eine Instanz dieser Liste muss im UI-Thread erzeugt werden, sodass der SynchronizationContext korrekt gesetzt werden kann. Alternativ sind andere Implementierungen denkbar, die den SynchronizationContext per Konstruktor übergeben bekommen.