Hosting .NET and WinForms, SynchronizationContexts

2019-08-08 22:45发布

We are hosting .NET 4.0 and lots of WinForms windows in a Delphi application.

We have discovered that whenever we from Delphi end up calling ShowDialog on a .NET form, when the form closes, SynchronizationContext.Current is reset back to System.Threading.SynchronizationContext which uses the thread pool.

Is there a way for us to force this to not happen, or trick the code to reset it back to a WindowsFormsSynchronizationContext instead, short of adding the necessary code to every call to ShowDialog?

We have been hosting .NET for some years now but have only recently started doing work involving asynchronous code and this fails on the second window we open.

You can reproduce what we're seeing with this simple program, simply create a new WinForms project and paste this code into Program.cs:

using System;
using System.Diagnostics;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApplication4
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Debug.WriteLine("P1: " + SynchronizationContext.Current);

            using (var fm = new Form())
            {
                fm.Load += (s, e) => Debug.WriteLine("P2: " + SynchronizationContext.Current);
                fm.ShowDialog();
            }

            Debug.WriteLine("P3: " + SynchronizationContext.Current);
        }
    }
}

The output:

P1: 
P2: System.Windows.Forms.WindowsFormsSynchronizationContext
P3: System.Threading.SynchronizationContext

Basically:

  • Before the form has been opened, SynchronizationContext.Current is null.
  • In the Form.Load event, SynchronizationContext.Current is an instance of WindowsFormsSynchronizationContext
  • After the form has closed, SynchronizationContext.Current has been reset back to System.Threading.SynchronizationContext

We have tried various tricks to try to fool this code:

  • Constructing a hidden form
  • Constructing a hidden form and making this the owner in the call to ShowDialog
  • Constructing a visible form (shown with Show, not ShowDialog)

After looking through the reference source we think we've identified the likely reason, at this line: Applicationcs#3445 and onwards:

messageLoopCount--;

if (messageLoopCount == 0) {
    // If last message loop shutting down, install the
    // previous op [....] context in place before we started the first
    // message loop.
    WindowsFormsSynchronizationContext.Uninstall(false);
}

Since the main message loop is done from Delphi, .NET has no knowledge of this and thus the first window to pop open will, when closed, end up tearing down the world since .NET thinks the world is about to end.

1条回答
来,给爷笑一个
2楼-- · 2019-08-08 23:07

Wrap WindowsFormsSynchronizationContext in your context. Like:

class MySynchronizationContext : SynchronizationContext
{
    private SynchronizationContext context = new WindowsFormsSynchronizationContext();

    public override SynchronizationContext CreateCopy()
    {
        return context.CreateCopy();
    }

    public override bool Equals(object obj)
    {
        return context.Equals(obj);
    }

    public override int GetHashCode()
    {
        return context.GetHashCode();
    }

    public override void OperationCompleted()
    {
        context.OperationCompleted();
    }

    public override void OperationStarted()
    {
        context.OperationStarted();
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        context.Post(d, state);
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        context.Send(d, state);
    }

    public override string ToString()
    {
        return "Wrapped";
    }

    public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout)
    {
        return context.Wait(waitHandles, waitAll, millisecondsTimeout);
    }
}

And set it before any windows form call. In this case it will not be replaced:

        /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        MySynchronizationContext context = new MySynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(context);
        Debug.WriteLine("P1: " + SynchronizationContext.Current );

        using (var fm = new Form())
        {
            fm.Load += (s, e) => Debug.WriteLine("P2: " + SynchronizationContext.Current);
            fm.ShowDialog();
        }

        Debug.WriteLine("P3: " + SynchronizationContext.Current );
    }

Hope it will help.

查看更多
登录 后发表回答