Hallo,
ich habe für ein Gerät einen Bootloader geschrieben. Zu diesem entwickle ich gerade Anwendung. Da einzelne Vorgänge länger dauern, habe ich diese in asynchrone Methoden ausgelagert und per IProgress update ich die UI, damit der Benutzer sieht, dass noch etwas passiert.
Ich habe in meinem ViewModel einen
private readonly Progress<CommunicationResult> _ProgressIndicator;
Im Konstruktor:
_ProgressIndicator = new Progress<CommunicationResult>(param => ReportProgress(param));
sowie eine Methode
private void ReportProgress(CommunicationResult progress)
{
if (!progress.Success)
Success = progress.Success;
ProgressState = progress.Progress;
ProgressItem item = new ProgressItem(progress.Message, progress.IsImportant);
UpdateStringList.Add(item);
}
ProgressState und UpdateStringList sind gebunden an die View (ProgressBar und ItemsControl). Wenn ich ProgressState ändere ist alles prima und der ProgressBar ändert seinen Wert. Bei UpdateStringList.Add... bekomme ich die Exception. Wenn ich alles in ein Invoke packe geht's. Ich dachte der Progress übernimmt den Kontext von dort, wo erstellt wird. Und warum kommt die Exception nicht schon beim Ändern von ProgressState, der ja auch an die UI gebunden ist?
Grüße, Alex
Final no hay nada más
Hallo,
was ist denn UpdateStringList? Möglicherweise ist die Methode ja einfach nicht implementiert und wirft immer die Exception?
Wissen ist nicht alles. Man muss es auch anwenden können.
PS Fritz!Box API - TR-064 Schnittstelle | PS EventLogManager |
Hallo inflames2k,
sorry, vergessen zu erwähnen:
private readonly ObservableCollection<ProgressItem> _UpdateStringList;
public ObservableCollection<ProgressItem> UpdateStringList
{
get { return _UpdateStringList; }
}
Grüße, Alex
Final no hay nada más
Wie sieht denn die genaue Exception aus? Am besten mit InnerException.
Wissen ist nicht alles. Man muss es auch anwenden können.
PS Fritz!Box API - TR-064 Schnittstelle | PS EventLogManager |
Message ist:
Fehlermeldung:
Von diesem CollectionView-Typ werden keine Änderungen der "SourceCollection" unterstützt, wenn diese nicht von einem Dispatcher-Thread aus erfolgen.
Okay, das erklärt zumindest, warum der ProgressBar funktioniert. Allerdings habe ich das alles schon mal genau so in einem anderen Projekt implementiert und da hatte ich das Problem nicht.
Grüße, Alex
Final no hay nada más
Ich hab nochmal gegoogled und das hier gefunden:
The Progress<T> constructor captures the current SynchronizationContext object.
The SynchronizationContext class is a facility that abstracts the particulars of the involved threading model. That is, in Windows Forms it will use Control.Invoke, in WPF it will use Dispatcher.Invoke, etc.
When the progress.Report object is called, the Progress object itself knows that it should run its delegate using the captured SynchronizationContext.
In other terms, it works because Progress has been designed to handle that without the developer having to explicitly say it.
Was ja genau heißt, dass ein Invoke nicht erforderlich ist, oder verstehe ich das falsch?
Grüße, Alex
Final no hay nada más
Jetzt müsste man ja nur wissen, welcher SyncContext beim Erzeugen gefunden wird.
Wie findet man das heraus? Der ViewModel-Konstruktor wird im UI-Thread aufgerufen. Ich muss gestehen, dass mir der Begriff SynchronisationsContext neu ist.
Final no hay nada más
SynchronizationContext.Current
Das Ding ist im Prinzip die zentrale Einheit, die dafür verantwortlich ist, dass ein Task wieder zum UI-Thread "zurück kehren" kann.
Viel spannender finde ich eigentlich die Frage, warum Du in einem anderen Thread bist, wenn Du dem Nutzer den aktuellen Zustand mitteilen willst bzw. wie der Code drum herum aussieht.
Wenn der aktuelle SynchronizationContext null ist, dann befindest Du dich sehr wahrscheinlich nicht im UI-Thread und bekommst daher diese Exception.
NuGet Packages im Code auslesen
lock Alternative für async/await
Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.
Ich poste mal den grundsätzlichen Code. Der oben stehende Code befindet sich im ViewModel.
Dann habe ich noch eine Klasse Bootloader, die hat einige asynchrone Methoden zur Gerätekommunikation.
Auf Knopdruck gehts los (Command)
private async void UpdateDevice(object param)
{
UpdateStringList.Clear();
Success = true;
IntelHexFile file;
if (IntelHexFile.TryParse(HexFileName, out file))
await _Bootloader.UpdateDevice(Port, file, EraseEEPROM, _ProgressIndicator);
}
Öffentliche Methode der Bootloaderklasse
public async Task<bool> UpdateDevice(string portName, IntelHexFile file, bool eraseEEPROM, IProgress<CommunicationResult> progressIndicator)
{
IsBusy = true;
if (progressIndicator == null) return false;
BlockList blockList = CreateBlockList(file);
BlockList.State state = BlockList.Validate(blockList);
bool success = state == BlockList.State.Valid;
//schnipp
//schnapp
success = await SearchDevice();
if (!success)
{
progressIndicator.Report(new CommunicationResult(true, 100, "Bitte Gerät einschalten", true));
success = await WaitForDevice(5);
if (!success)
progressIndicator.Report(new CommunicationResult(false, 100, "kein bootbares Gerät an Port: " + portName + " gefunden", false));
else
progressIndicator.Report(new CommunicationResult(true, 100, "Gerät an Port: " + portName + " gefunden", false));
}
else
progressIndicator.Report(new CommunicationResult(true, 100, "Gerät an Port: " + portName + " gefunden", false));
}
if (success)
{
progressIndicator.Report(new CommunicationResult(true, 0, "Signatur lesen... ", false));
success = await CheckSignature(new byte[] { 0x4C, 0x95, 0x1E });
if (!success)
progressIndicator.Report(new CommunicationResult(false, 100, "nicht OK", true));
else
{
//schnipp
So sieht das grob aus. es gibt eine öffentliche Methode, die aufgerufen wird und nicht öffentliche Methoden in dieser, die dann die einzelnen Schritte ausführen. Dazwischen wird das Ergebnis eines Teilschrittes zurückgemeldet.
Hier mal noch eine der Methoden
private async Task<bool> EraseDevice(bool eraseEEPROM)
{
return await Task.Run(() =>
{
try
{
_Port.Write("e");
if (eraseEEPROM)
_Port.Write("1");
else
_Port.Write("0");
Task.Delay(1000).Wait(); //give it a little extra time to erase the memory
_Port.ReadLine(); //newline character expected, 1s time out
return true;
}
catch (Exception) { return false; }
});
}
Grüße, Alex
Final no hay nada más
Update:
ausgehend von Sir Rufos Post habe ich noch etwas rumprobiert und herausgefunden, dass im ViewModel-Konstruktor tatsächlich
SynchronizationContext.Current == null;
Ausgehend von diesem Post
Of course there is no SynchronizationContext available when the entry point (Main) of your application is hit. You need to wait for the framework to initialize it.
In a Windows Forms application this happens when the first form is created. So only after the Application.Run method has been called, the SynchronizationContext.Current property will actually return a synchronization context.
habe ich festgestellt, dass der ViewModel-Konstruktor vor der App.OnStartup ausgeführt wird. In meinem anderen Projekt, wo es funktionierte, ist das nicht so. Als erste Lösung habe ich
_ProgressIndicator = new Progress<CommunicationResult>(param => ReportProgress(param));
in die Kommando-Methode verlegt. Und siehe da, es geht.
Danke und Grüße, Alex
Final no hay nada más
Ach sieh an, Progress speichert den SynchronizationContext intern, um die Aufrufe auch aus einem anderen Thread heraus zu synchronisieren.
Kannst Du dir hier anschauen:
Progress<T> Source Code
Und hier noch mal offiziell:
Progress<T> Class
Das erklärt es natürlich. Wenn das Progress-Objekt im Konstruktor gebaut wird, nimmt der sich den SyncContext von dort - der ja noch nicht existiert - und synchronisiert dagegen.
Faszinierend, gut zu wissen 😄
PS:
Schau mal hier rein:
Await, SynchronizationContext, and Console Apps
Hat mir sehr geholfen, das ganze System hinter Tasks, await und dem SyncContext zu verstehen.
NuGet Packages im Code auslesen
lock Alternative für async/await
Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.