Laden...

[gelöst] Async Task, IProgress, UI-Update, NotSupportedException

Erstellt von ill_son vor 6 Jahren Letzter Beitrag vor 6 Jahren 3.046 Views
I
ill_son Themenstarter:in
227 Beiträge seit 2009
vor 6 Jahren
[gelöst] Async Task, IProgress, UI-Update, NotSupportedException

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

2.298 Beiträge seit 2010
vor 6 Jahren

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 |

I
ill_son Themenstarter:in
227 Beiträge seit 2009
vor 6 Jahren

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

2.298 Beiträge seit 2010
vor 6 Jahren

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 |

I
ill_son Themenstarter:in
227 Beiträge seit 2009
vor 6 Jahren

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

I
ill_son Themenstarter:in
227 Beiträge seit 2009
vor 6 Jahren

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

D
985 Beiträge seit 2014
vor 6 Jahren

Jetzt müsste man ja nur wissen, welcher SyncContext beim Erzeugen gefunden wird.

I
ill_son Themenstarter:in
227 Beiträge seit 2009
vor 6 Jahren

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

2.080 Beiträge seit 2012
vor 6 Jahren

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.

I
ill_son Themenstarter:in
227 Beiträge seit 2009
vor 6 Jahren

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

I
ill_son Themenstarter:in
227 Beiträge seit 2009
vor 6 Jahren

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

2.080 Beiträge seit 2012
vor 6 Jahren

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.