简体   繁体   English

在跨线程WinForm事件处理中避免Invoke / BeginInvoke的困境?

[英]Avoiding the woes of Invoke/BeginInvoke in cross-thread WinForm event handling?

I'm still plagued by background threading in a WinForm UI. 我仍然受到WinForm UI中背景线程的困扰。 Why? 为什么? Here are some of the issues: 以下是一些问题:

  1. Obviously the most important issue, I can not modify a Control unless I'm executing on the same thread that created it. 显然是最重要的问题,除非我在创建它的同一个线程上执行,否则我无法修改控件。
  2. As you know, Invoke, BeginInvoke, etc are not available until after a Control is created. 如您所知,Invoke,BeginInvoke等在创建Control之后才可用。
  3. Even after RequiresInvoke returns true, BeginInvoke can still throw ObjectDisposed and even if it doesn't throw, it may never execute the code if the control is being destroyed. 即使在RequiresInvoke返回true之后,BeginInvoke仍然可以抛出ObjectDisposed,即使它没有抛出,如果控件被销毁,它也可能永远不会执行代码。
  4. Even after RequiresInvoke returns true, Invoke can indefinitely hang waiting for execution by a control that was disposed at the same time as the call to Invoke. 即使在RequiresInvoke返回true之后,Invoke也可以无限期地挂起等待由调用Invoke同时处理的控件执行。

I'm looking for an elegant solution to this problem, but before I get into specifics of what I'm looking for I thought I would clarify the problem. 我正在寻找这个问题的优雅解决方案,但在我了解我正在寻找的具体内容之前,我想我会澄清问题。 This is to take the generic problem and put a more concrete example behind it. 这是为了解决一般性问题并在其背后提出一个更具体的例子。 For this example let's say we are transferring larger amounts of data over the internet. 对于这个例子,假设我们正在通过互联网传输大量数据。 The user interface must be able to show a progress dialog for the transfer already in-progress. 用户界面必须能够显示正在进行的传输的进度对话框。 The progress dialog should update constantly and quickly (updates 5 to 20 times per second). 进度对话框应该持续快速更新(每秒更新5到20次)。 The user can dismiss the progress dialog at any time and recall it again if desired. 用户可以随时关闭进度对话框,并在需要时再次调用它。 And further, lets pretend for arguments sake that if the dialog is visible, it must process every progress event. 而且,让我们假装参数,如果对话框可见,它必须处理每个进度事件。 The user can click Cancel on the progress dialog and via modifying the event args, cancel the operation. 用户可以单击进度对话框上的取消,并通过修改事件参数,取消操作。

Now I need a solution that will fit in the following box of constraints: 现在我需要一个适合以下限制条件的解决方案:

  1. Allow a worker thread to call a method on a Control/Form and block/wait until execution is complete. 允许工作线程调用Control / Form上的方法并阻塞/等待,直到执行完成。
  2. Allow the dialog itself to call this same method at initialization or the like (and thus not use invoke). 允许对话框本身在初始化等时调用相同的方法(因此不使用invoke)。
  3. Place no burden of implementation on the handling method or the calling event, the solution should only change the event subscription itself. 对处理方法或调用事件不施加任何实施负担,解决方案应仅更改事件订阅本身。
  4. Appropriately handle blocking invokes to a dialog that might be in the process of disposing. 适当地处理阻塞调用到可能正在处理的对话框。 Unfortunately this is not as easy as checking for IsDisposed. 不幸的是,这并不像检查IsDisposed那么容易。
  5. Must be able to be used with any event type (assume a delegate of type EventHandler) 必须能够与任何事件类型一起使用(假设类型为EventHandler的委托)
  6. Must not translate exceptions to TargetInvocationException. 不得将异常转换为TargetInvocationException。
  7. The solution must work with .Net 2.0 and higher 该解决方案必须与.Net 2.0及更高版本配合使用

So, can this be solved given the constraints above? 那么,鉴于上述限制,这可以解决吗? I've searched and dug through countless blogs and discussions and alas I'm still empty handed. 我搜索并挖掘了无数的博客和讨论,唉,我还是空手而归。

Update: I do realize that this question has no easy answer. 更新:我确实意识到这个问题没有简单的答案。 I've only been on this site for a couple of days and I've seen some people with a lot of experience answering questions. 我只在这个网站上呆了几天,我见过一些有很多回答问题经验的人。 I'm hoping that one of these individuals has solved this sufficiently enough for me to not spend the week or so it will take to build a reasonable solution. 我希望其中一个人已经足够解决这个问题,这样我就不会花费一周时间来建立一个合理的解决方案。

Update #2: Ok, I'm going to try and describe the problem in a little more detail and see what (if anything) shakes out. 更新#2:好的,我将尝试更详细地描述问题,看看有什么(如果有的话)震动。 The following properties that allow us to determine it's state have a couple of things raise concerns... 允许我们确定其状态的以下属性引起了一些问题...

  1. Control.InvokeRequired = Documented to return false if running on current thread or if IsHandleCreated returns false for all parents. Control.InvokeRequired =记录如果在当前线程上运行或者IsHandleCreated为所有父项返回false,则返回false。 I'm troubled by the InvokeRequired implementation having the potential to either throw ObjectDisposedException or potentially even re-create the object's handle. 我很担心InvokeRequired实现有可能抛出ObjectDisposedException或甚至可能重新创建对象的句柄。 And since InvokeRequired can return true when we are not able to invoke (Dispose in progress) and it can return false even though we might need to use invoke (Create in progress) this simply can't be trusted in all cases. 并且由于InvokeRequired在我们无法调用(正在进行Dispose)时可以返回true,并且它可以返回false,即使我们可能需要使用invoke(正在创建),这在所有情况下都不可信任。 The only case I can see where we can trust InvokeRequired returning false is when IsHandleCreated returns true both before and after the call (BTW the MSDN docs for InvokeRequired do mention checking for IsHandleCreated). 唯一可以看到我们可以信任的地方InvokeRequired返回false是当IsHandleCreated在调用之前和之后都返回true时(BTW,InvokeRequired的MSDN文档确实提到了对IsHandleCreated的检查)。

  2. Control.IsHandleCreated = Returns true if a handle has been assigned to the control; Control.IsHandleCreated =如果已为控件分配了句柄,则返回true;否则返回true。 otherwise, false. 否则,错误。 Though IsHandleCreated is a safe call it may breakdown if the control is in the process of recreating it's handle. 虽然IsHandleCreated是一个安全的调用,但如果控件正在重新创建它的句柄,它可能会崩溃。 This potential problem appears to be solveable by performing a lock(control) while accessing the IsHandleCreated and InvokeRequired. 这个潜在的问题似乎可以通过在访问IsHandleCreated和InvokeRequired时执行锁定(控制)来解决。

  3. Control.Disposing = Returns true if the control is in the process of disposing. Control.Disposing =如果控件处于处理过程中,则返回true。

  4. Control.IsDisposed = Returns true if the control has been disposed. Control.IsDisposed =如果控件已被释放,则返回true。 I'm considering subscribing to the Disposed event and checking the IsDisposed property to determin if BeginInvoke will ever complete. 我正在考虑订阅Disposed事件并检查IsDisposed属性以确定BeginInvoke是否会完成。 The big problem here is the lack of a syncronization lock durring the Disposing -> Disposed transition. 这里的一个大问题是在Disposing - > Disposed过渡期间缺少同步锁定。 It's possible that if you subscribe to the Disposed event and after that verify that Disposing == false && IsDisposed == false you still may never see the Disposed event fire. 如果您订阅Disposed事件并在此之后验证Disposing == false && IsDisposed == false,您可能永远不会看到Disposed事件触发。 This is due to the fact that the implementation of Dispose sets Disposing = false, and then sets Disposed = true. 这是因为Dispose的实现设置Disposing = false,然后设置Disposed = true。 This provides you an oppertunity (however small) to read both Disposing and IsDisposed as false on a disposed control. 这为您提供了一个机会(无论多小),以便在已处理的控件上将Disposing和IsDisposed都读为false。

... my head hurts :( Hopefully the information above will shed a little more light on the issues for anyone having these troubles. I appreciate your spare thought cycles on this. ......我的头疼了:(希望上面的信息可以为那些遇到这些麻烦的人提供更多的解决方案。我很感激你的思考周期。

Closing in on the trouble... The following is the later half of the Control.DestroyHandle() method: 关闭麻烦......以下是Control.DestroyHandle()方法的后半部分:

if (!this.RecreatingHandle && (this.threadCallbackList != null))
{
    lock (this.threadCallbackList)
    {
        Exception exception = new ObjectDisposedException(base.GetType().Name);
        while (this.threadCallbackList.Count > 0)
        {
            ThreadMethodEntry entry = (ThreadMethodEntry) this.threadCallbackList.Dequeue();
            entry.exception = exception;
            entry.Complete();
        }
    }
}
if ((0x40 & ((int) ((long) UnsafeNativeMethods.GetWindowLong(new HandleRef(this.window, this.InternalHandle), -20)))) != 0)
{
    UnsafeNativeMethods.DefMDIChildProc(this.InternalHandle, 0x10, IntPtr.Zero, IntPtr.Zero);
}
else
{
    this.window.DestroyHandle();
}

You'll notice the ObjectDisposedException being dispatched to all waiting cross-thread invocations. 您会注意到ObjectDisposedException被调度到所有等待的跨线程调用。 Shortly following this is the call to this.window.DestroyHandle() which in turn destroys the window and set's it's handle reference to IntPtr.Zero thereby preventing further calls into the BeginInvoke method (or more precisely MarshaledInvoke which handle both BeginInvoke and Invoke). 紧接着这是对this.window.DestroyHandle()的调用,它反过来破坏窗口并设置它对IntPtr.Zero的句柄引用,从而阻止对BeginInvoke方法的进一步调用(或者更准确地说是MarshaledInvoke,它同时处理BeginInvoke和Invoke)。 The problem here is that after the lock releases on threadCallbackList a new entry can be inserted before the Control's thread zeros the window handle. 这里的问题是在threadCallbackList上发布锁之后,可以在Control的线程将窗口句柄归零之前插入一个新条目。 This appears to be the case I'm seeing, though infrequently, often enough to stop a release. 这似乎是我看到的情况,虽然很少,但通常足以阻止释放。

Update #4: 更新#4:

Sorry to keep dragging this on; 很抱歉继续拖动它; however, I thought it worth documenting here. 但是,我认为值得记录在这里。 I've managed to solve most of the problems above and I'm narrowing in on a solution that works. 我已经设法解决了上面的大多数问题,并且我正在缩小有效的解决方案。 I've hit one more issue I was concerned about, but until now, have not seen 'in-the-wild'. 我已经打了一个我担心的问题,但直到现在,还没有看到'在野外'。

This issue has to do with the genius that wrote Control.Handle property: 这个问题与编写Control.Handle属性的天才有关:

    public IntPtr get_Handle()
    {
        if ((checkForIllegalCrossThreadCalls && !inCrossThreadSafeCall) && this.InvokeRequired)
        {
            throw new InvalidOperationException(SR.GetString("IllegalCrossThreadCall", new object[] { this.Name }));
        }
        if (!this.IsHandleCreated)
        {
            this.CreateHandle();
        }
        return this.HandleInternal;
    }

This by itself is not so bad (regardless of my opinions on get { } modifications); 这本身并不是那么糟糕(无论我对get {}修改的意见如何); however, when combined with the InvokeRequired property or the Invoke/BeginInvoke method it is bad. 但是,当与InvokeRequired属性或Invoke / BeginInvoke方法结合使用时,它很糟糕。 Here is the basic flow the Invoke: 这是Invoke的基本流程:

if( !this.IsHandleCreated )
    throw;
... do more stuff
PostMessage( this.Handle, ... );

The issue here is that from another thread I can successfully pass through the first if statement, after which the handle is destroyed by the control's thread, thus causing the get of the Handle property to re-create the window handle on my thread. 这里的问题是,从另一个线程我可以成功地传递第一个if语句,之后句柄被控件的线程销毁,从而导致Handle属性的get在我的线程上重新创建窗口句柄。 This then can cause an exception to be raised on the original control's thread. 这可能会导致在原始控件的线程上引发异常。 This one really has me stumped as there is no way to guard against this. 这个真的让我难过,因为没有办法防范这一点。 Had they only use the InternalHandle property and tested for result of IntPtr.Zero this would not be an issue. 如果他们只使用InternalHandle属性并测试IntPtr.Zero的结果,这不会是一个问题。

Your scenario, as described, neatly fits BackgroundWorker - why not just use that? 如上所述,您的方案完全适合BackgroundWorker - 为什么不使用它呢? Your requirements for a solution are way too generic, and rather unreasonable - I doubt there is any solution that would satisfy them all. 您对解决方案的要求过于笼统,而且不合理 - 我怀疑是否有任何解决方案可以满足所有这些要求。

I ran into this problem awhile back and came up with solution involving Synchronization Contexts. 我在一段时间后遇到了这个问题,并提出了涉及同步上下文的解决方案。 The solution is to add an extension method to SynchronizationContext which binds a particular delegate to the thread that the SynchronizationContext is bound to. 解决方案是向SynchronizationContext添加扩展方法,该方法将特定委托绑定到SynchronizationContext绑定的线程。 It will generate a new delegate which when invoked will marshal the call to the appropraite thread and then call the original delegate. 它将生成一个新的委托,在调用时将封送对appropraite线程的调用,然后调用原始委托。 It makes it nearly impossible for consumers of the delegate to call it in the wrong context. 它使代表的消费者几乎不可能在错误的上下文中调用它。

Blog post on the subject: 关于这个主题的博客文章:

Ok, days later I've finished creating a solution. 好的,几天后我已经完成了一个解决方案。 It solves all of the listed constraints and objectives in the initial post. 它解决了初始帖子中列出的所有约束和目标。 The usage is simple and straight-forward: 用法简单明了:

myWorker.SomeEvent += new EventHandlerForControl<EventArgs>(this, myWorker_SomeEvent).EventHandler;

When the worker thread calls this event it will handle the required invocation to the control thread. 当工作线程调用此事件时,它将处理对控制线程的所需调用。 It ensures that it will not hang indefinitely and will consistently throw an ObjectDisposedException if it is unable to execute on the control thread. 它确保它不会无限期挂起,如果无法在控制线程上执行,它将始终抛出ObjectDisposedException。 I've created other derivations of the class, one to ignore the error, and another to directly call the delegate if the control is not available. 我已经创建了该类的其他派生,一个用于忽略错误,另一个用于在控件不可用时直接调用委托。 Appears to work well and fully passes the several tests that reproduce the issues above. 似乎运作良好,并完全通过了几个重现上述问题的测试。 There is only one issue with the solution I can't prevent without violating constraint #3 above. 在不违反上述约束#3的情况下,我无法阻止解决方案只有一个问题。 This issue is the last (Update #4) in the issue description, the threading problems in get Handle. 这个问题是问题描述中的最后一个(Update#4),get Handle中的线程问题。 This can cause unexpected behavior on the original control thread, and I've regularly seen InvalidOperationException() thrown while calling Dispose() since the handle in the process of being created on my thread. 这可能会导致原始控制线程出现意外行为,并且我经常看到在调用Dispose()时抛出的InvalidOperationException(),因为在我的线程上创建过程中的句柄。 To allow for dealing with this I ensure a lock around accessing functions that will use the Control.Handle property. 为了允许处理这个问题,我确保锁定访问将使用Control.Handle属性的函数。 This allows a form to overload the DestroyHandle method and lock prior to calling the base implementation. 这允许表单在调用基本实现之前重载DestroyHandle方法并锁定。 If this is done, this class should be entirely thread-safe (To the best of my knowledge). 如果这样做,这个类应该完全是线程安全的(据我所知)。

public class Form : System.Windows.Forms.Form
{
    protected override void DestroyHandle()
    {
        lock (this) base.DestroyHandle();
    }
}

You may notice the core aspect of solving the dead-lock became a polling loop. 你可能会注意到解决死锁的核心方面变成了一个轮询循环。 Originally I successfully solved the test cases by handling the control's event for Disposed and HandleDestroyed and using multiple wait handles. 最初我通过处理Disposed和HandleDestroyed的控件事件并使用多个等待句柄成功解决了测试用例。 After a more careful review, I found the subscription/unsubscription from these events is not thread-safe. 经过仔细审查后,我发现这些事件的订阅/取消订阅不是线程安全的。 Thus I chose to poll the IsHandleCreated instead so as not to create unneeded contention on the thread's events and thereby avoid the possibility of still producing a dead-lock state. 因此,我选择轮询IsHandleCreated,以免在线程的事件上产生不必要的争用,从而避免仍然产生死锁状态的可能性。

Anyway, here is the solution I came up with: 无论如何,这是我提出的解决方案:

/// <summary>
/// Provies a wrapper type around event handlers for a control that are safe to be
/// used from events on another thread.  If the control is not valid at the time the
/// delegate is called an exception of type ObjectDisposedExcpetion will be raised.
/// </summary>
[System.Diagnostics.DebuggerNonUserCode]
public class EventHandlerForControl<TEventArgs> where TEventArgs : EventArgs
{
    /// <summary> The control who's thread we will use for the invoke </summary>
    protected readonly Control _control;
    /// <summary> The delegate to invoke on the control </summary>
    protected readonly EventHandler<TEventArgs> _delegate;

    /// <summary>
    /// Constructs an EventHandler for the specified method on the given control instance.
    /// </summary>
    public EventHandlerForControl(Control control, EventHandler<TEventArgs> handler)
    {
        if (control == null) throw new ArgumentNullException("control");
        _control = control.TopLevelControl;
        if (handler == null) throw new ArgumentNullException("handler");
        _delegate = handler;
    }

    /// <summary>
    /// Constructs an EventHandler for the specified delegate converting it to the expected
    /// EventHandler&lt;TEventArgs> delegate type.
    /// </summary>
    public EventHandlerForControl(Control control, Delegate handler)
    {
        if (control == null) throw new ArgumentNullException("control");
        _control = control.TopLevelControl;
        if (handler == null) throw new ArgumentNullException("handler");

        //_delegate = handler.Convert<EventHandler<TEventArgs>>();
        _delegate = handler as EventHandler<TEventArgs>;
        if (_delegate == null)
        {
            foreach (Delegate d in handler.GetInvocationList())
            {
                _delegate = (EventHandler<TEventArgs>) Delegate.Combine(_delegate,
                    Delegate.CreateDelegate(typeof(EventHandler<TEventArgs>), d.Target, d.Method, true)
                );
            }
        }
        if (_delegate == null) throw new ArgumentNullException("_delegate");
    }


    /// <summary>
    /// Used to handle the condition that a control's handle is not currently available.  This
    /// can either be before construction or after being disposed.
    /// </summary>
    protected virtual void OnControlDisposed(object sender, TEventArgs args)
    {
        throw new ObjectDisposedException(_control.GetType().Name);
    }

    /// <summary>
    /// This object will allow an implicit cast to the EventHandler&lt;T> type for easier use.
    /// </summary>
    public static implicit operator EventHandler<TEventArgs>(EventHandlerForControl<TEventArgs> instance)
    { return instance.EventHandler; }

    /// <summary>
    /// Handles the 'magic' of safely invoking the delegate on the control without producing
    /// a dead-lock.
    /// </summary>
    public void EventHandler(object sender, TEventArgs args)
    {
        bool requiresInvoke = false, hasHandle = false;
        try
        {
            lock (_control) // locked to avoid conflicts with RecreateHandle and DestroyHandle
            {
                if (true == (hasHandle = _control.IsHandleCreated))
                {
                    requiresInvoke = _control.InvokeRequired;
                    // must remain true for InvokeRequired to be dependable
                    hasHandle &= _control.IsHandleCreated;
                }
            }
        }
        catch (ObjectDisposedException)
        {
            requiresInvoke = hasHandle = false;
        }

        if (!requiresInvoke && hasHandle) // control is from the current thread
        {
            _delegate(sender, args);
            return;
        }
        else if (hasHandle) // control invoke *might* work
        {
            MethodInvokerImpl invocation = new MethodInvokerImpl(_delegate, sender, args);
            IAsyncResult result = null;
            try
            {
                lock (_control)// locked to avoid conflicts with RecreateHandle and DestroyHandle
                    result = _control.BeginInvoke(invocation.Invoker);
            }
            catch (InvalidOperationException)
            { }

            try
            {
                if (result != null)
                {
                    WaitHandle handle = result.AsyncWaitHandle;
                    TimeSpan interval = TimeSpan.FromSeconds(1);
                    bool complete = false;

                    while (!complete && (invocation.MethodRunning || _control.IsHandleCreated))
                    {
                        if (invocation.MethodRunning)
                            complete = handle.WaitOne();//no need to continue polling once running
                        else
                            complete = handle.WaitOne(interval);
                    }

                    if (complete)
                    {
                        _control.EndInvoke(result);
                        return;
                    }
                }
            }
            catch (ObjectDisposedException ode)
            {
                if (ode.ObjectName != _control.GetType().Name)
                    throw;// *likely* from some other source...
            }
        }

        OnControlDisposed(sender, args);
    }

    /// <summary>
    /// The class is used to take advantage of a special-case in the Control.InvokeMarshaledCallbackDo()
    /// implementation that allows us to preserve the exception types that are thrown rather than doing
    /// a delegate.DynamicInvoke();
    /// </summary>
    [System.Diagnostics.DebuggerNonUserCode]
    private class MethodInvokerImpl
    {
        readonly EventHandler<TEventArgs> _handler;
        readonly object _sender;
        readonly TEventArgs _args;
        private bool _received;

        public MethodInvokerImpl(EventHandler<TEventArgs> handler, object sender, TEventArgs args)
        {
            _received = false;
            _handler = handler;
            _sender = sender;
            _args = args;
        }

        public MethodInvoker Invoker { get { return this.Invoke; } }
        private void Invoke() { _received = true; _handler(_sender, _args); }

        public bool MethodRunning { get { return _received; } }
    }
}

If you see anything wrong here, please let me know. 如果你在这里看到任何错误,请告诉我。

I'm not going to write an exhaustive solution for you that meets all of your requirements, but I will offer perspective. 我不会为您提供满足您所有要求的详尽解决方案,但我会提供观点。 Overall, though, I think you're shooting for the moon with those requirements. 但总的来说,我认为你是按照这些要求射击月球的。

The Invoke / BeginInvoke architecture simply executes a supplied delegate on the control's UI thread by sending it a Windows message and the message loop itself executes the delegate. Invoke / BeginInvoke体系结构只是通过向控件的UI线程发送一条Windows消息来执行提供的委托,并且消息循环本身执行委托。 The specific workings of this are irrelevant, but the point is that there is no particular reason that you have to use this architecture for thread sync with the UI thread. 这一点的具体工作方式是无关紧要的,但重点是没有特别的原因,您必须使用此架构与UI线程进行线程同步。 All you need is some other loop running, such as in a Forms.Timer or something like that, that monitors a Queue for delegates to execute and does so. 您所需要的只是运行其他一些循环,例如在Forms.Timer或类似的东西中,它监视Queue以便委托执行并执行此操作。 It would be fairly simple to implement your own, though I don't know what specifically it would get for you that Invoke and BeginInvoke don't provide. 实现自己的实现起来相当简单,但我不知道InvokeBeginInvoke不提供什么具体的功能。

This is not really answer to the second part of the question, but I will include it just for the reference: 这不是问题第二部分的真正答案,但我将其仅供参考:

private delegate object SafeInvokeCallback(Control control, Delegate method, params object[] parameters);
public static object SafeInvoke(this Control control, Delegate method, params object[] parameters)
{
    if (control == null)
        throw new ArgumentNullException("control");
    if (control.InvokeRequired)
    {
        IAsyncResult result = null;
        try { result = control.BeginInvoke(new SafeInvokeCallback(SafeInvoke), control, method, parameters); }
        catch (InvalidOperationException) { /* This control has not been created or was already (more likely) closed. */ }
        if (result != null)
            return control.EndInvoke(result);
    }
    else
    {
        if (!control.IsDisposed)
            return method.DynamicInvoke(parameters);
    }
    return null;
}

This code should avoid the most common pitfalls with Invoke/BeginInvoke and it's easy to use . 此代码应该避免使用Invoke / BeginInvoke最常见的陷阱,并且它易于使用。 Just turn 转身

if (control.InvokeRequired)
    control.Invoke(...)
else
    ...

into

control.SafeInvoke(...)

Similar construct is possible for BeginInvoke. BeginInvoke可以使用类似的构造。

Wow, long question. 哇,长问题。 I'll try to organize my answer so you can correct me if I understood something wrong, ok? 我会尝试整理我的答案,这样你就可以纠正我,如果我理解错了,好吗?

1) Unless you have an extremely good reason to call UI methods directly from different threads, don't. 1)除非你有充分的理由直接从不同的线程调用UI方法,否则不要。 You can always go for the producer/consumer model using event handlers: 您始终可以使用事件处理程序转到生产者/消费者模型:

protected override void OnLoad()
{
    //...
    component.Event += new EventHandler(myHandler);
}

protected override void OnClosing()
{
    //...
    component.Event -= new EventHandler(myHandler);
}

myHandler will be fired every time the component in a different thread needs to perform something in the UI, for example. 例如,每当不同线程中的组件需要在UI中执行某些操作时,myHandler将被触发。 Also, setting up the event handler in OnLoad and unsubscribing in OnClosing makes sure that the events will only be received/handled by the UI while its handle is created and ready to process the events. 此外,在OnLoad中设置事件处理程序并在OnClosing中取消订阅可确保在创建句柄并准备好处理事件时,UI仅接收/处理事件。 You won't even be able to fire events to this dialog if it is in the process of disposing, because you won't be subscribed to the event anymore. 如果处理过程中你甚至无法向该对话框发出事件,因为你将不再订阅该事件。 If another event is fired while one is still being processed, it will be queued. 如果在处理其中一个事件时触发另一个事件,则它将排队。

You can pass all the info you need in the event arguments: whether you're updating the progress, closing the window, etc. 您可以在事件参数中传递所需的所有信息:是否更新进度,关闭窗口等。

2) You don't need InvokeRequired if you use the model I suggested above. 2)如果您使用我上面建议的模型,则不需要InvokeRequired。 In this example, you know that the only thing that is firing myHandler will be your component that lives in another thread, for example. 在这个例子中,你知道触发myHandler的唯一事情就是你的组件,例如,它存在于另一个线程中。

private void myHandler(object sender, EventArgs args)
{
    BeginInvoke(Action(myMethod));
}

So you can always use invoke to make sure you'll be in the right thread. 因此,您始终可以使用invoke来确保您将处于正确的线程中。

3) Beware of synchronous calls. 3)注意同步呼叫。 If you want to, you can replace use Invoke instead of BeginInvoke. 如果需要,可以替换使用Invoke而不是BeginInvoke。 This will block your component until the event has been processed. 这将阻止您的组件,直到事件处理完毕。 However, if in the UI you need to communicate to something that is exclusive to the thread your component lives in, you can have deadlock problems. 但是,如果在UI中您需要与组件所在线程所独有的内容进行通信,则可能会出现死锁问题。 (I don't know if I made myself clear, please let me know). (我不知道自己是否清楚,请告诉我)。 I've had problems with exceptions when using reflection (TargetInvocationException) and BeginInvoke (as they start a different thread, you lose part of the stack trace), but I don't recall having a lot of trouble with Invoke calls, so you should be safe when it comes to exceptions. 我在使用反射(TargetInvocationException)和BeginInvoke时遇到异常问题(因为它们启动了不同的线程,你丢失了部分堆栈跟踪),但我不记得Invoke调用有很多麻烦,所以你应该在例外情况下要安全。

Whoa, long answer. 哇,答案很长。 If by any chance I missed any of your requirements or misunderstood something you said (english is not my native language, so we're never sure), please let me know. 如果有任何机会我错过了你的任何要求或误解了你说的话(英语不是我的母语,所以我们永远不确定),请告诉我。

I try to organize all such invoke messages to the GUI as fire and forget (handling the exception the GUI can throw due to the race condition on disposing the form). 我尝试将所有这样的调用消息组织到GUI中作为fire和forget(处理由于处理表单时的竞争条件而导致GUI可以抛出的异常)。

This way if it never executes no harm is done. 这样,如果它永远不会执行,则不会造成伤害。

If the GUI needs to respond to the working thread it has a way of effectively reversing the notification. 如果GUI需要响应工作线程,它有一种有效反转通知的方法。 For simple needs BackgroundWorker already handles this. 对于简单的需求,BackgroundWorker已经处理了这个问题

This is quite a difficult question. 这是一个非常困难的问题。 As I mention in a comment, I don't think it's solvable given the documented constraints. 正如我在评论中提到的,考虑到记录的约束,我认为它不可解决。 You can hack it given a particular implementation of the .net framework: knowing the implementation of various member functions may help you cheat by grabbing locks here and there, and knowing that "it's actually OKAY, to call other member functions on a different thread." 你可以通过.net框架的特定实现来破解它:知道各种成员函数的实现可以通过在这里和那里抓住锁来帮助你作弊,并且知道“它实际上是好的,在不同的线程上调用其他成员函数。 “

So, my basic answer for now is, "no." 所以,我现在的基本答案是,“不。” I hate to say it's not possible because I have a great deal of faith in the .Net framework. 我不愿意说这是不可能的,因为我对.Net框架有很大的信心。 Also, I'm comparatively a novice, not having studied frameworks in general, or CS, but the internet is open (even to ignorant people like me)! 另外,我比较新手,没有研究过一般的框架,或CS,但互联网是开放的(即使对像我这样无知的人)!

On a different topic, the argument can be posed and well-supported, "You should never need Invoke , only use BeginInvoke , and fire and forget." 在另一个主题上,可以提出并支持这个参数,“你永远不应该需要Invoke ,只使用BeginInvoke ,然后激发并忘记。” I won't bother trying to support it or even say that it's a correct assertion, but I will say that the common implementation is incorrect, and pose a working (I hope) one. 我不打算试图支持它,或者甚至说它是一个正确的断言,但我会说常见的实现是不正确的,并构成一个工作(我希望)。

Here's a common implementation (taken from a different answer here): 这是一个常见的实现(取自这里的不同答案):

protected override void OnLoad()
{
    //...
    component.Event += new EventHandler(myHandler);
}

protected override void OnClosing()
{
    //...
    component.Event -= new EventHandler(myHandler);
}

This isn't thread safe. 这不是线程安全的。 The component could have easily begun to call the invocation list just before the unsubscription, and only after we finish disposing does the handler get invoked. 组件可以在取消订阅之前很容易地开始调用调用列表,并且只有在我们完成处理之后才会调用处理程序。 The real point is, it's not documented how each component must use the event mechanism in .Net, and honestly, he doesn't have to unsubscribe you at all: once you've given out your phone number, nobody's required to erase it! 真正的问题是,没有记录每个组件必须如何使用.Net中的事件机制,老实说,他根本不需要取消订阅:一旦你给出了你的电话号码,没有人需要删除它!

Better is: 更好的是:

protected override void OnLoad(System.EventArgs e)
{
    component.Event += new System.EventHandler(myHandler);
}    
protected override void OnFormClosing(FormClosedEventArgs e)
{
    component.Event -= new System.EventHandler(myHandler);
    lock (lockobj)
    {
        closing = true;
    }
}
private void Handler(object a, System.EventArgs e)
{
    lock (lockobj)
    {
        if (closing)
            return;
        this.BeginInvoke(new System.Action(HandlerImpl));
    }
}
/*Must be called only on GUI thread*/
private void HandlerImpl()
{
    this.Hide();
}
private readonly object lockobj = new object();
private volatile bool closing = false;

Please let me know if I missed something. 如果我错过了什么,请告诉我。

如果您不喜欢BackgroundWoker(如@Pavel所述),您可能需要查看此库http://www.wintellect.com/PowerThreading.aspx

If I understand this, why do you ever need to dispose the progress dialog while the application is running? 如果我理解这一点,为什么在应用程序运行时需要处理进度对话框? Why not just show and hide it on the users request? 为什么不在用户请求中显示和隐藏它? This sounds like it will make your problem at least a little bit simpler. 这听起来会让你的问题至少更简单一些。

Why not just hide the dialog when the user dismisses it? 为什么不在用户解散时隐藏对话框? That should work fine if you don't show that dialog modally. 如果你没有以模态方式显示该对话框,那应该可以正常工作。 (use show instead of showdialog). (使用show而不是showdialog)。 I believe that you can keep your progress dialog on top of your owning window (if you need to) by passing the host to the dialog when you call show. 我相信您可以通过在调用show时将主机传递给对话框,将进度对话框保存在您拥有的窗口之上(如果需要)。

Using System.ComponentModel.ISynchronizeInvoke is nice when creating a System.ComponentModel.Component , such as the BackgroundWorker . 在创建System.ComponentModel.Component (例如BackgroundWorker时,使用System.ComponentModel.ISynchronizeInvoke非常好。 The following code snippet is how the FileSystemWater handles events. 以下代码片段是FileSystemWater处理事件的方式。

    ''' <summary>
    ''' Gets or sets the object used to marshal the event handler calls issued as a result of finding a file in a search.
    ''' </summary>
    <IODescription(SR.FSS_SynchronizingObject), DefaultValue(CType(Nothing, String))> _
    Public Property SynchronizingObject() As System.ComponentModel.ISynchronizeInvoke
        Get
            If (_synchronizingObject Is Nothing) AndAlso (MyBase.DesignMode) Then
                Dim oHost As IDesignerHost = DirectCast(MyBase.GetService(GetType(IDesignerHost)), IDesignerHost)
                If (Not (oHost Is Nothing)) Then
                    Dim oRootComponent As Object = oHost.RootComponent
                    If (Not (oRootComponent Is Nothing)) AndAlso (TypeOf oRootComponent Is ISynchronizeInvoke) Then
                        _synchronizingObject = DirectCast(oRootComponent, ISynchronizeInvoke)
                    End If
                End If
            End If
            Return _synchronizingObject
        End Get
        Set(ByVal Value As System.ComponentModel.ISynchronizeInvoke)
            _synchronizingObject = Value
        End Set
    End Property

    Private _onStartupHandler As EventHandler

    Protected Sub OnStartup(ByVal e As EventArgs)
        If ((Not Me.SynchronizingObject Is Nothing) AndAlso Me.SynchronizingObject.InvokeRequired) Then
            Me.SynchronizingObject.BeginInvoke(_onStartupHandler, New Object() {Me, e})
        Else
            _onStartupHandler.Invoke(Me, e)
        End If
    End Sub

Here's what I'm currently using. 这是我目前正在使用的。 It's based on the use of SynchronizationContext, and was inspired by JaredPar's blog article - see his answer above. 它基于SynchronizationContext的使用,并受到JaredPar博客文章的启发 - 请参阅上面的回答。 This may not be perfect, but it does avoid some of the OP's problems that I was also experiencing. 这可能不完美,但它确实避免了我也遇到的一些OP问题。

   // Homemade Action-style delegates to provide .Net 2.0 compatibility, since .Net 2.0 does not 
   //  include a non-generic Action delegate nor Action delegates with more than one generic type 
   //  parameter. (The DMethodWithOneParameter<T> definition is not needed, could be Action<T> 
   //  instead, but is defined for consistency.) Some interesting observations can be found here:
   //  http://geekswithblogs.net/BlackRabbitCoder/archive/2011/11/03/c.net-little-wonders-the-generic-action-delegates.aspx
   public delegate void DMethodWithNoParameters();
   public delegate void DMethodWithOneParameter<T>(T parameter1);
   public delegate void DMethodWithTwoParameters<T1, T2>(T1 parameter1, T2 parameter2);
   public delegate void DMethodWithThreeParameters<T1, T2, T3>(T1 parameter1, T2 parameter2, T3 parameter3);


   /// <summary>
   /// Class containing support code to use the SynchronizationContext mechanism to dispatch the 
   /// execution of a method to the WinForms UI thread, from another thread. This can be used as an 
   /// alternative to the Control.BeginInvoke() mechanism which can be problematic under certain 
   /// conditions. See for example the discussion here:
   /// http://stackoverflow.com/questions/1364116/avoiding-the-woes-of-invoke-begininvoke-in-cross-thread-winform-event-handling
   ///
   /// As currently coded this works with methods that take zero, one, two or three arguments, but 
   /// it is a trivial job to extend the code for methods taking more arguments.
   /// </summary>
   public class WinFormsHelper
   {
      // An arbitrary WinForms control associated with thread 1, used to check that thread-switching 
      //  with the SynchronizationContext mechanism should be OK
      private readonly Control _thread1Control = null;

      // SynchronizationContext for the WinForms environment's UI thread
      private readonly WindowsFormsSynchronizationContext _synchronizationContext;


      /// <summary>
      /// Constructor. This must be called on the WinForms UI thread, typically thread 1. (Unless 
      /// running under the Visual Studio debugger, then the thread number is arbitrary.)
      ///
      /// The provided "thread 1 control" must be some WinForms control that will remain in 
      /// existence for as long as this object is going to be used, for example the main Form 
      /// control for the application.
      /// </summary>
      /// <param name="thread1Control">see above</param>
      public WinFormsHelper(Control thread1Control)
      {
         _thread1Control = thread1Control;
         if (thread1Control.InvokeRequired)
            throw new Exception("Not called on thread associated with WinForms controls.");

         _synchronizationContext =
                            SynchronizationContext.Current as WindowsFormsSynchronizationContext;
         if (_synchronizationContext == null) // Should not be possible?
            throw new Exception("SynchronizationContext.Current = null or wrong type.");
      }


      // The following BeginInvoke() methods follow a boilerplate pattern for how these methods 
      // should be implemented - they differ only in the number of arguments that the caller wants 
      // to provide.

      public void BeginInvoke(DMethodWithNoParameters methodWithNoParameters)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithNoParameters();
         }, null);
      }


      public void BeginInvoke<T>(DMethodWithOneParameter<T> methodWithOneParameter, T parameter1)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithOneParameter(parameter1);
         }, null);
      }


      public void BeginInvoke<T1, T2>(DMethodWithTwoParameters<T1, T2> methodWithTwoParameters,
                                      T1 parameter1, T2 parameter2)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithTwoParameters(parameter1, parameter2);
         }, null);
      }


      public void BeginInvoke<T1, T2, T3>(DMethodWithThreeParameters<T1, T2, T3> methodWithThreeParameters,
                                          T1 parameter1, T2 parameter2, T3 parameter3)
      {
         _synchronizationContext.Post((object stateNotUsed) =>
         {
            if (!_thread1Control.IsDisposed)
               methodWithThreeParameters(parameter1, parameter2, parameter3);
         }, null);
      }
   }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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