WinForm-Gui verträgt sich schlecht mit Threading - aus einem Nebenthread darf keinesfalls auf ein Steuerelement zugegriffen werden.
Andererseits führen zeitaufwändige Arbeiten zum "Einfrieren" der Anwendung, weil während der Arbeit natürlich kein Steuerelement mehr reagieren kann.
Die Lösung:*Der aufwändige Job muß isoliert und innerhalb eines Nebenthreads abgearbeitet werden
*Nach Erledigung muß die Weiter-Verarbeitung (mindestens die "Erfolgsmeldung") wieder im Gui-Thread stattfinden, denn ohne Steuerelemente kann nichts gemeldet werden.
Folglich braucht man einen "Verlege-Mechanismus", der einen Methoden-Aufruf in den Nebenthread verlegt, und einen, der einen Methoden-Aufruf in den Gui-Thread verlegt.
Diese Mechanismen existieren, sind aber in der Anwendung recht umständlich:*Gui-Thread -> NebenThread:
System.IAsyncResult ConcreteDelegate.BeginInvoke({verschiedene Argumente}, System.AsyncCallback callback, object object)
*NebenThread -> Gui-Thread:
System.IAsyncResult Control.BeginInvoke(System.Delegate method, params object[] args)
Auf die Tücken von IAsyncResult und AsyncCallback gehe ich hier nicht ein, das möge man hier nachlesen. b) hat außerdem die Komplikation, daß zusätzlich zum Delegaten ein konkretes Control erforderlich ist (egal welches!). Aber was nun, wenn etwa eine Datenklasse ein Ereignis feuern will? - da ist kein Control verfügbar.
Also habich ein paar statische Methoden in einer statischen Klasse geproggt, die die Umständlichkeiten wegkapseln. Die zu verlegenden Methoden brauchen nicht geändert zu werden, sie werden nur modifiziert aufgerufen (Beispiel anhand einer Methode void Calculate(DateTime, Point)
) :
Calculate(DateTime.Now, e.Location); // Standard-Aufruf im akt. Thread
Crossthread.RunAsync(Calculate, DateTime.Now, e.Location); // verlagert in NebenThread
Crossthread.RunGui(Calculate, DateTime.Now, e.Location); // zurück-verlagert in Gui-Thread
Die Tatsache, daß die Crossthread-Methoden auch Extension-Methods generischer Action-Delegaten sind, wird häufig gar nicht ausgenutzt, weil das eine Delegat-Variable erfordert, die extendet werden kann.
Hat man eine solche, so stehen die 3 Aufruf-Varianten allerdings mit ganz identischer Signatur zur Verfügung, was den Umgang sehr erleichtert:
// ( Action<DateTime, Point> calculate = Calculate; )
calculate(DateTime.Now, e.Location); // Standard-Aufruf im akt. Thread
calculate.RunAsync(DateTime.Now, e.Location); // verlagert in NebenThread
calculate.RunGui(DateTime.Now, e.Location); // zurück-verlagert in Gui-Thread
Ein wesentlicher Trick der CrossThreadX-Klasse besteht darin, daß als invokendes Control einfach Application.OpenForms[0]
hergenommen wird. Das ist global zugreifbar, solange überhaupt irgendein Form angezeigt wird (und andernfalls ist eine WinForm-Anwendung i.A. ja beendet). Damit können also auch Klassen, die sonst nichts vom Gui wissen, Methoden sicher im Gui-Thread ausführen, insbesondere auch Events auslösen.
Edit: Den Original-Code habich jetzt rausgeschmissen, zugunsten des Uploades.
Der Upload enthält eine Version mit Extension-Methods (Framework 3), und eine für Framework 2, und je eine Sample-Solution, die die Verwendung der CrossThread-Klasse mit der (herkömmlichen) Verwendung eines Backgroundworkers vergleicht.
Jetzt auch eine im Nebenthread arbeitende Klasse, die ein Event im Gui-Thread auslöst.
Schlagwörter: <Threading, crossthread, Invoke, BeginInvoke,Gui,blockieren,einfrieren>
Der frühe Apfel fängt den Wurm.
using System;
using System.Runtime;
using System.Threading;
using System.Windows.Forms;
public static class CrossThreadX
{
private static WaitCallback callback = new WaitCallback(Invocation);
class TargetInfo
{
internal TargetInfo(Delegate d, object[] args)
{
Target = d;
Args = args;
}
internal readonly Delegate Target;
internal readonly object[] Args;
}
public static void RunAsync(this Delegate d, params object[] args)
{
ThreadPool.QueueUserWorkItem(callback, new TargetInfo(d, args));
}
private static void Invocation(object o)
{
TargetInfo ti = (TargetInfo)o;
ti.Target.DynamicInvoke(ti.Args);
}
private static bool InvokeGui(Delegate d, params object[] Args)
{
if (Application.OpenForms.Count == 0)
{
//wenn kein Form mehr da ist, so tun, als ob das Invoking ausgeführt wäre
return true;
}
if (Application.OpenForms[0].InvokeRequired)
{
Application.OpenForms[0].BeginInvoke(d, Args);
return true;
}
return false;
}
public static void RunGui(this Delegate d, params object[] args)
{
if (!InvokeGui(d, args)) Invocation(new TargetInfo(d, args));
}
}
Hab mir mal die Freiheit genommen und den Code ein wenig in Richtung General Purpose verschoben. Was mich noch stört ist das Application.OpenForms[0], könnte ja sein, dass auf ganz einer anderen Form operiert wird.
Shift to the left, shift to the right!
Pop up, push down, byte, byte, byte!
YARRRRRR!
Hmm, damit hast du aber die Typisierung in die Tonne getreten.
public static void RunAsync(this Delegate d, params object[] args)
Da kann ja jedem Delegaten alles untergeschoben werden.
Edit
Zu Application.Openforms[0]:
Ich hab das mal fett und umständlich umgebaut, undn System.Threading.SynchronisationContext eingebaut, weil jemand meinte, das sei sauberer, und dann habich im Reflector geguckt - letztlich macht der SynchronisationContext beiner WinForm-Application auch nix anneres als Control.BeginInvoke().
Und Control.BeginInvoke macht
return (IAsyncResult) this.FindMarshalingControl().MarshaledInvoke(this, method, args, false);
und ich glaub fast, FindMarshalingControl() gibt das Openforms[0] zurück, sicher binnich natürlich nicht.
Der frühe Apfel fängt den Wurm.
Ja klar, Kindersicherung fehlt da.
Shift to the left, shift to the right!
Pop up, push down, byte, byte, byte!
YARRRRRR!
Hi ErfinderDesRades,
es ist toll, dass du CrossThread(X) gemacht hast. Ich habe hier ein Problem:
Unter .NET Framework konnte ich CrossThreadX ohne Probleme einsetzen. Aber ich sollte das Programm möglichst .NET Framework 2 kompitabel machen und habe probiert, CrossThreadX.cs gegen CrossThread.cs auszutauschen.
Hier ist Fehlermeldung:
Fehler 1 Die Verwendung von Typ "System.Action<T>" (generisch) macht das 1-Typargument erforderlich. (Zeile: 10, 15, 22, 38, 42 und 48)
Habei ich da irgendwas vergessen?
Obiger Zip enthält jetzt eine 05er und eine 08er Version 🙂
Der frühe Apfel fängt den Wurm.
Ich weiß, ich habe 05er version nur das "CrossThread.cs" datei rausgeholt und in mein Projekt implementiert. Dann kam fehlermeldung raus, nachdem ich mein VS auf Zielframe 2.0 umgestellt habe.
Hallo,
irgendwie stehe ich hier gerade auf dem Schlauch:
if (Application.OpenForms.Count == 0)
{
//wenn kein Form mehr da ist, so tun, als ob das Invoking ausgeführt wäre
return true;
}
Sollte er nicht lieber false zurückgeben damit das Action Delegate trotzdem ausgeführt wird. Falls die CrossThread Geschichte allgemein eingesetzt wird aber in dem Fall gar nicht von einer Windows.Forms Anwendung, sondern von irgendeiner Library verwendet wird, würde das Action Delegate doch jetzt gar nicht ausgeführt werden oder sehe ich das falsch.
Eigentlich müsste es dann an dieser Stelle doch so behandelt werden als wenn kein Invoke erforderlich ist, oder nicht? am Kopf kratz
Gruß
Christoph
Hintergrund ist, es ist nur für WinForms gebastelt.
Und wenn da kein Form mehr da ist, dann ist die Anwendung beendet.
Sowas tritt auf, wenn ein Job im Nebenthread gestartet wurde, und in der ZwiZeit die Anwendung zugemacht wurde. Dann will der Nebenthread noch seine Erfolgsmeldung absetzen, aber das Gui ist nicht mehr da.
Und da tu ich halt so, als wärs schon passiert, damit nicht auf Steuerelemente zugegriffen wird, die schon disposed sind.
Einfach, dass die Anwendung schließen kann ohne Exception.
Der frühe Apfel fängt den Wurm.
Ich würde das ganze halt gerne für eine JobQueue benutzen. Die Queue arbeitet die Jobs alle hintereinander ab und erstellt für jeden Job einen neuen Thread. Wenn der Job abgearbeitet ist, wird ein Event gefeuert. Dieses Event soll jetzt allerdings wieder in dem Haupthread (den der Queue) gefeuert werden.
Auf diese Weise braucht der Entwickler, der die Queue verwendet im GUI nicht invoken. Das klappt auch alles super. Nur möchte ich natürlich auch sicherstellen, dass diese Queue auch in einer Nicht-WindowsForms Umgebung funktioniert.
Deswegen dachte ich, dass es theoretisch reichen sollte, wenn ich an der besagten Stelle false zurückgebe, was dann wiederum bedeutet, dass das Action Delegate direkt ausgeführt werden kann.
Allerdings stört mich der Verweis auf System.Windows.Forms generell. Es sollte halt einfach so sein, dass das Event im Thread der Queue und nicht im Thread des Jobs gefeuert wird und ob der Thread der Queue dann zufällig der GUI Thread ist oder eben einfach nur der Hauptthread sollte eigentlich keine Rolle spielen. Vielleicht sollte ich mir hierfür die SynchronisationContext Geschichte noch mal ansehen...
Hallo bvsn,
an dieser Stelle sollten wir nur festhalten, dass das Snippet eben für Windows-Forms ausgelegt ist und deshalb auch konsequenterweise true zurückgegeben wird. Wenn du zu deinem Fall noch Fragen hast, mach bitte einen neuen Thread auf. [EDIT]Siehe CrossThreadX - WorkerJobQueue[/EDIT]
herbivore