在UI线程中从异步组件触发事件

7
我正在使用.Net 2.0构建一个非可视组件。该组件使用异步套接字(BeginReceive、EndReceive等)。异步回调在由运行时创建的工作线程上下文中被调用。组件用户不必担心多线程(这是主要目标,我想要的)。
组件用户可以在任何线程中创建我的非可视组件(UI线程只是简单应用程序的常见线程。更严肃的应用程序可以在任意工作线程内创建组件)。组件触发事件,如“SessionConnected”或“DataAvailable”。
问题:由于异步回调和其中引发的事件处理程序在工作线程上下文中执行,因此需要使用中间层来强制事件处理程序在最初创建组件的线程上下文中执行。
示例代码(剥离了异常处理等...)
    /// <summary>
    /// Occurs when the connection is ended
    /// </summary>
    /// <param name="ar">The IAsyncResult to read the information from</param>
    private void EndConnect(IAsyncResult ar)
    {
        // pass connection status with event
        this.Socket.EndConnect(ar);

        this.Stream = new NetworkStream(this.Socket);

        // -- FIRE CONNECTED EVENT HERE --

        // Setup Receive Callback
        this.Receive();
    }


    /// <summary>
    /// Occurs when data receive is done; when 0 bytes were received we can assume the connection was closed so we should disconnect
    /// </summary>
    /// <param name="ar">The IAsyncResult that was used by BeginRead</param>
    private void EndReceive(IAsyncResult ar)
    {
        int nBytes;
        nBytes = this.Stream.EndRead(ar);
        if (nBytes > 0)
        {
            // -- FIRE RECEIVED DATA EVENT HERE --

            // Setup next Receive Callback
            if (this.Connected)
                this.Receive();
        }
        else
        {
            this.Disconnect();
        }
    }

由于异步套接字的特性,所有使用我的组件的应用程序都会充斥着“如果(this.InvokeRequired){...}”的代码,而我只希望用户能够无忧地使用我的组件作为一种即插即用的方式。
那么我该如何在不需要用户检查InvokeRequired的情况下引发事件(或者换句话说,如何强制在与最初触发事件的线程相同的线程中引发事件)?
我已经阅读了关于AsyncOperation、BackgroundWorkers、SynchronizingObjects、AsyncCallbacks和其他大量内容,但这些都让我头晕。
我确实想出了这个笨拙的“解决方案”,但在某些情况下似乎会失败(例如当我的组件通过静态类从WinForms项目调用时)。
    /// <summary>
    /// Raises an event, ensuring BeginInvoke is called for controls that require invoke
    /// </summary>
    /// <param name="eventDelegate"></param>
    /// <param name="args"></param>
    /// <remarks>http://www.eggheadcafe.com/articles/20060727.asp</remarks>
    protected void RaiseEvent(Delegate eventDelegate, object[] args)
    {
        if (eventDelegate != null)
        {
            try
            {
                Control ed = eventDelegate.Target as Control;
                if ((ed != null) && (ed.InvokeRequired))
                    ed.Invoke(eventDelegate, args);
                else
                    eventDelegate.DynamicInvoke(args);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.GetType());
                Console.WriteLine(ex.Message);
                //Swallow
            }
        }
    }

希望能得到任何帮助。提前感谢!

编辑: 根据this thread,我最好的选择是使用SyncrhonizationContext.Post,但我不知道如何将其应用于我的情况。

4个回答

2

好的,经过更多阅读,我得出了以下结论:

public class MyComponent {
    private AsyncOperation _asyncOperation;

    /// Constructor of my component:
    MyComponent() {
        _asyncOperation = AsyncOperationManager.CreateOperation(null);
    }

    /// <summary>
    /// Raises an event, ensuring the correct context
    /// </summary>
    /// <param name="eventDelegate"></param>
    /// <param name="args"></param>
    protected void RaiseEvent(Delegate eventDelegate, object[] args)
    {
        if (eventDelegate != null)
        {
            _asyncOperation.Post(new System.Threading.SendOrPostCallback(
                delegate(object argobj)
                {
                    eventDelegate.DynamicInvoke(argobj as object[]);
                }), args);
        }
    }
}

这里发布的另一种解决方案还处于正在进行中的状态。根据微软开发者网络(MSDN)的说法,这里发布的解决方案到目前为止似乎是最好的。非常欢迎提出建议。

1

我似乎找到了解决方案:

    private SynchronizationContext _currentcontext

    /// Constructor of my component:
    MyComponent() {
        _currentcontext = WindowsFormsSynchronizationContext.Current;
       //...or...?
        _currentcontext = SynchronizationContext.Current;
    }

    /// <summary>
    /// Raises an event, ensuring the correct context
    /// </summary>
    /// <param name="eventDelegate"></param>
    /// <param name="args"></param>
    protected void RaiseEvent(Delegate eventDelegate, object[] args)
    {
        if (eventDelegate != null)
        {
            if (_currentcontext != null)
                _currentcontext.Post(new System.Threading.SendOrPostCallback(
                    delegate(object a)
                    {
                        eventDelegate.DynamicInvoke(a as object[]);
                    }), args);
            else
                eventDelegate.DynamicInvoke(args);
        }
    }

我还在测试这个,但它似乎工作得很好。


当您的组件不是在UI线程上创建时,这种方法会发生什么? - Jeff Cyr
我忘记粘贴我的最新编辑,它检查_currentcontext是否为空;现在已经修复(并编辑)。 - ComponentBuilder
我刚刚检查了一下,如果在组件的构造函数中捕获了SynchronizationContext,并且该组件是在非UI线程中创建的,则您的eventDelegate将在ThreadPool上执行。 - Jeff Cyr
我现在使用WindowsFormsSynchronizationContext(我又编辑了我的“解决方案”)。这会有什么区别吗?恐怕我对这个话题一无所知。我通过控制台应用程序和WinForms应用程序进行了检查,两者似乎都正常工作。 - ComponentBuilder

0
也许我没有理解问题,但在我的看法中,您只需在异步状态中传递一个自定义对象的引用即可。
我编写了以下示例来说明:
首先,我们有一个回调对象。它有两个属性——一个控件用于分派操作和一个要调用的操作;
public class Callback
{
    public Control Control { get; set; }
    public Action Method { get; set; }
}

然后我有一个WinForms项目,在另一个线程上调用一些随机代码(使用BeginInvoke),并在代码执行完成后显示一个消息框。

    private void Form1_Load(object sender, EventArgs e)
    {
        Action<bool> act = (bool myBool) =>
            {
                Thread.Sleep(5000);
            };

        act.BeginInvoke(true, new AsyncCallback((IAsyncResult result) =>
        {
            Callback c = result.AsyncState as Callback;
            c.Control.Invoke(c.Method);

        }), new Callback()
        {
            Control = this,
            Method = () => { ShowMessageBox(); }
        });            
    }

ShowMessageBox方法必须在UI线程上运行,代码如下:

    private void ShowMessageBox()
    {
        MessageBox.Show("Testing");
    }

这是您在寻找的内容吗?


并不是真的;当使用我的组件时,这对人们来说太复杂了。他们所要做的就是引用组件,并使用如下代码://使用IP、端口等初始化MyComponent,然后: MyComponent.SendString("这很酷");MyComponent会触发事件;无论这些事件是否由GUI或非GUI项目处理都没有关系,我不希望用户负责检查InvokeRequired。 - ComponentBuilder

0
如果您的组件必须始终由同一线程使用,可以像这样操作:
public delegate void CallbackInvoker(Delegate method, params object[] args);

public YourComponent(CallbackInvoker invoker)
{
    m_invoker = invoker;
}

protected void RaiseEvent(Delegate eventDelegate, object[] args)
{
    if (eventDelegate != null)
    {
        try
        {
            if (m_invoker != null)
                m_invoker(eventDelegate, args);
            else
                eventDelegate.DynamicInvoke(args);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.GetType());
            Console.WriteLine(ex.Message);
            //Swallow
        }
    }
}

然后,当您从表单或其他控件实例化组件时,可以执行以下操作:

YourComponent c = new YourComponent(this.Invoke);

为了将事件排队到非 UI 工作线程上,它必须具有某种工作排队机制,然后您可以给一个具有 CallbackInvoker 签名的方法来将委托排队到工作线程上。

这会让使用我的组件的用户面临过于复杂的挑战。 - ComponentBuilder
那个复杂性在哪里?在组件的构造函数中指定委托? - Jeff Cyr
我希望我的组件可以透明地异步化(这样最终用户就不会注意到)。当最终用户不得不出于(显而易见的)原因传递委托时,我认为这并不“用户友好”。 不过,我似乎已经找到了解决方案;但我不确定它是否是“最佳”的解决方案。 - ComponentBuilder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接