BackgroundWorker.RunWorkCompleted - 无法重新抛出异常

4
我正在编写一种WCF调用的包装器(使用BackgroundWorker),以保持GUI在调用正在进行时不会冻结。它通常能够正常工作,但是我在BackgroundWorker出现问题,特别是当WCF调用引发异常时。如果DoWork中出现异常,我可以在RunWorkCompleted中检测到它,但是重新抛出到GUI并不起作用。我已经阅读了许多关于这应该如何工作的线程。

下面是包装器的代码(请注意,WCF调用由抛出的异常表示):
private void GetSomething(Action<IEnumerable<int>> completedAction)
{
    BackgroundWorker b = new BackgroundWorker();

    b.DoWork += (s, evt) => { throw new Exception(); evt.Result = new List<int> { 1, 2, 3 }; };

    b.RunWorkerCompleted += (s, evt) =>
    {
        if (evt.Error == null && completedAction != null)
        {
           completedAction((IEnumerable<int>)evt.Result);
        }
        else if(evt.Error != null)
        {
           throw evt.Error;
        }
    };

    b.RunWorkerAsync();
}

在 Windows 表单中调用代码:

private void button3_Click(object sender, EventArgs e)
{
    try
    {
        GetSomething(list =>
        {
           foreach (int i in list)
           {
              listView1.Items.Add(new ListViewItem(i.ToString()));
           }
        });
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

在调试过程中,我收到了以下信息:

  1. 在DoWork中抛出了“System.Exception”类型的异常
  2. throw evt.Error处抛出了“System.Exception”类型的异常
  3. Main方法中的Application.Run(new Form1())处未处理“TargetInvocationException”异常

我做错了什么?我想在Windows窗体中捕获异常。


你应该在RunWorkerCompleted中处理异常。你重新抛出异常有什么特殊的原因吗? - Prabhu Murthy
@CodeIgnoto:我的想法是将这个决定推给调用者。我不想从这个封装器中使用消息框,比如说。它应该尽可能与GUI无关。 - MRT
根据我的经验,在RunWorkerCompleted代码中抛出的任何异常都会导致应用程序崩溃。因此,最好使用Tasks或在那里处理它。如果您在启动时使用while(true)Application.DoEvents()而不是Application.Run(),则可以在doevents周围进行try catch,并且它将捕获您刚刚重新抛出的异常。但是,某些代码可能无法按预期运行,具体取决于这种“消息”循环的类型。 - Wolf5
2个回答

1

您应该将此更改为:

throw evt.Error;

改为:

MessageBox.Show(evt.Error.Message);

您的异常目前未被处理,因为RunWorkerCompleted处理程序稍后才会运行。它不在您在button3_Click中使用的try/catch中运行。


0

b.RunWorkerCompleted 事件是您应该处理错误的地方。您可以传递一个 Action<Exception> 来处理错误,例如:

private void GetSomething(Action<IEnumerable<int>> completedAction, Action<Exception> exceptionAction)
{
    BackgroundWorker b = new BackgroundWorker();

    b.DoWork += (s, evt) => { throw new Exception(); evt.Result = new List<int> { 1, 2, 3 }; };

    b.RunWorkerCompleted += (s, evt) =>
    {
        if (evt.Error == null && completedAction != null)
            completedAction((IEnumerable<int>)evt.Result);
        else if(evt.Error != null)
            exceptionAction(evt.Error);
    };

    b.RunWorkerAsync();
}

然而这往往会变得很丑陋。如果您使用的是 .Net 4 或 4.5,您可以采用任务(Tasks)的方式。 Task<TResult> 就是为了解决这个问题而创建的:

Task<IEnumerable<int>> GetSomething()
{
    return Task.Factory.StartNew(() => { 
        Thread.Sleep(2000);
        throw new Exception(); 
        return (new List<int> { 1, 2, 3 }).AsEnumerable(); 
        });
}

Task 基本上是一个带有以下内容的信号结构:

  • 一个 .Result 属性
  • 一个 .Exception 属性
  • 一个 .ContinueWith() 方法

ContinueWith() 中,您可以检查 Task 是否处于故障状态(是否抛出异常)。

您可以像这样使用它:

    private void button3_Click(object sender, EventArgs e)
    {
        GetSomething()
            .ContinueWith(task =>
                {
                    if (task.IsCanceled)
                    {
                    }
                    else if (task.IsFaulted)
                    {
                        var ex = task.Exception.InnerException;
                        MessageBox.Show(ex.Message);
                    }
                    else if (task.IsCompleted)
                    {
                        var list = task.Result;
                        foreach (int i in list)
                        {
                            listView1.Items.Add(new ListViewItem(i.ToString()));
                        }
                    }
                });
    }

如果您使用的是 .Net 4.5 和 C#5(您需要 VS2012 或 VS2010 以及 Async CTP),您甚至可以使用 asyncawait
    private async void button3_Click(object sender, EventArgs e)
    {
        try
        {
            var list = await GetSomething();
            foreach (int i in list)
            {
                listView1.Items.Add(new ListViewItem(i.ToString()));
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }

...所有的魔法都是由编译器完成的。请注意,您可以像往常一样使用trycatch


再次打招呼。我现在试过了。第一个问题是“foreach(int i in list)”无法编译,因为GetSomething的返回类型是任务(Task)。将其更改为迭代task.Result后,在调用期间我的GUI会被阻塞,并且异常不会被捕获。 - MRT
抱歉,你说得完全正确。使用ContinueWith的例子是无意义的。我已经进行了更正。然而,如果你使用Task.Factory.StartNew(),GUI不应该被阻塞。 - PeterB
我会尝试这个,谢谢。实际上,我认为你的第一个解决方案通过传递一个Action<Exception>是最简单的。这将抽象出许多细节,使GUI更加简洁。 - MRT

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