等待下载完成时取消 WebClient 下载

Met*_*urf 0 c# webclient cancellation

寻找一种更普遍接受的等待模式WebClient

  • 下载文件(可能需要几百毫秒或几分钟)
  • 等待下载完成后再执行任何其他工作
  • 定期检查另一个类的标志 (bool) 并根据需要取消下载(无法修改此类)

限制条件:

  • 不能使用 async/await 除非它是类似的Task.Run(async () => await method())
  • Download调用该方法时,它只需要像普通方法一样返回字符串
  • 可以使用 .Net 4.5 和 Roslyn 编译器的任何功能
  • 是否使用WebClient.DownloadFileTaskAsync或 都没有区别;DownloadFileAsync只需能够根据需要使用取消下载WebClient

当前的实现似乎有效,但似乎不太正确。是否有比使用while循环并在使用时Thread.Sleep定期检查更普遍可接受的替代方案?otherObject.ShouldCancelWebClient

private string Download(string url)
{
    // setup work
    string fileName = GenerateFileName();

    // download file
    using (var wc = new WebClient()) 
    {
        wc.DownloadFileCompleted += OnDownloadCompleted

        Task task = wc.DownloadFileTaskAsync(url, fileName);

        // Need to wait until either the download is completed
        // or download is canceled before doing any other work
        while (wc.IsBusy || task.Status == TaskStatus.WaitingForActivation) 
        {
            if (otherObject.ShouldCancel) 
            {
                wc.CancelAsync();
                break;
            }

            Thread.Sleep(100);
        }

        void OnDownloadCompleted(object obj, AsyncCompletedEventArgs args)
        {
            if(args.Cancelled)
            {
                // misc work
                return;
            }

            // misc work (different than other work below)
        }
    }

    // Other work after downloading, regardless of cancellation.
    // Could include in OnDownloadCompleted as long as this
    // method blocked until all work was complete

    return fileName;
}
Run Code Online (Sandbox Code Playgroud)

小智 5

我希望这会有所帮助。基本上你的包装器使用cancelToken.Register(webClient.Cancel);注册一个回调。一旦调用cancelToken.Cancel(),异步任务应该抛出一个异常,您可以按如下方式处理该异常:

public class Client
{
    public async Task<string> DownloadFileAsync(string url, string outputFileName, CancellationToken cancellationToken)
    {
        using (var webClient = new WebClient())
        {
            cancellationToken.Register(webClient.CancelAsync);
            
            try
            {
                var task = webClient.DownloadFileTaskAsync(url, outputFileName);

                await task; // This line throws an exception when cancellationTokenSource.Cancel() is called.
            }
            catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled)
            {
                throw new OperationCanceledException();
            }
            catch (AggregateException ex) when (ex.InnerException is WebException exWeb && exWeb.Status == WebExceptionStatus.RequestCanceled)
            {
              throw new OperationCanceledException();
            }
            catch (TaskCanceledException)
            {
                throw new OperationCanceledException();
            }

            return outputFileName;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

尝试此示例的简单方法

    private async static void DownloadFile()
    {
        var cancellationTokenSource = new CancellationTokenSource();
        var client = new Client();

        var task = client.DownloadFileAsync("url",
            "output.exe", cancellationTokenSource.Token);

        cancellationTokenSource.Token.WaitHandle.WaitOne(TimeSpan.FromSeconds(5));

        cancellationTokenSource.Cancel();

        try
        {
            var result = await task;
        }
        catch (OperationCanceledException)
        {
            // Operation Canceled
        }
    }
Run Code Online (Sandbox Code Playgroud)

在更现实的场景中,cancelTokenSource.Cancel() 将由由于用户交互或回调而引发的事件调用。

更新

另一种方法是订阅 DownloadProgressChanged 事件并在调用回调时检查 otherObject.ShouldCancel。

这是一个例子:

public class Client
{
    public string Download(string url)
    {
        // setup work
        string fileName = GenerateFileName();

        // download file
        using (var wc = new WebClient())
        {
            wc.DownloadProgressChanged += OnDownloadProgressChanged;
            wc.DownloadFileCompleted += OnDownloadFileCompleted;

            DownloadResult downloadResult = DownloadResult.CompletedSuccessfuly;

            void OnDownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
            {
                if (otherObject.ShouldCancel)
                {
                    ((WebClient)sender).CancelAsync();
                }
            }

            void OnDownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
            {
                if (e.Cancelled)
                {
                    downloadResult = DownloadResult.Cancelled;
                    return;
                }

                if (e.Error != null)
                {
                    downloadResult = DownloadResult.ErrorOccurred;
                    return;
                }
            }

            try
            {
                Task task = wc.DownloadFileTaskAsync(url, fileName);
                task.Wait();
            }
            catch (AggregateException ex)
            {
            }

            switch (downloadResult)
            {
                case DownloadResult.CompletedSuccessfuly:

                    break;
                case DownloadResult.Cancelled:

                    break;
                case DownloadResult.ErrorOccurred:

                    break;
            }
        }

        // Other work after downloading, regardless of cancellation.
        // Could include in OnDownloadCompleted as long as this
        // method blocked until all work was complete

        return fileName;
    }
}

public enum DownloadResult
{
    CompletedSuccessfuly,
    Cancelled,
    ErrorOccurred
}
Run Code Online (Sandbox Code Playgroud)