Laden...

Threading-Problem bei MVVM-Struktur mit asynchronem Service

Erstellt von blackdynamic vor 12 Jahren Letzter Beitrag vor 12 Jahren 4.085 Views
Thema geschlossen
B
blackdynamic Themenstarter:in
51 Beiträge seit 2010
vor 12 Jahren
Threading-Problem bei MVVM-Struktur mit asynchronem Service

Hallo liebe Community,

ich habe zwei Probleme bei folgendem Szenario:


public class PersonViewModel()
{
	public ObservableCollection<Person> People;
	private IPersonManager _personManager;

	public PersonViewModel(IPersonManager personManager)
	{
		_personManager = personManager;
		People = new ObservableCollection<Person>();
		_personManager.People.CollectionChanged += ((object sender, NotifyCollectionChangedEventArgs e) => this.People = _personManager.People);
	}
}

public class PersonManager
{
	public ObservableCollection<Person> People;
	private IAsyncPersonService _asyncPersonService;
	private IPersonChangeInitiator _initiator;
	
	public PersonManager(IAsyncPersonService asyncPersonService, IPersonChangeInitiator initiator)
	{
		People = new ObservableCollection<Person>();
		_asyncPersonService = asyncPersonService;
		_initiator = initiator;
		_initiator.OnAddNewPerson += onAddNewPerson;
	}
	
	public void onAddNewPerson()
	{
		_asyncPersonService.InvokeGetAnyPerson(new GetAnyPersonRequest(), invokeGetAnyPersonCallback);
	}
	
	private void invokeGetAnyPersonCallback(GetAnyPersonResponse response)
	{
		if(response != null)
		{
			Person person;
			
			if(response.IsNewPerson)
			{
				person = response.Person;
				People.Add(person);
			}
			else
				// Für dieses Beispiel setze ich einfach mal voraus, dass der Nachname eindeutig ist
				person = People.GetPersonByName(response.Person.Name);
				
			doAnythingWithPerson(person);
		}
	}
}

Der AsyncPersonService startet die aufgerufene Methode über einen Backgroundworker asynchron.

Wenn der PersonChangeInitiator das erste mal das OnAddNewPerson-Event schmeisst, und es sich um eine neue Person handelt, wird diese ohne Probleme der Liste im PersonManager hinzugefügt
und das CollectionChanged-Event kann ohne Probleme im View-Model gefangen werden. Das bedeutet, auch die Liste im ViewModel hat nun die neue Person.

Problem 1:
Schmeisst der PersonChangeInitiator jedoch ein zweites mal dieses Event, natürlich dieses mal mit anderen Werten um noch eine andere Person anzulegen, bekomme ich bei "People.Add(person);" folgende Exception:> Fehlermeldung:

Von diesem CollectionView-Typ werden keine Änderungen der "SourceCollection" unterstützt, wenn diese nicht von einem Dispatcher-Thread aus erfolgen

Problem 2:
Wird nach dem Ersten OnAddNewPerson-Event vom PersonChangeInitiator erneut das OnAddNewPerson-Event geschmissen, und in der GetAnyPersonResponse befindet sich keine neue Person sondern die gerade zuvor angelegte,
kann ich mir das zuvor angelegte Personenobjekt über den Namen aus der People-Liste holen("People.GetPersonByName") und in der Methode "doAnythingWithPerson(person)" zwar verändern,
dass ViewModel wird darüber allerdings nicht mehr über das CollectionChanged-Event der People-Liste benachrichtigt.

Ich vermute mal, dass diese beiden Probleme miteinander zusammenhängen, richtig?

Ich hatte schon vor etwas längerer Zeit mal Probleme mit Threading, Dispatcher usw. Hatte damals aber auch keine MVVM-Anwendung und habe es einfach im Codebehind über so einen Dispatcher-Workaround gedreht.
Da ich mich damals aber noch nicht so gut mit Programmierung auskannte, war der Code sehr unübersichtlich und schlecht geschrieben, auch wenn er funktionierte.

Deshalb hatte ich mich zum Entwurf einer komplett neuen Struktur entschlossen, die nun wie eben gesagt auf dem MVVM Pattern basiert und mit asynchronen Methoden arbeitet.
Ich dachte eigentlich, durch das Verwenden des Callbacks bei asynchronen Methoden könnte ich sicher stellen, dass ich immer wieder im gleichen Thread lande, nachdem die asynchrone Methode durchgelaufen ist.
Von daher hatte ich mir davon eigentlich erhofft, die Dispatcher-Problematik umgangen zu haben.

Könnt ihr mir eventuell weiter helfen, was ich verändern / anpassen muss um die Probleme zu beheben?
Und wo liegt der Fehler in meiner Denkweise, bezogen auf das Threading-Verhalten der MVVM Struktur mit asynchronen Methoden?

Viele Grüße
blackdynamic

F
10.010 Beiträge seit 2004
vor 12 Jahren

Deshalb hatte ich mich zum Entwurf einer komplett neuen Struktur entschlossen, die nun wie eben gesagt auf dem MVVM Pattern basiert und mit asynchronen Methoden arbeitet.
Ich dachte eigentlich, durch das Verwenden des Callbacks bei asynchronen Methoden könnte ich sicher stellen, dass ich immer wieder im gleichen Thread lande, nachdem die asynchrone Methode durchgelaufen ist.
Von daher hatte ich mir davon eigentlich erhofft, die Dispatcher-Problematik umgangen zu haben

Da hast Du aber sowohl das mit dem Asynchronen als auch das mit dem Invoke/Displatch falsch verstanden.
Wenn du Irgendwas asynchron machst findet das in einem anderen Thread statt, und wenn du von da aus ein event feuerst, ist das Ergebnis ja bekannt.
[FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke)

B
blackdynamic Themenstarter:in
51 Beiträge seit 2010
vor 12 Jahren

Ich bin mittlerweile auf die Erklärung von herbivore bezüglich meines Problems gestoßen:
[FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke)

Allerdings befindet sich dort folgende Aussage:

BackgroundWorker

Statt mit extra Threads und Control.Invoke, kann man auch mit der BackgroundWorker-Klasse ungültige threadübergreifende Vorgänge vermeiden. Dazu müssen bei BackgroundWorker alle Zugriffe auf das GUI einfach aus den ProgressChanged- oder RunWorkerCompleted-EventHandlern durchgeführt werden. Wenn man sich daran hält, ist explizites Control.Invoke bei BackgroundWorker nicht erforderlich, denn die genannten EventHandler werden hinter den Kulissen automatisch per Control.BeginInvoke aufgerufe

Und das ist es ja, was ich auch erwartet habe ... vielleicht zum besseren Verständniss ein Ausschnitt aus meinem BackgroundWorker:


public abstract class MyAsyncServiceBase
{
	(...)
	protected void AnyGenericInvoke<TAsyncRequest, TAsyncResponse>(TAsyncRequest asyncRequest, Func<TAsyncRequest, TAsyncResponse> method, Action<TAsyncResponse> asyncCallback)
	{
		BackgroundWorker worker = new BackgroundWorker();

		worker.DoWork += (sender, args) =>
		{
			args.Result = method(asyncRequest);
		};

		worker.RunWorkerCompleted += (sender, args) =>
		{
			if (asyncCallback != null)
				asyncCallback(args.Result as TAsyncResponse);
		};

		using (worker) 
		{ 
			worker.RunWorkerAsync(); 
		}
	}
}

Ich hoffe es gibt hier jemanden, der mich "erleuchten" kann ..

*edit Ja, den habe ich auch gerade gefunden, den Thread. Aber wie ich ja oben zitiert habe sagt auch dieser aus, dass es mit dem Backgroundworker so funktionieren sollte..
In schmeisse in keiner asynchronen Methode ein Event.

49.485 Beiträge seit 2005
vor 12 Jahren

Hallo blackdynamic,

die Aussage aus der FAQ ist etwas vereinfacht. Das ganze gilt, wenn der BackGroundWorker in einer Windows Forms Anwendung im GUI-Thread korrekt erstellt wird. Genaugenomen verwendet der BGW nicht Control.Invoke/BeginInvoke, sondern SynchronizationContext.Send/Post. Das entspricht nur dann einem Control.Invoke/BeginInvoke, wenn ein für Windows Forms passender SynchronizationContext verwendet wird. Wenn man unter Verwendung von VS den BGW innerhalb des GUI-/Main-Threads erstellt, sollte das aber automatisch der Fall sein. Wenn es bei dir nicht klappt, dann ist wohl eine der genannten Voraussetzungen nicht gegeben.

Das gilt für WPF analog.

herbivore

PS: Ich habe in der FAQ den folgenden Absatz hinzugefügt:

Alles vorausgesetzt, der BackgroundWorker wurde korrekt initialisiert, so dass er insbesondere einen passenden SynchronizationContext verwendet. Wenn der BGW unter Verwendung von VS im GUI-/Main-Thread erzeugt wurde, sollte das automatisch der Fall sein.

B
blackdynamic Themenstarter:in
51 Beiträge seit 2010
vor 12 Jahren

Ich verwende WPF, aber die konkreten asynchronen Services, welche dann von der oben geposteten Klasse abgeleitet sind, werden dem PersonenManager über einen IOC Container injeziert.
Dieser Container registriert die asynchronen Services innerhalb der App.xaml.cs also direkt im GUI-/Main-Thread.

Ich hoffe das hilft euch weiter ..

49.485 Beiträge seit 2005
vor 12 Jahren

Hallo blackdynamic,

dass das von mir Gesagte analog für WPF gilt, hatte ich oben schon ergänzt. Ich hoffe, das hilft dir, denn mit dem bisher gesagten und dem in der FAQ beschriebenen Grundprinzip solltest du es jetzt alleine hinbekommen.

herbivore

Thema geschlossen