使用WebClient.DownloadFileAsync时如何处理异常

How to deal with exceptions when using WebClient.DownloadFileAsync

我正在使用 WebClient 通过以下方式从 Internet 下载一些文件:

try  
{
   ManualResetEvent mr = new ManualResetEvent(false);
   mr.Reset();
   using (WebClient wc = new WebClient())
   {
          wc.DownloadFileCompleted += ((sender, args) =>
          {
               if (args.Error == null)
               {
                    File.Move(filePath, Path.ChangeExtension(filePath, ".jpg"));
                    mr.Set();
               }
               else
               {
                  //how to pass args.Error?
               }
           });
           wc.DownloadFileAsync(new Uri(string.Format("{0}/{1}", Settings1.Default.WebPhotosLocation, Path.GetFileName(f.FullName))), filePath);
           mr.WaitOne();
    }
}
catch (Exception ex)
{
   //Catch my error here and handle it (display message box)
}

但我似乎无法将错误从我的匿名 DownloadFileCompleted 方法传递到我的主要捕获。正确的做法是什么?

你应该使用 awaitDownloadFileTaskAsync:

try  
{

   using (WebClient wc = new WebClient())
   {

         await  wc.DownloadFileTaskAsync(new Uri(string.Format("{0}/{1}", Settings1.Default.WebPhotosLocation, Path.GetFileName(f.FullName))), filePath);

    }
}
catch (Exception ex)
{
   //Catch my error here and handle it (display message box)
}

DownloadFileAsync 使用 Event-based Asynchronous Pattern, you can't catch the exception, you can get exception throw AsyncCompletedEventArgs.Error 属性

您可以做的是创建一个任务(异步操作)并使用 ContinueWith 指令来处理异常。这可能有点不可读

using (WebClient wc = new WebClient())
{
    wc.DownloadFileCompleted += ((sender, args) =>
    {
        if (args.Error == null)
        {
            File.Move(filePath, Path.ChangeExtension(filePath, ".jpg"));
            mr.Set();
        }
        else
        {
            //how to pass args.Error?
        }
    });
    Task.Factory.StartNew(() => wc.DownloadFile(
                                 new Uri(string.Format("{0}/{1}",
                                 Settings1.Default.WebPhotosLocation,
                                 Path.GetFileName(f.FullName))), filePath))
                .ContinueWith(t => Console.WriteLine(t.Exception.Message));
}

但是,随着 .NET 4.5 的引入,Webclient 为您公开了基于任务的异步下载!

using (WebClient wc = new WebClient())
{
    wc.DownloadFileCompleted += ((sender, args) =>
    {
        if (args.Error == null)
        {
            File.Move(filePath, Path.ChangeExtension(filePath, ".jpg"));
            mr.Set();
        }
        else
        {
            //how to pass args.Error?
        }
    });
    wc.DownloadFileTaskAsync(new Uri(string.Format("{0}/{1}",
                                 Settings1.Default.WebPhotosLocation,
                                 Path.GetFileName(f.FullName))),
                                 filePath)
      .ContinueWith(t => t.Exception.Message)
}

重新抛出的错误解决方案

您可以将异常保存在 lambda 外部定义的一些变量中。然后可以重新抛出:

Exception exc = null;
using (WebClient wc = new WebClient())
{
      wc.DownloadFileCompleted += ((sender, args) =>
      ...

      mr.WaitOne();

      if (exception != null) throw exception;
}

为什么不好?因为您将丢失堆栈跟踪(它将显示异常是在当前方法中抛出的,而不是在 WebClient 中抛出的)。不过,如果您不需要或不关心堆栈跟踪,这是可能的解决方案。

就地处理异常

您也可以只创建一些方法来处理外部 try-catch 和下载的处理程序中的异常:

void HandleWebClientException(Exception exc)
{
    ...
}

try  
{
   ManualResetEvent mr = new ManualResetEvent(false);
   mr.Reset();
   using (WebClient wc = new WebClient())
   {
          wc.DownloadFileCompleted += ((sender, args) =>
          {
               if (args.Error == null)
               {
                    File.Move(filePath, Path.ChangeExtension(filePath, ".jpg"));
                    mr.Set();
               }
               else
               {
                  HandleWebClientException(args.Error);
               }
           });
           wc.DownloadFileAsync(new Uri(string.Format("{0}/{1}", Settings1.Default.WebPhotosLocation, Path.GetFileName(f.FullName))), filePath);
           mr.WaitOne();
    }
}
catch (Exception ex)
{
   HandleWebClientException(ex);
}

做对了

最好的办法是避免 WebClient, because you can't await on them or apply some continuation.

上的 void 方法

这些方法在某种意义上很方便,但它们迫使您使用具有同步结构的秘密解决方案,以减少工作流对不同回调的依赖。

要使用 async-await,您必须应用 public Task<byte[]> DownloadDataTaskAsync(Uri address) 方法。

您可以:

1. await 它获取数据的字节数组以便稍后手动保存,但需要在您的应用程序中进行大量返工才能使其成为 async all the way:

public async Task LoadFile()
{
    try
    {
        using (WebClient wc = new WebClient())
        {
            var bytes = await wc.DownloadDataTaskAsync(new Uri(string.Format("{0}/{1}", Settings1.Default.WebPhotosLocation, Path.GetFileName(f.FullName))), filePath);
            System.IO.File.WriteAllBytes(bytes); // Probably turn it into async too
        }                    
    }
    catch (Exception ex)
    {
        //Catch my error here and handle it (display message box)
    }
}

它会起作用,但我不确定 DownloadDataTaskAsync 是否是真正的异步方法。

2. 所以你也可以考虑用Task Continuations,方法一样:

public Task LoadFile()
{
    Task<Byte[]> bytesTask = wc.DownloadDataTaskAsync(new Uri(string.Format("{0}/{1}", Settings1.Default.WebPhotosLocation, Path.GetFileName(f.FullName))), filePath);

    var success = bytesTask.ContinueWith((prev) =>
        {
            System.IO.File.WriteAllBytes(prev.Result); 
        },
        TaskContinuationOptions.OnlyOnRanToCompletion);


    var failure = bytesTask.ContinueWith(prev =>
        {
            MessageBox.Show //...
        },
        TaskContinuationOptions.OnlyOnFaulted);

    return Task.WhenAny(success, failure);
}

P.S.: 如果不需要异步加载文件,为什么不使用简单的阻塞方法public void DownloadFile(Uri address, string fileName)