回调UI线程时尽量避免异常

发布于 2024-11-02 18:01:36 字数 2648 浏览 0 评论 0原文

我有一种控件,其中包含 GridView 和一些实用程序按钮。该控件在我的应用程序中随处使用。它是通过委托异步填充的:

    protected virtual void PopulateGridView()
    {
        if (isPopulating) return;

        //a delegate given to the control by its parent form
        if (GetterMethod != null)
        {
            isPopulating = true;
            /*unimportant UI fluff here*/

            //some controls are fast enough to not have to mess with threading
            if(PopulateSynchronously) 
            {
                PopulateWithGetterMethod();
                InitializeGridView();
            }
            else //most aren't
            {
                Action asyncMethod = PopulateWithGetterMethod;
                asyncMethod.BeginInvoke(
                   ar => Invoke((MethodInvoker)InitializeGridView)), null);
            }
        }
    }

    private void PopulateWithGetterMethod()
    {
        //a list of whetever the control is displaying;
        //the control ancestor and this collection are generic.
        RetrievedInformation = GetterMethod();
    }

    protected virtual void InitializeGridView()
    {
        //use RetrievedInformation to repopulate the GridView;
        //implementation not important, except it touches UI elements,
        //so it needs to be called from the worker thread using Invoke.
    }

在长时间运行的查询中,有时用户会变得不耐烦并关闭窗口。或者,当其中一个控件基于计时器自动刷新时,用户会意外地关闭窗口。当发生这种情况并且查询确实完成时,回调委托中的 Invoke 调用将失败并出现 InvalidOperationException,因为该控件没有窗口句柄。

为了解决这个问题,我尝试使用内置的 IsHandleCreated 属性:

    ...
            else
            {
                Action asyncMethod = PopulateWithGetterMethod;
                asyncMethod.BeginInvoke(
                   ar => { if(IsHandleCreated)
                              Invoke((MethodInvoker)InitializeGridView)); 
                         }, null);
            }

但是,异常仍然发生,只是不那么频繁。我设法重现它,发现 Invoke 调用仍然发生,即使 IsHandleCreated 上的手表显示错误。我的猜测是,线程在检查和 Invoke 调用之间被抢占,就像您在引发事件委托之前检查事件委托是否为 null 时看到的那样。

我认为我仍然有选择,但我想知道最好的是什么:

  • 不仅检查 IsHandleCreated,还检查 Dispose,以确保控件确实处于活动状态并且运行良好,而不仅仅是即将被销毁。
  • 在进行检查之前执行 Thread.Yield(),以便操作系统有机会在检查句柄之前执行任何窗口管理。
  • 将 Invoke 调用包装在 try/catch 中,以抑制任何 InvalidOperationExceptions,或者至少抑制报告缺少窗口句柄的异常。老实说,在这种情况下,我不在乎 GridView 无法更新;用户关闭了窗口,所以显然他们不在乎。让线程静静地死去,而不需要关闭整个应用程序。

第三种选择似乎是一种逃避。必须有一种更清洁的方法来处理它。但是,我不确定另外两个是否能 100% 修复。

编辑:检查 Diswriting 和 IsDispose 也不起作用;我在 if 块中抛出了一个异常,条件为“IsHandleCreated && !Disusing && !IsDispose”,其中第一个和最后一个节点在观看时为 false。目前,我正在捕获所有异常,并显示消息“在创建窗口句柄之前,无法在控件上调用 Invoke 或 BeginInvoke。”,这是我希望不要做的事情。

I have a type of control that holds a GridView and some utility buttons. The control is used everywhere in my application. It is populated asynchronously, through delegates:

    protected virtual void PopulateGridView()
    {
        if (isPopulating) return;

        //a delegate given to the control by its parent form
        if (GetterMethod != null)
        {
            isPopulating = true;
            /*unimportant UI fluff here*/

            //some controls are fast enough to not have to mess with threading
            if(PopulateSynchronously) 
            {
                PopulateWithGetterMethod();
                InitializeGridView();
            }
            else //most aren't
            {
                Action asyncMethod = PopulateWithGetterMethod;
                asyncMethod.BeginInvoke(
                   ar => Invoke((MethodInvoker)InitializeGridView)), null);
            }
        }
    }

    private void PopulateWithGetterMethod()
    {
        //a list of whetever the control is displaying;
        //the control ancestor and this collection are generic.
        RetrievedInformation = GetterMethod();
    }

    protected virtual void InitializeGridView()
    {
        //use RetrievedInformation to repopulate the GridView;
        //implementation not important, except it touches UI elements,
        //so it needs to be called from the worker thread using Invoke.
    }

On long-running queries, sometimes the user would get impatient and close the window. Or, the user would serendipitously close a window when one of the controls was auto-refreshing based on a Timer. When that happened, and the query DID finish, the Invoke call in the callback delegate would fail with an InvalidOperationException because the control didn't have a window handle.

To fix this, I attempted to use the built-in IsHandleCreated property:

    ...
            else
            {
                Action asyncMethod = PopulateWithGetterMethod;
                asyncMethod.BeginInvoke(
                   ar => { if(IsHandleCreated)
                              Invoke((MethodInvoker)InitializeGridView)); 
                         }, null);
            }

However, the exception still happens, just not as often. I managed to reproduce it, and found that the Invoke call still happened, even though the watch on IsHandleCreated showed false. My guess is that the thread was pre-empted between the check and the Invoke call, like you'd see with checking an event delegate for null before raising it.

I still have options, I think, but I'm wondering what the best is:

  • Check not only IsHandleCreated, but Disposing, to make sure the control really is alive and well, and not JUST about to be destroyed.
  • Perform a Thread.Yield() before making the check, to allow the OS a chance to do any window management before checking for the handle.
  • Wrap the Invoke call in a try/catch that suppresses any InvalidOperationExceptions, or at least ones reporting the lack of a window handle. Honestly, in this case, I don't care that the GridView can't be updated; the user closed the window, so obviously they don't care. Let the thread die quietly, without taking down the whole app.

The third option seems like a cop-out; there has to be a cleaner way to handle it. But, I'm not sure that either of the other two will be a 100% fix.

EDIT: Checking Disposing and IsDisposed didn't work either; I got an exception thrown out from within an if block with the condition "IsHandleCreated && !Disposing && !IsDisposed", in which the first and last nodes were false when watched. Currently I'm trapping all exceptions with the message "Invoke or BeginInvoke cannot be called on a control until the window handle has been created.", which is what I'd hoped not to do.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

欢你一世 2024-11-09 18:01:36

是的,有一种 100% 干净的方法可以做到这一点:在允许窗体关闭之前终止线程。其他任何事情都是对令人讨厌的伤口的创可贴,检查表单是否仍然存在是不可避免的竞争条件,您无法解决。当您调用 Invoke() 时,您只能最小化表单出现奇闻趣事的可能性,而无法消除它们。

检查此答案以了解该模式。

Yes, there's a 100% clean way to do this: terminate the thread before you allow the form to close. Anything else is a band-aid over a nasty cut, checking if the form is still alive is an inevitable race condition that you cannot solve. You can only minimize the odds that the form is gonzo when you call Invoke(), you can't eliminate them.

Check this answer for the pattern.

旧人哭 2024-11-09 18:01:36

处理掉是你最好的选择;然而,我们每隔一段时间就会遇到同样的问题,有时 Dispose 调用会返回 false,但当我们尝试使用它已释放的控件时,即使是 3 行之后。

在这些情况下,我们发现最好捕获特定异常并验证异常文本包含我们正在查找的内容。如果它是一个例外并且是一个已知的例外,那么吞掉它并不是一个糟糕的主意。问题是要确保您只接受您正在寻找的特定异常。

Disposing is your best bet; however, we have this same problem every once in awhile and sometimes the Disposing call returns false but by the time we try to use the control it's disposed, even if it's 3 lines later.

In these cases, we've found it best to catch a specific exception and also verify the exception text contains what we're looking for. If it's an exception and it's a known exception, then swallowing it isn't a terrible idea. The thing is to make sure you're only swallowing the specific exception you're looking for.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文