3

[EDIT]Solved, see below[/EDIT]

this is a newbie-question.

I'm just digging in to c# and async and whyt i would like to have:

  • click Button
  • run several tasks in order but in background-thread, one after another
  • running tasks should notifiy their progress if possible

right now i can click the botton and start the task-chain, but within the completition event i would like (for testing) show a message-box every time a task has finished. this may lead to a crash (?) and i don't know why since i thought i would be within the ui-thread ...

here are some parts of the code:

AppViewModel:

    void handlePhaseCompletedEvent(object sender, SyncPhaseCompletedEventArgs e)
    {
        Shell.Current.DisplayAlert("TEST", "PHASE " + e.phase.ToString(), "OK"); // <<<< doesn't show up, maybe because its crashing a short time after?
        syncToolService.StartSyncPhaseAsync(e.phase + 1, this); // <<<< seems to crash here?
    }

    [RelayCommand]
    async Task StartSyncAsync()
    {
        syncToolService.NotifySyncPhaseCompleted += handlePhaseCompletedEvent;
        syncToolService.StartSyncPhaseAsync(0, this);
    }   

syncToolService:

public event EventHandler<SyncPhaseCompletedEventArgs> NotifySyncPhaseCompleted;

    public async Task StartSyncPhaseAsync(int phase, AppViewModel viewModel)
    {
        // search for Remote-peer
        if (phase == 0)
        {
            Task t = new Task(() => Task.Delay(100)); // dummy, not implemented yet
            t.ConfigureAwait(false);
            t.ContinueWith(t => NotifySyncPhaseCompleted?.Invoke(this, new SyncPhaseCompletedEventArgs { phase = phase }));
            t.Start();
            return;
        }

        // Remote Sync start preparations
        if (phase == 1)
        {
            Task t = new Task(() => Task.Delay(100)); // dummy, not implemented yet
            t.ConfigureAwait(false);
            t.ContinueWith(t => NotifySyncPhaseCompleted?.Invoke(this, new SyncPhaseCompletedEventArgs { phase = phase }));
            t.Start();
            return;
        }

        //////// LOCAL PREPARATIONS

        // read local files
        if (phase == 2)
        {
            Task t = new Task(() => BPMSyncToolService.loadLocalData(viewModel.DataFiles));
            t.ConfigureAwait(false);
            t.ContinueWith(t => NotifySyncPhaseCompleted?.Invoke(this, new SyncPhaseCompletedEventArgs { phase = phase }));
            t.Start();
            return;
        }
    }

basicly i thought StartSyncPhaseAsync would run a Task (and it seems to do so) and it also seems to trigger the event (whicht seems not to raise the exeption) when running line by line in debug it crashes after syncToolService.StartSyncPhaseAsync(e.phase + 1, this); with this stack:

>   [Exception] WinRT.Runtime.dll!WinRT.ExceptionHelpers.ThrowExceptionForHR.__Throw|20_0(int hr)   
    [Exception] Microsoft.WinUI.dll!Microsoft.UI.Xaml.Controls.ContentDialog._IContentDialogFactory.CreateInstance(object baseInterface, out System.IntPtr innerInterface)  
    [Exception] Microsoft.WinUI.dll!Microsoft.UI.Xaml.Controls.ContentDialog.ContentDialog()    
    [Exception] Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.Platform.AlertManager.AlertRequestHelper.OnAlertRequested(Microsoft.Maui.Controls.Page sender, Microsoft.Maui.Controls.Internals.AlertArguments arguments)  
    System.Private.CoreLib.dll!System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()   
    System.Private.CoreLib.dll!System.Threading.Tasks.Task.ThrowAsync.AnonymousMethod__128_1(object state)  
    System.Private.CoreLib.dll!System.Threading.QueueUserWorkItemCallbackDefaultContext.Execute()   
    System.Private.CoreLib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch()  
    System.Private.CoreLib.dll!System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart() 

i also may have a general problem in my design, any help would be great!

[UPDATE] it runs now as expected. newbie-thoughts:

so this is basicly the actual working code that seems NOT to lock the UI and doesn't crash (the crash was because of Microsoft.VisualBasic.FileIO.TextFieldParser that tried to read a line and found a field beginning with a quote and thought it would be an enclosing quote which it wasn't)

AppViewModel:

    private void HandleSyncProgressChanged(object sender, SyncStatus e)
    {
        NumFilesProcessed = e.filesProcessed;
        NumFilesNotFound = e.filesNotFound;
        AktueleAufgabe = e.workingPhase;
    }
    [RelayCommand]
    async Task StartSyncAsync()
    {
        Progress<SyncStatus> progress=new Progress<SyncStatus>();
        progress.ProgressChanged += HandleSyncProgressChanged;
        await BPMSyncToolService.StartSyncPhaseAsync(this, progress);
    }   

syncToolService:

   public static async Task StartSyncPhaseAsync(AppViewModel viewModel, IProgress<SyncStatus> progress)
    {
        SyncStatus report = new SyncStatus();
        report.workingPhase = "Suche Synchronisationspartner";
        progress.Report(report);
        // search for Remote-peer
        await Task.Delay(100); // dummy, not implemented yet

        report.workingPhase = "Starte Vorbereitungen beim Synchronisationspartner";
        progress.Report(report);
        // Remote Sync start preparations
        await  Task.Delay(100); // dummy, not implemented yet

        //////// LOCAL PREPARATIONS

        report.workingPhase = "lese lokale Dateien";
        progress.Report(report);
        // read local files
        await BPMSyncToolService.LoadLocalDataAsync(viewModel.DataFiles, progress, report);
//     [...]
   }

what i actually can't see is the counting up of processed files, maybe it's too fast, don't know, will see in further tasks that will require more time

anyways, thanks, both answers helped, i will mark the one as solution, that was closer to the core problem (i think)

mech
  • 617
  • 6
  • 16
  • 2
    As a side-note, if you decide to use the `ContinueWith` method you should be aware of the rule CA2008: [Do not create tasks without passing a TaskScheduler](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2008). – Theodor Zoulias Aug 18 '22 at 13:09

2 Answers2

6

Given async/await, it is almost never necessary to use task continuations or ConfigureAwait.

  • To start a sequence in the background, wrap the sequence in Task.Run.
  • To report progress on UI thread, use Dispatcher.Dispatch.

Example:

// IMPORTANT: `await`.
// Otherwise, current method would continue before Task.Run completes.
await Task.Run(async () =>
{
    // Now on background thread.
    ...

    // Report progress to UI.
    Dispatcher.Dispatch(() =>
    {
        // Code here is queued to run on MainThread.
        // Assuming you don't need to wait for the result,
        // don't need await/async here.
    }

    // Still on background thread.
    ...
};

// This is effectively the "continuation": Code here runs after Task.Run completes.
...

UPDATE

In response to comment, this is how you use async/await to start a sequence of tasks, without waiting for the result:

If your top-level code does UI calls:

// This queues an independent execution to MainThread.
// We don't "await" the Dispatch, because we want it to run independently.
Dispatcher.Dispatch(async () => await TopMethod());

If your top-level code does not do UI calls:

// This queues an independent execution to the Thread Pool.
// We don't "await" the Run, because we want it to run independently.
Task.Run(async () => await TopMethod());

In either case, instead of using continuations, TopMethod uses awaits to sequence the tasks:

async void TopMethod()
{
    await ..Task1..;
    await ..Task2..;
    await ..Task3..;
}

This is equivalent to Task1.ContinueWith(Task2.ContinueWith(Task3)); (Off the top of my head; I may not have the syntax quite right on this.)


If you are on a background thread (did Task.Run), then to do UI calls, simply wrap in Dispatcher.Dispatch( ... ). As shown in first code snippet.

ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196
  • thank you, but it was intended that the UI-Thread is NOT waiting for completition, it should be notified when complete and run something – mech Aug 18 '22 at 22:09
  • What's the difference? The entire point of async/await is that it suspends **only that method**, but **releases the (UI) thread itself** for other purposes. This makes task continuations moot (except in rare circumstances). As I say in a comment in the code, what you normally would put in a continuation, you simply put in the next line of the method. Much simpler coding; equivalent result. – ToolmakerSteve Aug 19 '22 at 03:14
  • ... this is why async/await was invented. It doesn't do anything that couldn't be previously done with tasks and continuations; it is simply a more succinct way to code continuations. – ToolmakerSteve Aug 19 '22 at 03:21
1

You can capture SynchronizationContext in your syncToolService in constructor, or by defining explicitly API for capturing, kinda:

public void CaptureSynchronizationContext(SynchronizationContext context)
{   
    var current = SynchronizationContext.Current;
    if (context is null)
    {            
        this.capturedScheduler = TaskScheduler.Current;
        return;
    } 
   SynchronizationContext.SetSynchronizationContext(context);
   this.capturedScheduler = TaskScheduler.FromCurrentSynchronizationContext();
   SynchronizationContext.SetSynchronizationContext(current);
}

Add make some wrapper for your logic to be called in specified context:

private void RunTaskWithContinuation(Task task, Action<Task> continuation)
{
  task.ConfigureAwait(false);
  task.ContinueWith(t => continuation(t), capturedScheduler);
  task.Start();
}

So, somewhere in your UI:

// afaik you should call it once per every Window
syncToolService.CaptureSynchronizationContext(SynchronizationContext.Current);

And your code above would look like this:

// read local files
if (phase == 2)
{
    Task t = new Task(() => BPMSyncToolService.loadLocalData(viewModel.DataFiles));
    RunTaskWithContinuation(t, () => NotifySyncPhaseCompleted?.Invoke(this, new SyncPhaseCompletedEventArgs { phase = phase }));
}

Not tested, but i would try this idea first.
Btw, if SynchronizationContext is null, guess your problem would be persisted.

There is space for refactoring, just wanted to show the idea.

UPDATE

There is ReportProgress type - right tool for reports in multithreaded environment. May be this is what you are looking for.
But it works the same way, as i did above - via context capturing.

Ryan
  • 609
  • 6
  • 13