Laden...

BackgroundWorker, MMTimer & GUI

Erstellt von Maddinel vor 11 Jahren Letzter Beitrag vor 11 Jahren 4.693 Views
Maddinel Themenstarter:in
1.371 Beiträge seit 2004
vor 11 Jahren
BackgroundWorker, MMTimer & GUI

Hi,

ich habe folgende Situation:

In meiner Anwendung lasse ich einen MultimediaTimer laufen um sehr zeitgenau und regelmäßig eine Aktion durchführen zu können (der Standard-Timer war hierfür nicht genau genug). Um die Timerausführung auch bei weiteren Aktionen in der GUI auf keinen Fall zu stören, lagere ich die Erzeugung und das Tick-Event über einen Backgroundworker in einen zweiten Thread aus. Das funktioniert soweit auch so wie gedacht.

Und jetzt zum Problem bei der Sache:

Ich muss die Ausführung des TickEvents in der GUI anzeigen. Gemäß [FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke) habe ich mir also gedacht, dass das ProgressChanged-Event des BackgroundWorkers genau das ist was ich suche. Leider klappt das nicht. Da ich im Worker die Klasse nur erzeuge, in der dann der MMTimer gestartet wird, bekomme ich beim Aufruf von ProgressChanged im Tick-Event dann die Meldung "Für diesen Vorgang wurde OperationCompleted bereits aufgerufen. Weitere Aufrufe sind unzulässig."

Wie komme ich aus dieser Misere?

==============================
Wenn ichs wüsst', würd' ich nicht fragen!!! 😁
==============================

D
216 Beiträge seit 2009
vor 11 Jahren

Wie komme ich aus dieser Misere?

Indem du den Backgroundworker weglässt oder im BackgroundWorker DoWork event so lange was machst, bis du den Timer nicht mehr brauchst. Aber der BackgroundWorker ist dafür eigentlich nicht gedacht.

Darth Maim

Maddinel Themenstarter:in
1.371 Beiträge seit 2004
vor 11 Jahren

Du meinst ich sollte lieber direkt mit "Thread" arbeiten?

==============================
Wenn ichs wüsst', würd' ich nicht fragen!!! 😁
==============================

U
1.688 Beiträge seit 2007
vor 11 Jahren

Das funktioniert soweit auch so wie gedacht.

Bisschen Code zum Verständnis wäre nicht schlecht. Was erzeugst Du wo und wie? Was hat der BackgroundWorker mit dem Multimedia-Timer zu tun?

Maddinel Themenstarter:in
1.371 Beiträge seit 2004
vor 11 Jahren

Bisschen Code zum Verständnis wäre nicht schlecht.

Ok, hier mal zum Verständnis:


class Main
{
  BackgroundWorker worker = new BackgroundWorker();
  Working_Hard wh_ref;

  worker_DoWork()
  {
    Working_Hard wh = new Working_Hard()
    wh_ref = wh;
    wh.starte_timer();
  }

  button1_click()
  {
    worker.RunWorkerAsync();
  }

  button2_click()
  {
    wh_ref.verändere_parameter(parameter);
  }
}

class Working_Hard
{
  MMTimer timer = new MMTimer();

  starte_timer()
  {
    timer.Start();
  }

  timer_Tick()
  {
    //mache was zeitkritisches ...
  }

  verändere_parameter(parameter)
  {
    //Parameter ändern, die im Tick-Event verarbeitet werden ...
  }
}

Das ist es in groben Zügen. Im Hauptprogramm starte ich den Vorgang per button1, der dann im Hintergrund den Timer startet. Der wiederum wird im Hintergrund regelmäßig durchlaufen über das Tick-Event im zweiten Thread. Ich habe auch die Möglichkeit über meine Referenzklasse wh_ref dem Vorgang auch im Nachhinein noch andere Parameter, die z.B. durch den Benutzer eingegeben wurden, zu verändern.

==============================
Wenn ichs wüsst', würd' ich nicht fragen!!! 😁
==============================

3.430 Beiträge seit 2007
vor 11 Jahren

Hallo,

das Problem ist dass deine DoWork Methode lange schon fertig ist bis du das Timer-Tick kriegst und damit dann den Progress festlegen willst
D.h. der BackgroundWorker hat seine Arbeit schon erledigt (DoWork ist fertig) und du willst später erst sagen. So jetzt habe ich 10% gemacht, was nicht sein kann da er schon fertig hat.

Irgendwie ist der gesamte Code irgendwie sehr schwierig überschaubar. Sind zwar nur ein paar Zeilen aber wirklich strukturiert wirkt das nicht.
Statt den BackgroundWorker kannst du einfach einen Thread verwenden. In deiner Working_Hard Klasse kannst du ein Event einbauen welches du schmeisst wenn sich der Fortschritt ändert. Dieses Event abonnierst du im MainWindow und gibst dann den Wert auf der GUI aus.
Du darfst dabei aber nicht das Invoken vergessen da du dich in einen anderen Thread befindest.

Grüße
Michael

Maddinel Themenstarter:in
1.371 Beiträge seit 2004
vor 11 Jahren

das Problem ist dass deine DoWork Methode lange schon fertig ist bis du das Timer-Tick kriegst und damit dann den Progress festlegen willst

Ja genau. Der BW dient hier quasi nur zum Einstieg in den zweiten Thread. Die Thread-Variante macht wohl wirklich deutlich mehr Sinn. Ich werde mir das mal entsprechend umbauen und mit eigenem Event umsetzen.
Danke!

==============================
Wenn ichs wüsst', würd' ich nicht fragen!!! 😁
==============================

D
216 Beiträge seit 2009
vor 11 Jahren

Das Problem ist, dass nachdem du den Timer gestartet hast (also nach wh.starte_timer()), die DoWork Methode vorbei ist und damit der BackgroundWorker nicht mehr läuft. Dann kannst du natürlich nicht mehr die ProgressChanged Methode aufrufen.

Aber da meiner Meinung nach das tick event sowieso nicht im GUI, bzw. dem Thread, der den Timer gestartet hast, brauchst du das doch alles garnicht mit dem BackgroundWorker.

Also im Grunde sowas:

class Program {
	private WorkingHard _wh;
	
	private void startWork() {
		_wh = new WorkingHard();
		_wh.Start();
		_wh.StatusUpdate += (o,e) => this.Invoke(new Action<object, WorkingHardEventArgs>(wh_StatusUpdate), o, e);
	}
	
	private void updateParameters(Foo bar) {
		_wh.updateParameter(bar);
	}
	
	private void wh_StatusUpdate(object sender, WorkingHardEventArgs e) {
		this.progressBar.Value = e.Percentage;
	}
}

class WorkingHard {
	MMTimer _timer = new MMTimer();
	
	public void Start() {
		_timer.Start();
	}
	
	private void Tick() {
		//Do something

		OnStatusUpdate(42);
	}
	
	private void OnStatusUpdate(int percentage) {
		if(StatusUpdate != null) StatusUpdate(percentage);
	}
	
	public event EventHandler<WorkingHardEventArgs> StatusUpdate;
}

class WorkingHardEventArgs : EventArgs {
	public WorkingHardEventArgs(int percentage) { 
		_percentage = percentage;
	}
	
	private int _percentage = 0;
	public int Percentage { get { return _percentage; } }
}

Falls das Tick-Event des MMTimers doch im gleichen Thread ausgeführt wird, in dem er erstellt wurde, mach die Start-Methode einfach so:

ThreadPool.QueueUserWorkItem(o => _timer.Start());

Darth Maim

U
1.688 Beiträge seit 2007
vor 11 Jahren

Aber da meiner Meinung nach das tick event sowieso nicht im GUI, bzw. dem Thread, der den Timer gestartet hast, brauchst du das doch alles garnicht mit dem BackgroundWorker.

Das ist richtig. Es gibt ggf. ein kleines Aber. Wenn es sich bei der MMTimer-Klasse um die Klasse von Codeproject handelt und ich mich recht erinnere, so hatte diese ein SynchronizationObject (o. ä.). Das darf man natürlich nicht benutzen (s. Quelltext). Vielleicht willst Du es aber auch gerade verwenden, um direkt auf die GUI zugreifen zu könnnen?

Interessant wäre dann noch, wie lange Deine "zeitkritische" Operation gegenüber dem Intervall des Timers dauert?

Maddinel Themenstarter:in
1.371 Beiträge seit 2004
vor 11 Jahren

@michlG:
Perfekt! So klappts. Ich habs jetzt per Thread umgesetzt und den Backgroundworker komplett rausgeschmissen. Das Problem der Aktualisierung löse ich über ein in die Worker-Klasse eingebautes Event, das ich im Hauptprogramm abonniere. Im Eventhandler Invoke ich die zu verwendeten Elemente des Main-Threads und schwups ... es klappt so wie ich das wollte! Danke noch mal!

@Darth Maim:
Stimmt, ich hatte es zu Anfang auch genau so (ohne zweiten Thread) genau so umgesetzt, da ich auch so dachte. Das Problem ist aber hierbei, dass das Tick-Event selbst zar in einem zweiten Thread läuft, aber der Aufruf des Tick-Events wird trotzdem ausgebremst, wenn das Hauptprogramm beschäftigt ist.

==============================
Wenn ichs wüsst', würd' ich nicht fragen!!! 😁
==============================