C# Delegaten/Invoke/Thread/Backgroundworker?
Huhu,
bin aktuell am rumexperimentieren, da ich es am lernen bin. Dieses Test Form hat nur 2 Dinge die es machen soll.
- Das Form Load wird mit einem Label gestartet um den Spieler zu begrüßen. Nach 2 bzw 3 Sekunden soll dass label unsichtbar werden mithilfe eines Backgroundworkes und delegaten. Alles startet normal. Nach den besagten paar Sekunden wirft er mir eine exeption. Als Grund nennt er mir immer das label1. Meine Vermutung ist, dass label 1 ja bereits Form1 Thread läuft und daher nicht mehr geändert werden kann über einen Backgroundworker.
Die Exeption lautet : Ungültiger threadübergreifender Vorgang: Der Zugriff auf das Steuerelement label1 erfolgte von einem anderen Thread als dem Thread, für den es erstellt wurde."
- Der zweite Punkt wäre, dass bei einem Button click ein Fenster aufgehen soll und sich nach ein paar Sekunden wieder schließen soll. Das tut es auch. Drücke ich aber nochmal den Button, wirft er mir diese Exeption: System.ObjectDisposedException: "Auf das verworfene Objekt kann nicht zugegriffen werden.Objektname: "Skynet"."
Hier ist der Code:
Für den Kenner ist das bestimmt alles Kraut und Ruben. Kurz: Ein Chaos. Aber aktuell ist es eher Try and Error :)
public partial class Form1 : Form
{
public delegate void invokeDelegate();
//Backgroundworker wird initialisiert
public BackgroundWorker arbeiter = new BackgroundWorker();
public BackgroundWorker arbeiter2 = new BackgroundWorker();
//Delegaten
public delegate void deleWorker (BackgroundWorker a);
public delegate void gruss(BackgroundWorker b);
//Begrüßungsfenster
Skynet hello = new Skynet();
//Zeigt Skynet Fenster an
public void Show()
{
hello.Show();
Thread.Sleep(3000);
this.hello.Close();
hello.Dispose();
}
//Willkommensgruß wird für 2 Sekunden angezeigt
private void Willkommensgruss(Label a)
{
a.Text = "Willkommen, Fremder.";
Thread.Sleep(2000);
}
public Form1()
{
InitalizeComponent();
arbeiter.DoWork += backgroundWorker1_DoWork;
arbeiter2.DoWork += backgroundWorker2_DoWork;
Skynet hello = new Skynet();
}
private void Form1_Load(object sender, EventArgs e)
{
//delegate
gruss hallo = new gruss(backgroundWorker2.RunWorkerAsync);
hallo.Invoke(backgroundWorker2);
}
//Öffnet Skynet Fenster
private void button2_Click(object sender, EventArgs e)
{
deleWorker test = new deleWorker(backgroundWorker1.RunWorkerAsync);
test.Invoke(backgroundWorker1);
}
//Backgroundworker 1
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
Show();
}
//Backgroundworker 2
private void backgroundWorker2_DoWork(object sender, DoWorkEventArgs e)
{
Willkommensgruss(label1);
}
}
}
2 Antworten
Meine Vermutung ist, dass label 1 ja bereits Form1 Thread läuft und daher nicht mehr geändert werden kann über einen Backgroundworker.
Genau. Wenn du Elemente, die von unterschiedlichen Threads geteilt werden, nutzen möchtest, muss dies synchronisiert erfolgen. Teile also strikt auf in View und Model. Das Model wird durch ein String-Property repräsentiert, welches geblockt werden muss, bevor eine Veränderung am Wert vorgenommen werden darf. An dieses Property wird das Label gebunden.
Lies dazu hier weiter:
- https://docs.microsoft.com/de-de/dotnet/csharp/programming-guide/concepts/threading/thread-synchronization
- http://www.jaylee.org/post/2010/01/02/WinForms-DataBinding-and-Updates-from-multiple-Threads.aspx
System.ObjectDisposedException: "Auf das verworfene Objekt kann nicht zugegriffen werden.Objektname: "Skynet"."
Du gibst das Objekt ja auch nach Nutzung explizit zur Löschung frei.
hello.Dispose();
huhu, wie du gesagt hattest, hab ich versucht das label1 zu blocken. Leider mit mäßigen Erfolg. Eigentlich gar keiner. Er meckert nach wie vor an der Methode
private void Willkommensgruss() {
lock (this.label1) {
Thread.Sleep(2000);
label1.Visible = true;
}
}
Was mach ich nur falsch?
Nein, ich habe nicht geschrieben, dass du das Label blockieren sollst.
Ich werde mir das Ganze später noch einmal genauer anschauen und etwas dazu schreiben.
So, tut mir leid, dass ich nun so lange für eine Antwort benötigt habe, ich hatte die letzten Tage kaum Zeit und schreibe deswegen eine neue Antwort auf deine letzte Frage.
Zuerst einmal habe ich mir ein neues Windows Form erstellt und darauf ein Label (label1) sowie 2 Buttons (button1, button2) gelegt. Mein Code sieht folgendermaßen aus:
public partial class MyForm : Form, INotifyPropertyChanged
{
public delegate void Worker(BackgroundWorker backgroundWorker);
private BackgroundWorker _worker;
private string _labelText;
public MyForm()
{
_worker = new BackgroundWorker();
InitializeComponent();
PropertyChanged += HandleLabelTextChange;
button1.Click += delegate
{
Thread thread = new Thread(() => LabelText = "Hallo")
{
IsBackground = true
};
thread.Start();
};
_worker.DoWork += (sender, e) =>
{
LabelText = "Welcome";
Thread.Sleep(2000);
};
}
public string LabelText
{
get => _labelText;
set
{
_labelText = value;
OnNotifyChanged("LabelText");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnNotifyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// bound to button2
private void HandleButtonClick(object sender, EventArgs e)
{
var workerDelegate = new Worker(_worker.RunWorkerAsync);
workerDelegate.Invoke(_worker);
}
private void HandleLabelTextChange(object sender, EventArgs e)
{
Invoke((MethodInvoker)delegate
{
label1.Text = LabelText;
});
}
}
Der Kürze halber habe ich Model und View in eine Klasse zusammengelegt. Die Aufgabe des Models besteht darin, den Text für das Label zu speichern und zu verwalten. Damit auf Änderungen an dem Text sofort reagiert werden kann, wird das INotifyPropertyChanged-Interface implementiert, welches ein Event definiert. Mit der Methode OnNotifyPropertyChanged wird das Event getriggert, als Information in das Eventobjekt der Name des Property gespeichert, welches sich geändert hat.
Für einen Klick auf den ersten Button lasse ich einen neuen Thread starten, welcher im Hintergrund läuft und das Property in seinem Wert verändert. Der zweite Button macht es so wie bei dir über eine Instanz der BackgroundWorker-Klasse.
Die eigentliche Magie geschieht nun in dem Handler für das PropertyChanged-Event. Er wird aufgerufen, nachdem die Änderung des Models getriggert wurde. Die Invoke-Methode führt einen threadsafe Request an die UI aus, dass das Label geändert werden soll.
Dieses Beispiel - und auch eines, welches nur den BackgroundWorker nutzt, wird in der Dokumentation mit einem guten Beispiel erklärt.
https://msdn.microsoft.com/de-de/library/ms171728(v=vs.110).aspx
Das Ändern des Labels muss auf dem Thread geschehen, der das Element erzeugt hat, der UI-Thread darf nicht blockiert werden.
Hab das Dispose gelöscht. Der Fehler ist aber noch immer vorhanden :)