TextBox.AppendText() von anderem Thread über Delegate



  • Hallo,

    ich möchte Werte in der TextBox meiner GUI angeben, die von einem anderen Thread stammen, der ständig ausgeführt wird. Ich habe den Tip bekommen, das sauber über ein Delegate zu lösen. Nun habe ich über Google sehr viele unterschiedliche Beispiele gefunden. Eines davon habe ich nun umgesetzt, bin mir aber nicht sicher, ob das der richtige und saubere Weg ist, funktionieren tut es. Die Experten müssten aber gleich erkennen ob es sauber ist:

    So sieht meine Definition in der Klasse "GUI : Form" aus:

    public delegate void UpdateStatusBoxDelegate(string TextLine);
            public void UpdateSatusBox(string TextLine)
            {
                this.StatusBox.AppendText(TextLine);
            }
    

    Und so würde ich es eben aus der Klasse aufrufen, wo der Thread läuft:

    GUI.UpdateStatusBoxDelegate UpdateIt = GUI.UpdateSatusBox;
                UpdateIt.Invoke(statusText);
            }
    

    Ist das OK so? Weil das selbe werde ich für weitere Prozesse anwender, wie bspweise eine ProgressBar...



  • Deine Klasse mit dem Thread sollte die Form nicht kennen. Wirf dort lieber ein Event, das von der Form abonniert wurde. Dieser Eventhandler wird dann natürlich nicht im GUI-Thread ausgeführt. Also musst Du "Invoke" des entsprechenden Controls aufrufen und zwar innerhalb des Eventhandlers.

    Was ist bei Dir "statusText"? Invoke erwartet einen Delegaten, kein string oder sonst was. (Parameter kannst Du natürlich trotzdem mitübergeben in einer überladenen Variante von Invoke(...,object[]))

    Mir ist es zu umständlich immer Delegaten zu definieren. Ich verwende deswegen gerne so etwas:

    internal class ThreadUI
        {
            public delegate void threadDel(Control control, string propertyName, object value);
    
            public static void setProperty(Control control, string propertyName, object value)
            {
                if (control.InvokeRequired)
                    control.Invoke(new threadDel(setProperty), new object[] { control, propertyName, value });
                else
                    control.GetType().GetProperty(propertyName).SetValue(control, value, null);
            }
        }
    

    Wenn Du eine TextBox StatusBox hast kannst Du dann einfach mit diesem Aufruf das Text-Property setzen:
    ThreadUI.setProperty(StatusBox, "Text", "mein neuer Text für das Text-Property der StatusBox");

    Das funktioniert natürlich auch für jedes andere Property, jedes anderen Controls.



  • Klasse, vielen Dank für die Aufklärung, werde es gleich mal umsetzen, danke 🙂



  • Sorry:

    Was ist bei Dir "statusText"?

    Eine Variable vom typ string.

    Was genau ist jetzt der Vorteil deiner Vorgehensweise?



  • Na ich lagere den Code irgendwo aus und muss dann immer nur sowas aufrufen:

    ThreadUI.setProperty(StatusBox, "Text", "mein neuer Text");

    oder sowas

    ThreadUI.setProperty(myLabel, "Visible", false);

    oder sowas

    ThreadUI.setProperty(myCheckBox, "Checked", true);

    oder ...



  • µ schrieb:

    Deine Klasse mit dem Thread sollte die Form nicht kennen. Wirf dort lieber ein Event, das von der Form abonniert wurde. Dieser Eventhandler wird dann natürlich nicht im GUI-Thread ausgeführt. Also musst Du "Invoke" des entsprechenden Controls aufrufen und zwar innerhalb des Eventhandlers.

    Was ist bei Dir "statusText"? Invoke erwartet einen Delegaten, kein string oder sonst was. (Parameter kannst Du natürlich trotzdem mitübergeben in einer überladenen Variante von Invoke(...,object[]))

    Mir ist es zu umständlich immer Delegaten zu definieren. Ich verwende deswegen gerne so etwas:

    public static void setProperty(Control control, string propertyName, object value)
            {
                if (control.InvokeRequired)
                    control.Invoke(new threadDel(setProperty), new object[] { control, propertyName, value });
                else
                    control.GetType().GetProperty(propertyName).SetValue(control, value, null);
            }
        }
    

    Das new object[] ist nicht notwendig da das ein Param parameter ist.



  • Soweit so gut,

    wie stelle ich das ganze aber an, wenn ich mehrere values für ein control setzen möchte wie zum Beispiel:

    private void SetProgrBarValues(int min, int max, int step)
    {
    this.progrBar.Minimum = min;
    this.progrBar.Maxmimum = max;
    this.progrBar.Step = step;
    }
    

    ...oder mehrere values für mehrere control (auf einen Schlag mehrere Labels aktualisieren), wie zum Beispiel:

    public void SetInfo(string fwName, string fwSize, string busFrequ,
    string cmBaud, string ver)
    {
    this.label22.Text = fwName;
    this.label21.Text = fwSize;
    this.label20.Text = busFrequ;
    this.label24.Text = cmBaud;
    this.label19.Text = ver;
    }
    

    (Diese 2 Methoden stammen aus der Form Klasse)

    Das wäre noch super zu wissen.



  • Noch vergessen:

    ...bzw. wenn ich folgendes aufrufen möchte:

    private void UpdateProgresssBar()
    {
    this.progrBar.PerformStep();
    }
    


  • internal class ThreadUI
        {
            public static void setProperty(Control control, string propertyName, object value)
            {
                if (control.InvokeRequired)
                	control.Invoke( new Action<Control, string, object>(setProperty), control,propertyName, value );
                else
                    control.GetType().GetProperty(propertyName).SetValue(control, value, null);
            }
    
            public static void setProperties(Control control, Action action)
            {
            	if( control.InvokeRequired )
            		control.Invoke(action);
            	else
            		action();
            }
        }
    

    Die erste Methode macht das gleiche wie früher. Nur war das Delegate überflüssig, da es ab NET 3.0 (oder 3.5) ein Action-Delegat gibt.

    Die zweite Methode ist für mehrere Aktionen gedacht. Es findet nur ein Wechsel in den GUI-Thread statt, was performanter ist.

    Beispiel:

    public void SetInfo1(string fwName, string fwSize, string busFrequ, string cmBaud, string ver)
    {
      ThreadUI.setProperty(label22, "Text", fwName);
      ThreadUI.setProperty(label21, "Text", fwSize);
      ThreadUI.setProperty(label20, "Text", busFrequ);
      ThreadUI.setProperty(label24, "Text", cmBaud);
      ThreadUI.setProperty(label19, "Text", ver);
    }
    
    public void SetInfo2(string fwName, string fwSize, string busFrequ, string cmBaud, string ver, int min, int max, int step)
    {
      ThreadUI.setProperties(label22,
    	() => 
    	  {
                     label22.Text = fwName;
                     label21.Text = fwSize;
                     label20.Text = busFrequ;
                     label24.Text = cmBaud;
                     label19.Text = ver; 
                     progrBar.Minimum = min;
                     progrBar.Maxmimum = max;
                     progrBar.Step = step;
                     progrBar.PerformStep();
    	 });
    }
    


  • Hey genial "Mü" danke 🙂

    Ich hatte es zwar hinbekommen, der Code sieht aber schrecklich aus, es ist natürlich nicht so elegant wie deine Lösung, werde es so übernehmen...

    Danke für deine Beteiligung und Hilfe, das Abend-Bier geht auf dich 😉



  • Hehe, reich mir lieber eins (Bier) rüber 😉



  • *BeckRüberReich* 🙂

    So eine Frage hätte ich jetzt doch:
    Ich habe das nun mit der Progressbar getestet. Habe mir eine Schleife gebastelt um die Progressbar langsam zu füllen. Es funktioniert auch sehr gut. Nur so lange eben die Progressbar gefüllt wird, ist die GUI nicht zugänglich, ich kann sie beispielsweise nicht verschieben.

    Das ist in der GUI : Form Klasse

    public void AccessGUI(Control control, Action action)
            {
                if (control.InvokeRequired)
                    control.Invoke(action);
                else
                    action();
            }
    

    Das in der Klasse in dem der andere Thread läuft:

    public void SetPBValue2()
            {
                GUI.F_GUI.AccessGUI(GUI.F_GUI.toolStripProgressBar1.Control, () =>
                {
    
                    GUI.F_GUI.toolStripProgressBar1.PerformStep();
    
                });
            }
    
            public void StartProgressBar()
            {
                for (int i = 0; i < 100000000; i++)
                {
                    if (i % 1000000 == 0)
                    {
                        SetPBValue2();
                    }
                }
            }
    

    Über einen Button in der GUI rufe ich eben StartProgressBar() auf, und die ProgressBar wird gefüllt. Erst nach dem Füllen, ist sie wieder zugänglich.



  • StartProgressBar läuft im GUI-Thread. Natürlich ist die GUI blockiert bis die Schleife abgearbeitet wurde.

    Ganz allgemein solltest Du die Anzahl der Wechsel in den GUI-Thread möglichst gering halten. Eine Progressbar (mit Threadwechsel) 1000 mal in der Sekunde zu "stepen" ist unnötig und extrem unperformant.

    Mit Invoke kann man auch schnell Deadlocks bauen. Also vorsicht 😉



  • StartProgressBar läuft im GUI-Thread. Natürlich ist die GUI blockiert bis die Schleife abgearbeitet wurde.

    OK stimmt hätte ich doch selbst so schlussfolgern können 🙂

    Eine Progressbar 1000 mal in der Sekunde zu "stepen" ist unnötig und extrem unperformant.

    Das war nur testweise 🙂

    Mit Invoke kann man auch schnell Deadlocks bauen. Also vorsicht ;)
    

    Ja meine GUI-Zugriffe beschränken sich hauptsächlich auf das gelegentliche aktualisieren von textboxen oder labels. Es gibt 2 prozesse bei denen ich die Progressbar benutze, die dauern aber maximal 35 sekunden.

    Super dann wäre alles endgültig geklärt, mir gefällt die lösung sehr gut, sieht sehr sauber aus, ich spare einige zeilen im gegensatz zu meiner "hauptsacheEsFunktioniertMitDelegaten"-Lösung 🙂

    Da sind dann heute 2 Bier drin die auf dich gehen 😉 Schönes WE



  • Dir auch einen schönen Samstag-Abend 👍



  • Du schreibst ja selbst, dass es nur ein Beispiel war. Aber ich möchte gerne noch etwas anmerken.

    Joeyeay078087679697 schrieb:

    public void SetPBValue2()
            {
                GUI.F_GUI.AccessGUI(GUI.F_GUI.toolStripProgressBar1.Control, () =>
                {
    
                    GUI.F_GUI.toolStripProgressBar1.PerformStep();
    
                });
            }
    
    1. Du hast AccessGUI in deine Form gepackt. Bedenke, dass Du diese Methode (und die andere aus der ThreadUI-Klasse von mir) wunderbar auslagern und in JEDER Form wiederverwenden kannst. Zum Beispiel in eine Utility-DLL.

    2. Es sieht so aus, als würde deine Arbeiterklasse immer noch die Form kennen. Und die Form hast Du wahrscheinlich über ein Singleton global bekannt gemacht (GUI.F_GUI). Das ist beides nicht so toll. Keine Hilfs-, Arbeiter- oder ThreadWorkerklasse sollte über die GUI bescheid wissen. Es ist sehr viel sauberer wenn solche Klassen niedriger Ebene (im Schichtenmodell) Events werfen bei interessanten Ereignissen, und die GUI (d.h. die entsprechenden Forms) diese Events abonnieren. Innerhalb der Eventhandler verwendest Du dann einfach die sauber ausgelagerte AccessGUI-Methode.



  • Hallo Joey...

    du solltest dir auch mal die BackgroundWorker-Klasse anschauen, welche insbesondere für Aktualisierungen von ProgressBar etc. einfacher zu bedienen ist (die ProgressChanged-Ereignismethode läuft automatisch im GUI-Thread, so daß manuell dann kein Invoke mehr nötig ist).



    1. Deine Controls machst Du public: GUI.F_GUI.toolStripProgressBar1.Control. Das ist ganz übel. Du kannst diese Abhängigkeiten aus deinem Code entfernen, durch ein Design mittels Events.


  • Oh man ich blicke gar nicht mehr durch.
    Ist mein 1. C# Projekt, in Java war das immer einfacher zu lösen.

    Nochmal zusammengefasst:
    -Ich habe eine Form-Klasse "GUI"
    -Ich habe eine Klasse mit dem anderen Thread "MyThread".

    Ich habe insgesamt 5 "Prozesse", also Zugriffe auf verschiedene Elemente der Form:
    -UpdateProgrammInfo(5 strings die 5 verschiedene Labels aktualisieren sollen)
    -UpdateStatusBox(1 string der auf eine TextBox geschrieben werden soll)
    -UpdateConnectionStatus(1 ToolStripStatusLabel dessen Text und Farbe geändert werden soll)
    -SetProgressBarValues(ProgressBar Values Minimum, Maximum und Step setzen)
    -UpdateProgressbar(ProgressBar per PerformStep() erhöhen)

    Wo erstelle ich jetzt was womit bzw. wo definiere ich was und wie erfolgen die Zugriffe? Sorry ich blicke nicht mehr durch 😞



  • Also ich hoffe das ist jetzt sauber, hatte einen kleinen Workaround über Guggle gefunden. Ein einfaches Beispiel:

    In meiner "GUI : Form" Klasse definiere ich ein Objekt der Klasse GUIAccess, wo ich die Delegate und Events deklariere, die Methode die Veränderungen auf der GUI durchführt, und Abonniere das Event dazu:

    GUIAccess Gu = new GUIAccess;
    
    private void UpdateLabel(string Text)
    {
    this.label1.Text = Text;
    }
    
    Gu.UpdateLabelDelegateEvent += new Gu.UpdateLabelDelegate(UpdateLabel);
    

    In meiner Klasse "GUIAccess" deklariere ich das Delegate, das Event, und die Methode die von dem anderen Thread aufgerufen wird, wenn das Event ausgelöst werden soll:

    public delegate void UpdateLabelDelegate(string Text);
    
    public event UpdateLabelDelegate UpdateLabelDelegateEvent;
    
    public void EventAusloesen()
    {
    UpdateLabelDelegateEvent("Neuer Wert");
    }
    

Log in to reply