简体   繁体   中英

Why does calling anotherForm.ShowDialog() cause exceptions in System.IProgress.Report() later?

I have a form with a single text box txtOutput and the following source:

public partial class frmMain : Form
{
    private IProgress reporter;

    public frmMain(string[] args)
    {
        InitializeComponent();

        new anotherForm().ShowDialog();
        reporter = new Progress<string>(txtOutput.AppendText);

        Task.Run((Action)ReadReply);
    }

    private void ReadReply()
    {
        reporter.Report("test");
    }
}

If you run this, an exception is thrown:

System.InvalidOperationException: Cross-thread operation not valid: Control 'txtOutput' accessed from a thread other than the thread it was created on.

   at System.Windows.Forms.Control.get_Handle()
   at System.Windows.Forms.TextBoxBase.GetSelectionStartAndLength(Int32& start, Int32& length)
   at System.Windows.Forms.TextBoxBase.AppendText(String text)
   at System.Progress`1.InvokeHandlers(Object state)
   at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

If you reorder these two lines, everything works:

reporter = new IProgress<string>(txtOutput.AppendText);
new anotherForm().ShowDialog();
  1. Why does Progress.Report() throw this exception at all? Wasn't it created to avoid this kind of exceptions?
  2. Why does calling ShowDialog() break IProgress.Report() ?

If you are using Progress<T> as the IProgress<T> its expected to behave like this.

Progress<T> captures the SyncronizationContext when it's constructed, and when using Report it will try to invoke with the captured SyncronizedContext :
From the docs :

Any handler provided to the constructor or event handlers registered with the ProgressChanged event are invoked through a SynchronizationContext instance captured when the instance is constructed.

If you will check your SyncronizationContext.Current before and after the ShowDialog you'll see that it changed from WindowsFormsSynchronizationContext to SynchronizationContext .

So the original SyncronizationContext is lost.

That's why if you construct the Progress<T> before the ShowDialog it works.


The reason for the loss of the SyncronizationContext can be found through the source code:

When the form is shown with .ShowDialog it causes a new message loop to be generated for it and a new WindowsFormsSynchronizationContext is installed for it only if no previous one exists ( see here ) and then on closed it is uninstalled to the previous one ( here ).

Since the context was already WindowsFormsSynchronizationContext when the second form shown, no new context was created, thus when closing causes the context to be set the the one before the main form was constructed.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM