Home > Net >  How to use Progress class when showing form on second UI thead?
How to use Progress class when showing form on second UI thead?

Time:09-17

I have a situation where I have to run some long running work on the main thread (which is the UI thread). I want to show a progress form for this long running work and since I cannot show it on the main thread (because it is already busy working) I want to show it in a new thread.

In principle this seems to work and I can see my form showing on the new thread.

My problem is that I cannot get the Progress class to work so that I can update my form that is showing on the "second UI thread". I tried the below code:

        Dim oProgress As Progress(Of clsPrgObject)

        Dim _thread As Thread = New Thread(Sub()
                                               userControl.FrmPrg = New frmProgress(userControl.CancellationTokenSource, bAllowCancel)
                                               Application.Run(userControl.FrmPrg)
                                           End Sub)

        _thread.SetApartmentState(ApartmentState.STA)
        _thread.Start()

        oProgress = New Progress(Of clsPrgObject)(Sub(runNumber)
                                                          userControl.FrmPrg.Invoke((Sub() userControl.FrmPrg.Status = runNumber))
                                                      End Sub)

        DoSomeWorkOnUIThread(oProgress)

I call oProgress.Report in DoSomeWorkOnUIThread and this works but only if I perform the work on a background thread and show the form on the UI thread (which is exactly the reverse of how I actually want to do it).

I also tried to move the code part shown below to the creation of _thread, but then oProgress is Nothing when I try to use it in DoSomeWorkOnUIThread.

    oProgress = New Progress(Of clsPrgObject)(Sub(runNumber)
                                                      userControl.FrmPrg.Invoke((Sub() userControl.FrmPrg.Status = runNumber))
                                                  End Sub)

CodePudding user response:

This approach has some drawbacks, so I don't really recommend it. It would be better to use async on the UI thread, so that you have a possibility to give your status dialog an owner handle. If you show this status form on a background thread, you could still pass them an owner handle, but as the UI thread will be blocked, your status dialog probably also won't update (the input queues of the two threads get automatically attached, so if one is blocked, the other gets also blocked), even deadlock would be possible. If your dialog doesn't have an owner, it could disappear in the background (for example by switching to another app, showing desktop, etc.) and most of the users don't know how to switch between the open windows with Ctrl-Esc. A very dirty workaround would be to set Form.TopMost and/or Form.ShowInTaskBar to true to avoid this - i don't recommend either one. So the following code is merely for demonstration purposes. I'm not familiar with VB.NET syntax, so the code is in C#.

I suppose, your clsPrgObject class is there to report more than a progress percent value, its equivalent in my code is the class ProgressStatus. You can expand it at will to carry arbitrary progress data:

internal class ProgressStatus
{
    /// <summary>
    /// Initializes a new instance of the class providing only a <see cref="StatusText"/> change.
    /// </summary>
    /// <param name="text">The value for <see cref="StatusText"/>.</param>
    internal ProgressStatus(string text) => StatusText = text;

    /// <summary>
    /// Initializes a new instance of the class providing only a progress value in percent.
    /// </summary>
    /// <param name="percentComplete">The value for <see cref="PercentComplete"/>.</param>
    internal ProgressStatus(int percentComplete) => PercentComplete = percentComplete;

    /// <summary>
    /// Initializes a new instance of the class providing a status text and a progress value.
    /// </summary>
    /// <param name="text">The value for <see cref="StatusText"/>.</param>
    /// <param name="percentComplete">The value for <see cref="PercentComplete"/>.</param>
    internal ProgressStatus(string text, int percentComplete)
    {
        StatusText = text;
        PercentComplete = percentComplete;
    }

    /// <summary>
    /// An optional status text. If null, there is no change.
    /// </summary>
    public string StatusText { get; set; }

    /// <summary>
    /// An optional progress value. If null, there is no change.
    /// </summary>
    public int? PercentComplete { get; set; }
}

You also have a CancellationTokenSource in your code, I assume to allow cancellation from your progress dialog. So I've included this in the ProgressDialog class:

internal sealed partial class ProgressDialog : Form
{
    private readonly ManualResetEvent ready;
    private readonly CancellationTokenSource cts;
    private IProgress<ProgressStatus> progress;

    private ProgressDialog()
    {
        InitializeComponent();
    }

    internal ProgressDialog(ManualResetEvent ready, CancellationTokenSource cts)
        : this()
    {
        this.ready = ready;
        this.cts = cts;
    }

    protected override void OnShown(EventArgs e)
    {
        base.OnShown(e);
        /* Normally in a windows forms app you will have at this stage a proper
         * WindowsFormsSynchronizationContext. Under VSTO check it for sure.
         * It is essential for the Progress<T> class to work properly.
         */

        if (!(SynchronizationContext.Current is WindowsFormsSynchronizationContext))
            SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());

        progress = new Progress<ProgressStatus>(x =>
        {
            if (x.StatusText != null)
                statusLabel.Text = x.StatusText;
            if (x.PercentComplete.HasValue)
                progressBar.Value = x.PercentComplete.Value;
        });
        //Signal that the instance is ready for usage, the other (UI) thread may continue
        ready.Set();
    }

    internal void SetProgress(ProgressStatus status)
    {
        progress.Report(status ?? throw new ArgumentNullException(nameof(status)));
        
        //Alternate method without using the Progress<T> class:
        //if (!IsHandleCreated)
        //    return;
        //if (InvokeRequired)
        //{
        //    Invoke(new Action<ProgressStatus>(SetProgress), status);
        //    return;
        //}
        //if (status.StatusText != null)
        //    statusLabel.Text = status.StatusText;
        //if (status.PercentComplete.HasValue)
        //    progressBar.Value = status.PercentComplete.Value;
    }

    private void CancelButton_Click(object sender, EventArgs e)
    {
        cts.Cancel();
    }
}

And finally the CancellableProgress class which manages the CancellationTokenSource and the progress form:

/// <summary>
/// Shows a dialog with a status text and a progress value on a background thread 
/// allowing the user to cancel an operation.
/// </summary>
internal sealed class CancellableProgress : IDisposable
{
    private CancellationTokenSource cts;
    private ProgressDialog dialog;

    internal CancellableProgress()
    {
        cts = new CancellationTokenSource();
        using (var ready = new ManualResetEvent(false))
        {
            var t = new Thread(() =>
            {
                dialog = new ProgressDialog(ready, cts);
                dialog.FormClosed  = (s, e) => dialog = null;
                //Run a message pump on the background thread.
                Application.Run(dialog);
                dialog = null;
            });
            t.SetApartmentState(ApartmentState.STA);
            t.Start();
            //Wait until the dialog is set up and shown, ready to accept progress changes.
            ready.WaitOne();
        }
    }

    internal CancellationToken CancellationToken => cts.Token;

    internal void ReportProgress(int percent) => ReportProgress(new ProgressStatus(percent));

    internal void ReportProgress(string text) => ReportProgress(new ProgressStatus(text));

    internal void ReportProgress(string text, int percent) => ReportProgress(new ProgressStatus(text, percent));

    internal void ReportProgress(ProgressStatus status)
    {
        dialog?.SetProgress(status);
    }

    public void Dispose()
    {
        if (dialog == null)
            return;
        dialog.Invoke((MethodInvoker)dialog.Close);
        dialog = null;
    }
}

A simple sample usage would be:

private void HeavyWorkOnUIThread()
{
    bool wasCancelled = false;
    using (var progress = new CancellableProgress())
    {
        var token = progress.CancellationToken;
        try
        {
            progress.ReportProgress("Loop 1 running...");
            for (int i = 0; i < 100; i  )
            {
                token.ThrowIfCancellationRequested();
                Thread.Sleep(10);
                progress.ReportProgress(i);
            }

            progress.ReportProgress("Loop 2 running...", 0);
            for (int i = 0; i < 100; i  )
            {
                token.ThrowIfCancellationRequested();
                Thread.Sleep(10);
                progress.ReportProgress(i);
            }
        }
        catch (OperationCanceledException)
        {
            wasCancelled = true;
        }
    }
    MessageBox.Show(wasCancelled ? "Cancelled." : "All done.");
}

The code is missing some argument checkings, also, the ProgressDialog can be simply closed by the user and so on... You can solve those problems yourself, if you decide to use this solution. I still prefer the other one.

  • Related