Delegaten und Asynchron-Verhalten - Verständnisproblem

  • WPF

Es gibt 10 Antworten in diesem Thema. Der letzte Beitrag () ist von PadreSperanza.

    Delegaten und Asynchron-Verhalten - Verständnisproblem

    Hallo Leute, ich steh mal wieder - ziemlich - auf dem Schlauch. Diesmal aber mit einem generellen Verständnisproblem was die Delegaten und den Asynchronen Aufruf angeht.

    Kurzum: ich habe ein WPF-Programm, das einiges an Dingen abarbeitet und darin befindet sich auch eine Instanz von einer Console, die alles, was geloggt werden muss, loggt. Das funktioniert ohne Probleme. Nun möchte ich jedoch eine Applikation (später auch Webseite, etc. ) aus dem Programm heraus starten. Manche Startvorgänge brauchen jedoch einiges an Zeit, bis sie fertig sind, deshalb möchte ich das Starten gerne in einen anderen Thread auslagern. Folgender Code liegt derzeit vor (alles leider in c#, ich hoffe, ihr vergebt mir):

    C#-Quellcode

    1. public static bool ExecuteManagedApp(ManagedApp MA, Console console)
    2. {
    3. ArgArray = MA.Args.Split();
    4. try
    5. {
    6. switch (MA.MyType)
    7. {
    8. case Enums.AppliactionTypes.Application:
    9. Thread thread = new Thread(ApplicationThread);
    10. thread.Start(new object[] {console, MA.Path, MA.Args });
    11. return true;
    12. case Enums.AppliactionTypes.Folder:
    13. Thread thread1 = new Thread(FolderThread);
    14. thread1.Start(new object[] { MA.Path });
    15. return true;
    16. case Enums.AppliactionTypes.Website:
    17. Thread thread2 = new Thread(WebsiteThread);
    18. thread2.Start(new object[] { MA.Path });
    19. return true;
    20. }
    21. }
    22. catch (System.IO.FileNotFoundException)
    23. {
    24. console.AddLog("Das angegebene Programm/die Webseite <" + MA.Description + "|" + MA.Path + "> konnte nicht gefunden werden", LogLevel.Error);
    25. return false;
    26. }
    27. catch (System.Exception)
    28. {
    29. console.AddLog("Schwerwiegender Fehler durch den Aufruf von <Executer<" + MA.Description + "<" + MA.MyType + "<" + MA.Path + "<" + MA.Args + ">>>>>", LogLevel.Error);
    30. return false;
    31. }


    Wir fokussieren uns erstmal nur auf den ersten Teil der Switch-Anweisung: Case: Enums.ApplicationType.Application. Hier soll dann die Funktion ApplicationThread aufgerufen werden. Sie ist wie folgt definiert:

    C#-Quellcode

    1. private static void ApplicationThread(object param)
    2. {
    3. try
    4. {
    5. var t = new string[] { ((param as object[])[1] as string), ((param as object[])[2] as string) };
    6. System.Diagnostics.Process process = System.Diagnostics.Process.Start(t[0], t[1]);
    7. while (true)
    8. {
    9. try
    10. {
    11. var time = process.StartTime;
    12. ((param as object[])[0] as Console).AddLog("Thread|Start: " + t[0] + " | " + time.ToString(), LogLevel.Detail);
    13. break;
    14. }
    15. catch { }
    16. }
    17. }
    18. catch (System.IO.FileNotFoundException)
    19. {
    20. ((param as object[])[0] as Console).AddLog("Das angegebene Programm <" + ((param as object[])[1] as string) + "> konnte nicht gefunden werden", LogLevel.Error);
    21. }
    22. catch (System.Exception)
    23. {
    24. ((param as object[])[0] as Console).AddLog("Schwerwiegender Fehler durch den Aufruf von <" + ((param as object[])[1] as string) + ">|Application", LogLevel.Error);
    25. }
    26. }


    Da nun aber die console sich im Thread der UI befindet, verursachen selbstverständlich die Aufrufe innerhalb der ApplicationThread-Methode Fehler, da sie auf ein Element eines anderen Threads zugreifen möchten.

    Ich weiß auch, dass ich mit Delegaten und Invoke() arbeiten müsste, auch, dass WPF eine Dispatcher-Funktion zur Verfügung stellt, um das zu ermöglichen. Doch hier setzt mein Verständnis aus und die beispiele auf Webseiten zeigen immer Code-Schnipsel, die aber nie das Problem beschreiben, auf das ich treffe. Und somit fehlt mir etwas das Verständnis hierfür.

    Deshalb zu Frage:
    Wie müsste ich generell in der ApplicationThread-Methode auf die console zugreifen, welche als Instanz im MainWindow befindet?
    Wo müsste welcher Delegate, welcher Aufruf mit welchen Parametern geschehen, damit ich der Console aus dem Main-Thread sagen kann, sie solle die Logs hinzufügen?

    Zur Erläuterung der vielleicht merkwürdig aussehenden ApplicationThread-Methode:
    Der innere Try-Catch-Block soll die Process-started-time ermitteln (die liegt aber vielleicht noch nicht vor), deshalb try-catch. Und das soll solange passieren bis es erfolgreich passiert ist. Und diese Zeit möchte ich dann am liebsten in die Console schreiben und danach kann der Prozess verworfen werden. Ebenso wenn es dort zu Fehlern kommt, sollen diese ebenfalls protokolliert werden.

    Vielleicht kann mir jemand das Verständnis für diese ganzen Delegaten-Sache an meinem konkreten Beispiel erläutern. Ich brauche auch nicht zwangsläufig "den Code dafür", sondern das generelle Verständnis hierfür (auch wenn es mit Code natürlich eifnacher zu verstehen ist :P )

    Vielleicht kann mir jemand helfen. Wenn noch mehr Code gebraucht wird (ist nur ein seeeeeehr kleiner Auszug meines vollen Programms), dann eifnach sagen, was ihr braucht.

    Ich danke euch

    PadreSperanza schrieb:

    Da nun aber die console sich im Thread der UI befindet, verursachen selbstverständlich die Aufrufe innerhalb der ApplicationThread-Methode Fehler, da sie auf ein Element eines anderen Threads zugreifen möchten.
    Glaub ich garnetmal. Ist Console denn ein Control??
    Bei mir spuckt der ObjectBrowser zu Console folgendes aus:

    ObjectBrowser schrieb:

    public static class Console
    Member of System

    Summary:
    Represents the standard input, output, and error streams for console applications. This class cannot be inherited.To browse the .NET Framework source code for this type, see the Reference Source.
    (Da wundert mich allerdins, wie du sowas als Methoden-Argument verwenden kannst.)

    Jedenfalls wenn wir vonne selben Console reden, dann ist Threading glaub unnötig und fehl am Platz.
    Aber sicher bin ich nicht - aber provozier erstmal sone Exception, die du mit Threading zu vermeiden versuchst. (Und poste Fehlerzeile, Exception, Fehlertext)
    ja, ne. Ich habe tatsächlich eine eigene Klasse Console, welche von RichTextBox erbt.

    C#-Quellcode

    1. public class Console : System.Windows.Controls.RichTextBox
    2. {
    3. protected override void OnMouseEnter(MouseEventArgs e)
    4. {
    5. this.Cursor = Cursors.Arrow;
    6. base.OnMouseEnter(e);
    7. }
    8. public int Errors
    9. {
    10. get { return (int)GetValue(ErrorsProperty); }
    11. set { SetValue(ErrorsProperty, value); }
    12. }
    13. // Using a DependencyProperty as the backing store for Errors. This enables animation, styling, binding, etc...
    14. public static readonly DependencyProperty ErrorsProperty =
    15. DependencyProperty.Register("Errors", typeof(int), typeof(Console), new PropertyMetadata(0));
    16. public int Warnings
    17. {
    18. get { return (int)GetValue(WarningsProperty); }
    19. set { SetValue(WarningsProperty, value); }
    20. }
    21. public static readonly DependencyProperty WarningsProperty =
    22. DependencyProperty.Register("Warnings", typeof(int), typeof(Console), new PropertyMetadata(0));
    23. public int Infos
    24. {
    25. get { return (int)GetValue(InfosProperty); }
    26. set { SetValue(InfosProperty, value); }
    27. }
    28. public static readonly DependencyProperty InfosProperty =
    29. DependencyProperty.Register("Infos", typeof(int), typeof(Console), new PropertyMetadata(0));
    30. public Console()
    31. {
    32. }
    33. internal void Init()
    34. {
    35. this.Background = (System.Windows.Media.Brush)new System.Windows.Media.BrushConverter().ConvertFromString("gray");
    36. AddLog("Program has started", LogLevel.Detail);
    37. this.FontFamily = new System.Windows.Media.FontFamily("Courier New");
    38. }
    39. internal void AddLog(string s, LogLevel l)
    40. {
    41. System.Windows.Media.SolidColorBrush b = System.Windows.Media.Brushes.Black;
    42. if (l == LogLevel.Error)
    43. {
    44. b = System.Windows.Media.Brushes.MediumVioletRed;
    45. Errors ++;
    46. }
    47. else if (l == LogLevel.Warning)
    48. {
    49. b = System.Windows.Media.Brushes.DarkOrange;
    50. Warnings += 1;
    51. }
    52. else if (l == LogLevel.Detail)
    53. {
    54. b = System.Windows.Media.Brushes.Beige;
    55. }
    56. else
    57. {
    58. Infos += 1;
    59. }
    60. this.Document.Blocks.Add(new Paragraph(new Run(GetLogLevel(l) + DateTime.Now + " - " + s) { Foreground = b }));
    61. //this.AppendText(new Run(GetLogLevel(l) + DateTime.Now + " - " + s + Environment.NewLine));
    62. }
    63. private string GetLogLevel(LogLevel l)
    64. {
    65. if (l == LogLevel.Error) return "ERR.: ";
    66. else if (l == LogLevel.Warning) return "WAR.: ";
    67. else if (l == LogLevel.Info) return "INF.: ";
    68. else if (l == LogLevel.Detail) return "DET.: ";
    69. else return "UNK.: ";
    70. }
    71. }


    Diese RichTextBox wird in MainWindow eingebunden und soll alles mitloggen, was an wichtigen Elementen vorherrscht.

    Die Dependency Properties sind dafür, dass die Console insgesamt zusammenzählt wie viele Einträge pro Typ vorhanden sind.

    EDIT: ich bekomme folgende Fehlermeldung: System.InvalidOperationException: "Der aufrufende Thread kann nicht auf dieses Objekt zugreifen, da sich das Objekt im Besitz eines anderen Threads befindet."

    Weil ich console.AddLog(s,l) in einem neu erstellten Thread ausführen möchte.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „PadreSperanza“ ()

    Jo, dann isses ein Thread-übergreif-Problem.
    Aber mir sieht der Code aus, als sei er sehr abseits vom MVVM-Pattern.
    Alles wäre einfacher, hättest du ein ViewModel mit dem anzuzeigenden Text. Da könntest du eine normale Richtextbox dran binden und gut - keine selber basteln.
    Eventuell im ViewModel die Progrss<T> - Klasse benützen, aber glaub ich nichtmal.
    Das Wpf-BindungsSystem ist zumindest in Teilen Threadsicher, soweit ich weiss.
    Ja, aber selbst wenn ich MVVM nutzen würde (da bin ich leider noch nicht recht fit drin), ändert das dann doch dennoch nichts am Problem, dass ich in einem zweiten Thread ein Programm starte und wenn es fertig ist, das Ergebnis in der RichTextBox (thread 1) anzeigen lassen möchte. Deshalb versuche ich zu verstehen, wie ich der RichTextBox einen Log-Eintrag invoken lassen kann, damit die TextBox das selbst macht, angestoßen jedoch durch Thread 2. Genau hier liegt leider mein Problem. Und zwar beim Verständnis, wann ich wie einem anderen Thread übergreifend etwas mitteilen und ausführen lassen kann.

    ErfinderDesRades schrieb:

    nochmal: Mit MVVM kann der NebenThread einfach ins Viewmodel schreiben - wann immer er will.
    Die Richtextbox ist ans Viewmodel gebunden, und zeigt das immer akkurat an.
    Keine besonderen Vorkehrungen erforderlich.

    Ohne MVVM ist meines Erachtens Wpf sinnlos und nur ein irrsinniger Krampf.


    Soweit ich weiß, funktioniert das ebenso wenig. Eine Property, die an einem Control gebunden ist, kann nicht (ohne Dispatcher) von einem Nebenthread aus verändert werden.

    MfG Mika
    Ich weiß, dass eine WPF-Applikation, wenn sie gestartet wird, im Hintergrund einen Thread öffnet, der von einem sogenannten Dispatcher verwaltet wird. Dieser achtet auf den Thread und arbeitet alles ab, was in die Queue des Threads gegeben wird. Er sorgt auch dafür, dass die entsprechenden Tätigkeiten von demjenigen Objekt ausgeführt werden, das dafür notwendig ist. Das Problem ist, ich weiß, dass es den Dispatcher gibt, ich weiß auch, dass man mittels Dispatcher.Invoke() das entsprechende aufrufen kann (soweit habe ich schon versucht, mich reinzulesen). Leider habe ich keine Ahnung, wie ich die Methoden entsprechend schreiben muss, damit ich im zweiten Thread dem Dispatcher des WPF-Threads zurufen kann, dass er eine Ausführung machen soll. :/

    Und ja, auch mit reinem DataBinding komme ich bei mehreren Threads nicht weiter. Und da ich mit MVVM noch nicht 100% warmgeworden bin (ich lerne durch reines Selbststudium durch Bücher und Foren), versuche ich es erstmal auf dem Wege, den ich bisher beherrsche, wobei meine Kenntnisse und Fähigkeiten stetig wachsen. Also habt bitte etwas Nachsicht dabei :)

    Ich werde mir mal die Progress<T>-Klasse ansehen. Vielleicht komme ich damit ja weiter. ich danke dir auf jeden Fall für den Hinweis :)

    PadreSperanza schrieb:

    ch weiß, dass eine WPF-Applikation, wenn sie gestartet wird, im Hintergrund einen Thread öffnet, der von einem sogenannten Dispatcher verwaltet wird. Dieser achtet auf den Thread und arbeitet alles ab, was in die Queue des Threads gegeben wird. Er sorgt auch dafür, dass die entsprechenden Tätigkeiten von demjenigen Objekt ausgeführt werden, das dafür notwendig ist. Das Problem ist, ich weiß, dass es den Dispatcher gibt, ich weiß auch, dass man mittels Dispatcher.Invoke() das entsprechende aufrufen kann (soweit habe ich schon versucht, mich reinzulesen).

    Das ist doch schon eine gute Definition des Dispatchers. ;) Und der Ansatz mit Invoke ist auch richtig, damit kannst du Arbeit von Thread X auf den Dispatcher schieben.

    Mika2828 schrieb:

    Soweit ich weiß, funktioniert das ebenso wenig. Eine Property, die an einem Control gebunden ist, kann nicht (ohne Dispatcher) von einem Nebenthread aus verändert werden.

    Das stimmt soweit nicht (bzw. nur in Teilen). WPF führt jede Property-Änderung die per INotifyPropertyChanged propagiert wird via Dispatcher auf dem UI Thread aus. Deswegen hat EDR auch Recht: Das MVVM Pattern benutzt konventionell das INotifyPropertyChanged Interface um Änderungen bekannt zu machen, weshalb Bindings an ViewModels auch per default threadsicher sind (zumindest von der UI Seite her).
    Deswegen auch von mir der Rat an den TE: Schau dir MVVM an. Es ist - ohne Diskussion - der beste Weg, mit WPF zu arbeiten. Mach es dir nicht mit Ausreden wie "Kenn ich noch nicht gut, lerne ich später" bequem, sondern lerne es *jetzt*. Ja, es ist anfangs schwer, aber auf lange Sicht wirst du dir einen unfassbar großen Aufwand sparen, wenn du jetzt die Zeit investierst.

    Nichtsdestoweniger ist es natürlich auch sinnvoll zu wissen, wie man den Dispatcher richtig nutzt, bzw. wie man Tasks auf den UI Thread auslagert. Du hast ja schon Dispatcher.Invoke angesprochen. Jede Zeile Code, die in einem Invoke steckt, wird korrekt auf dem Dispatcher Thread ausgeführt. Als Beispiel eine fiktive TextBox, deren Text wir von einem anderen Thread ändern wollen:

    C#-Quellcode

    1. var tb = new TextBox();
    2. new Thread(() =>
    3. {
    4. tb.Dispatcher.Invoke(() => tb.Text = $"Dieser Text wurde von einem anderen Thread gesetzt!");
    5. }).Start();


    Kannst du das auch auf deine Console TextBox anwenden? ;)
    :thumbup: Ich bin aber vielleicht auch ein Schaf :D

    Mein Ansatz war also gar nicht doof. Ich wusste nur nicht, wer wie den Invoke-Befehl wo aufrufen muss. Aber ja, in meinem Fall sähe das Resultat nun so aus:

    C#-Quellcode

    1. private static void ApplicationThread(object param)
    2. {
    3. try
    4. {
    5. var t = new string[] { ((param as object[])[1] as string), ((param as object[])[2] as string) };
    6. System.Diagnostics.Process process = System.Diagnostics.Process.Start(t[0], t[1]);
    7. Console con = ((param as object[])[0] as Console);
    8. con.Dispatcher.Invoke(() => con.AddLog("Programm <" + ((param as object[])[1] as string) + "> gestartet.", LogLevel.Detail));
    9. }
    10. //...
    11. }


    Ich hatte scheinbar zwei offensichtliche Probleme:

    1. ich habe nicht kapiert, dass jedes Controls-Element den Dispatcher nutzen kann

    C#-Quellcode

    1. con.Dispatcher.Invoke(...);

    2. Wie muss ich das dann formulieren. Ich habe die ganze Zeit versucht, zu verstehen, was mit "Action" gemeint ist. Das ist aber durch die "Aktion"

    C#-Quellcode

    1. Invoke( () => ...);

    formuliert.
    Jetzt funktioniert das alles super einfach und nun habe ich das auch verstanden, wer das wie aufrufen muss. Ihr habt mir wirklich geholfen :) !!!! Vielen Dank!!!
    Durch diesen einfachen Text kann ich nun bei Einträgen, die als Fehlern erkannt werden, ein Warnhinweis optisch Invoken :) Mehr bekommen als ich erstmal eigentlich wollte.


    Und ja, MVVM ist das nächste, das ich anfangen werde. Leider habe ich nur eine Frist, innerhalb derer ich das Programm vorstellen muss, um es eventuell dann auch einführen und nutzen zu dürfen. Deshalb muss ich - zumindest im Augenblick - mit meinen Kenntnissen Vorlieb nehmen. Aber ich bin dran und lerne fleißig weiter. Auch dank euch allen hier