1

Currently I am creating a background STA thread to keep the UI responsive but it slows down my function calls on the main thread.

Based on this thread How to update progress bar while working in the UI thread I tried the following but the UI only gets updated after all of the work has finished. I tried playing around with the Dispatcher priorities but none of them seem to work.

What I also tried is adding _frmPrg.Refresh() to my Progress callback but this does not seem to change anything.

    Dim oProgress = New Progress(Of PrgObject)(Sub(runNumber)
                                                      _frmPrg.Invoke((Sub()
                                                                          _frmPrg.Status = runNumber
                                                                      End Sub))
                                                  End Sub)

    System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(Sub()
                                                                          DoLongRunningWork(oProgress, _cancellationToken)
                                                                      End Sub, System.Windows.Threading.DispatcherPriority.Background)
chriscode
  • 121
  • 1
  • 8

2 Answers2

2

I can't really help you with your problem, but I'll try to clarify what happens in your posted code. DoLongRunningWork will be invoked through Dispatcher on the UI thread, when the UI thread is not busy. But once started, it will block the UI thread until it completes. So you can't show a progress this way. Your single chance is, to let DoLongRunningWork run on a background thread. That brings you nothing, if the long-running methods come from office objects, which must be accessed from the UI thread...

The Progress class (see the remarks section) invokes your event handler on the UI thread automatically, so you don't need _frmPrg.Invoke in your event handler.

Maybe you can start a STAthread for your progress form and show it from there. The instance of your Progress class must be created in this thread too, but not before your form is shown to ensure, that the thread becomes a WindowsFormsSynchronisationContext (or you set one explicitly after starting the thread). A plain SynchronisationContext won't work! At least you get updates in your form this way, but the UI thread of the office app will still be blocked. And of course, any action you make with your progress form must be invoked on the UI thread, if accessing office objects.

Steeeve
  • 824
  • 1
  • 6
  • 14
  • Does this mean that my invokes in the Progress are also not executed in the dispatcher once my DoLongRunningWork method is started? That would explain why calling _frmPrg.Refresh() in my progress callback does not work. – chriscode Aug 05 '21 at 09:56
  • Ah I see that in the link I posted the Dispatcher is actually called INSIDE of a loop, so what you are saying makes perfect sense. Maybe somebody else has another idea otherwise I will accept your answer. Thanks! – chriscode Aug 05 '21 at 11:33
  • @chriscode Yes. Your calls to the Progress class will be posted (not sent), so they get queued and executed on the UI thread, after your DoLongRunningWork completes. – Steeeve Aug 05 '21 at 11:35
  • I would like to also try the approach you outlined in this answer, i.e. doing the work on the main thread and showing a progressbar on a separate thread. I created a new question for this because this one was (falsely) marked as duplicate. Here is the link if you are interested in following up on this topic. Thanks: https://stackoverflow.com/questions/69133412/how-to-use-progress-class-when-showing-form-on-second-ui-thead – chriscode Sep 10 '21 at 16:19
  • @chriscode are you expecting better performace (you won't get it) or is it just curiosity? – Steeeve Sep 10 '21 at 16:33
  • The accepted answer is perfectly fine but for my actual code base it looks like if I could use a new thread to show the progress form and somehow get the Progress Class to work (i.e. I can update this form from the UI thread) I would only have to rewrite small parts of my code. Also I am curious how to make the Progress class work here :-) – chriscode Sep 10 '21 at 16:54
1

After reading some other posts, I decided to suggest another solution. My previous answer still contains usable information, so I'll leave it there. I'm not familiar with VB.NET syntax, so the samples are in C#. I have tested the code in a VSTO plugin for PowerPoint, but it should run in any office application.

Forget the Progress class and background threads. Run everything on the UI thread!

Now use some async code. To stay on the UI thread, we need a "good" SynchronizationContext.

private static void EnsureWinFormsSyncContext()
{
    // Ensure that we have a "good" SynchronisationContext
    // See https://stackoverflow.com/a/32866156/10318835
    if (SynchronizationContext.Current is not WindowsFormsSynchronizationContext)
        SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
}

This is the event handler of a button. Note the manually added async keyword. The SynchronizationContext.Current gets resetted again and again, so ensure the good one in the EventHandler:

private async void OnButtonClick(object sender, EventArgs e)
{
    EnsureWinFormsSyncContext();
    // Return from event handler, ensure that we are really async
    // See https://stackoverflow.com/a/22645114/10318835
    await Task.Yield();
    await RunLongOnUIThread();
}

This will be the worker method, also running on the UI thread.

private async Task RunLongOnUIThread()
{
    //Dummy code, replace it with your code
    var pres = addIn.Application.Presentations.Add();
    for (int i = 0; i < 100; i++)
    {
        Debug.Print("Creating slide {0} on thread {1}", i, Thread.CurrentThread.ManagedThreadId);
        // If you have some workloads that can be run on a background 
        // thread, execute them with await Task.Run(...).
        try
        {
            var layout = pres.Designs[1].SlideMaster.CustomLayouts[1];
            var slide = pres.Slides.AddSlide(i + 1, layout);
            var shape = slide.Shapes.AddLabel(Microsoft.Office.Core.MsoTextOrientation.msoTextOrientationHorizontal, 0, 15 * i, 100, 15);
            shape.TextFrame.TextRange.Text = $"Text on slide {i + 1}";
        }
        catch (Exception ex)
        {
            Debug.Print("I don't know what am I doing here, I'm not familiar with PowerPoint... {0}", ex);
        }

        // Update UI
        statusLabel.Text = $"Slide {i + 1} done";
        progressBar1.Value = i + 1;

        // This is the magic! It gives the main thread the opportunity to update the UI.
        // It also processes input messages so you need to disable unwanted buttons etc.
        await IdleYield();
    }
}

The following method is for Windows Forms Applications where it does the job perfect. I've tried it also in PowerPoint. If you are facing problems, try the WPF flavour with await Dispatcher.Yield(DispatcherPriority.ApplicationIdle) instead of await IdleYield().

private static Task IdleYield()
{
    var idleTcs = new TaskCompletionSource<bool>();
    void handler(object s, EventArgs e)
    {
        Application.Idle -= handler;
        idleTcs.SetResult(true);
    }
    Application.Idle += handler;
    return idleTcs.Task;
}

Here are the (clickable) links to the answers that I used (I can't put them in the code-blocks...).

If in your real code something runs not as expected, check the thread you are running on and SynchronizationContext.Current.

Steeeve
  • 824
  • 1
  • 6
  • 14
  • You should not use any background threads to access the object model, that was the idea, to prevent marshaling. Parallel execution doesn't make any sense, all calls will be marshaled back to the UI thread. Maybe you can show some more code to see what exactly is time consuming. Which office app is your addin for? What version? I'm out for today, it's 3.40 AM here... – Steeeve Aug 06 '21 at 01:41
  • Async/await does not necessarily mean, that a new thread will be started. It depends on what exactly are you doing. In my sample you won't get nothing running on a threadpool thread. I know, it's a bit complicated, i didn't understand it either as I began using async/await :) – Steeeve Aug 06 '21 at 08:19
  • @chriscode: This is what I meant with "good" `SynchronizationContext` classes. Both `WindowsFormsSynchonizationContext` and `DispatcherSynchronizationContext` are synchonizing by using Windows Messages (probably `SendMessage` and `PostMessage` API). You can read all the linked posts, I can't explain it better :) – Steeeve Aug 07 '21 at 15:16