C# .Net DownloadFileTaskAsync健壮WPF代码

C# .Net DownloadFileTaskAsync健壮WPF代码,c#,wpf,asynchronous,webclient,C#,Wpf,Asynchronous,Webclient,当网络连接丢失3分钟或更长时间时,下面的WPF代码将永久挂起。当连接恢复时,它既不会抛出也不会继续下载,也不会超时。若网络连接丢失的时间较短,比如半分钟,它会在连接恢复后抛出。如何使其更健壮,以在网络中断后生存 using System; using System.Net; using System.Net.NetworkInformation; using System.Windows; namespace WebClientAsync { public partial class

当网络连接丢失3分钟或更长时间时,下面的WPF代码将永久挂起。当连接恢复时,它既不会抛出也不会继续下载,也不会超时。若网络连接丢失的时间较短,比如半分钟,它会在连接恢复后抛出。如何使其更健壮,以在网络中断后生存

using System;
using System.Net;
using System.Net.NetworkInformation;
using System.Windows;

namespace WebClientAsync
{

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            NetworkChange.NetworkAvailabilityChanged +=
                (sender, e) => Dispatcher.Invoke(delegate()
                    {
                        this.Title = "Network is " + (e.IsAvailable ? " available" : "down");
                    });
        }

        const string SRC = "http://ovh.net/files/10Mio.dat";
        const string TARGET = @"d:\stuff\10Mio.dat";

        private async void btnDownload_Click(object sender, RoutedEventArgs e)
        {
            btnDownload.IsEnabled = false;
            btnDownload.Content = "Downloading " + SRC;
            try {
                using (var wcl = new WebClient())
                {
                    wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
                    await wcl.DownloadFileTaskAsync(new Uri(SRC), TARGET);
                    btnDownload.Content = "Downloaded";
                }
            }
            catch (Exception ex)
            {
                btnDownload.Content = ex.Message + Environment.NewLine
                    + ((ex.InnerException != null) ? ex.InnerException.Message : String.Empty);
            }
            btnDownload.IsEnabled = true;
        }
    }
}
更新

当前解决方案基于在
DownloadProgressChangedEventHandler
中重新启动
计时器
,因此只有在超时时间内没有发生DownloadProgressChanged事件时,计时器才会触发。看起来像一个丑陋的黑客,仍在寻找更好的解决方案

using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace WebClientAsync
{

    public partial class MainWindow : Window
    {

        const string SRC = "http://ovh.net/files/10Mio.dat";
        const string TARGET = @"d:\stuff\10Mio.dat";
        // Time needed to restore network connection
        const int TIMEOUT = 30 * 1000;

        public MainWindow()
        {
            InitializeComponent();
        }

        private async void btnDownload_Click(object sender, RoutedEventArgs e)
        {
            btnDownload.IsEnabled = false;
            btnDownload.Content = "Downloading " + SRC;
            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken token = cts.Token;
            Timer timer = new Timer((o) =>
                {
                    // Force async cancellation
                    cts.Cancel();
                }
                , null //state
                , TIMEOUT
                , Timeout.Infinite // once
            );
            DownloadProgressChangedEventHandler handler = (sa, ea) =>
                {
                    // Restart timer
                    if (ea.BytesReceived < ea.TotalBytesToReceive && timer != null)
                    {
                        timer.Change(TIMEOUT, Timeout.Infinite);
                    }

                };
            btnDownload.Content = await DownloadFileTA(token, handler);
            // Note ProgressCallback will fire once again after awaited.
            timer.Dispose();
            btnDownload.IsEnabled = true;
        }

        private async Task<string> DownloadFileTA(CancellationToken token, DownloadProgressChangedEventHandler handler)
        {
            string res = null;
            WebClient wcl = new WebClient();
            wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
            wcl.DownloadProgressChanged += handler;
            try
            {
                using (token.Register(() => wcl.CancelAsync()))
                {
                    await wcl.DownloadFileTaskAsync(new Uri(SRC), TARGET);
                }
                res = "Downloaded";
            }
            catch (Exception ex)
            {
                res = ex.Message + Environment.NewLine
                    + ((ex.InnerException != null) ? ex.InnerException.Message : String.Empty);
            }
            wcl.Dispose();
            return res;
        }
    }
}
使用系统;
Net系统;
使用系统线程;
使用System.Threading.Tasks;
使用System.Windows;
命名空间WebClientAsync
{
公共部分类主窗口:窗口
{
常量字符串SRC=”http://ovh.net/files/10Mio.dat";
常量字符串目标=@“d:\stuff\10Mio.dat”;
//恢复网络连接所需的时间
const int TIMEOUT=30*1000;
公共主窗口()
{
初始化组件();
}
私有异步void btnDownload\u单击(对象发送方,路由目标)
{
btnDownload.IsEnabled=false;
btnDownload.Content=“下载”+SRC;
CancellationTokenSource cts=新的CancellationTokenSource();
CancellationToken=cts.token;
计时器=新计时器((o)=>
{
//强制异步取消
cts.Cancel();
}
,null//state
,超时
,Timeout.Infinite//一次
);
DownloadProgressChangedEventHandler=(sa,ea)=>
{
//重新启动计时器
if(ea.bytesservedwcl.CancelAsync())
{
等待wcl.DownloadFileTaskAsync(新Uri(SRC),目标);
}
res=“下载”;
}
捕获(例外情况除外)
{
res=ex.Message+Environment.NewLine
+((ex.InnerException!=null)?ex.InnerException.Message:String.Empty);
}
wcl.Dispose();
返回res;
}
}
}

您需要为该下载执行适当的超时。但您不需要使用计时器,只需使用
Task.Delay
Task.WaitAny即可。例如:

static async Task DownloadFile(string url, string output, TimeSpan timeout) {            
    using (var wcl = new WebClient())
    {
        wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;                                                
        var download = wcl.DownloadFileTaskAsync(url, output);
        // await two tasks - download and delay, whichever completes first
        await Task.WhenAny(Task.Delay(timeout), download);
        var exception = download.Exception; // need to observe exception, if any
        bool cancelled = !download.IsCompleted && exception == null;

        // download is not completed yet, nor it is failed - cancel
        if (cancelled) {
            wcl.CancelAsync();
        }

        if (cancelled || exception != null) {
            // delete partially downloaded file if any (note - need to do with retry, might not work with a first try, because CancelAsync is not immediate)
            int fails = 0;
            while (true) {
                try {
                    File.Delete(output);
                    break;
                }
                catch {
                    fails++;
                    if (fails >= 10)
                        break;

                    await Task.Delay(1000);
                }
            }
        }
        if (exception != null) {
            throw new Exception("Failed to download file", exception);
        }
        if (cancelled) {
            throw new Exception($"Failed to download file (timeout reached: {timeout})");
        }
    }
}
static async Task DownloadFile(string url, string output, TimeSpan timeout)
{
    using (var wcl = new WebClient())
    {
        wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
        DateTime? lastReceived = null;
        wcl.DownloadProgressChanged += (o, e) =>
        {
            lastReceived = DateTime.Now;
        };
        var download = wcl.DownloadFileTaskAsync(url, output);
        // await two tasks - download and delay, whichever completes first
        // do that until download fails, completes, or timeout expires
        while (lastReceived == null || DateTime.Now - lastReceived < timeout) {
            await Task.WhenAny(Task.Delay(1000), download); // you can replace 1 second with more reasonable value
            if (download.IsCompleted || download.IsCanceled || download.Exception != null)
                break;
        }
        var exception = download.Exception; // need to observe exception, if any
        bool cancelled = !download.IsCompleted && exception == null;

        // download is not completed yet, nor it is failed - cancel
        if (cancelled)
        {
            wcl.CancelAsync();
        }

        if (cancelled || exception != null)
        {
            // delete partially downloaded file if any (note - need to do with retry, might not work with a first try, because CancelAsync is not immediate)
            int fails = 0;
            while (true)
            {
                try
                {
                    File.Delete(output);
                    break;
                }
                catch
                {
                    fails++;
                    if (fails >= 10)
                        break;

                    await Task.Delay(1000);
                }
            }
        }
        if (exception != null)
        {
            throw new Exception("Failed to download file", exception);
        }
        if (cancelled)
        {
            throw new Exception($"Failed to download file (timeout reached: {timeout})");
        }
    }
}
用法:

const string SRC = "http://ovh.net/files/10Mio.dat";
const string TARGET = @"d:\stuff\10Mio.dat";
// Time needed to restore network connection
TimeSpam TIMEOUT = TimeSpan.FromSeconds(30);
DownloadFile(SRC,TARGET, TIMEOUT); // might want to await this to handle exceptions
更新以回应评论。如果您希望超时基于接收到的数据,而不是整个操作时间,也可以使用
Task.Delay
。例如:

static async Task DownloadFile(string url, string output, TimeSpan timeout) {            
    using (var wcl = new WebClient())
    {
        wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;                                                
        var download = wcl.DownloadFileTaskAsync(url, output);
        // await two tasks - download and delay, whichever completes first
        await Task.WhenAny(Task.Delay(timeout), download);
        var exception = download.Exception; // need to observe exception, if any
        bool cancelled = !download.IsCompleted && exception == null;

        // download is not completed yet, nor it is failed - cancel
        if (cancelled) {
            wcl.CancelAsync();
        }

        if (cancelled || exception != null) {
            // delete partially downloaded file if any (note - need to do with retry, might not work with a first try, because CancelAsync is not immediate)
            int fails = 0;
            while (true) {
                try {
                    File.Delete(output);
                    break;
                }
                catch {
                    fails++;
                    if (fails >= 10)
                        break;

                    await Task.Delay(1000);
                }
            }
        }
        if (exception != null) {
            throw new Exception("Failed to download file", exception);
        }
        if (cancelled) {
            throw new Exception($"Failed to download file (timeout reached: {timeout})");
        }
    }
}
static async Task DownloadFile(string url, string output, TimeSpan timeout)
{
    using (var wcl = new WebClient())
    {
        wcl.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
        DateTime? lastReceived = null;
        wcl.DownloadProgressChanged += (o, e) =>
        {
            lastReceived = DateTime.Now;
        };
        var download = wcl.DownloadFileTaskAsync(url, output);
        // await two tasks - download and delay, whichever completes first
        // do that until download fails, completes, or timeout expires
        while (lastReceived == null || DateTime.Now - lastReceived < timeout) {
            await Task.WhenAny(Task.Delay(1000), download); // you can replace 1 second with more reasonable value
            if (download.IsCompleted || download.IsCanceled || download.Exception != null)
                break;
        }
        var exception = download.Exception; // need to observe exception, if any
        bool cancelled = !download.IsCompleted && exception == null;

        // download is not completed yet, nor it is failed - cancel
        if (cancelled)
        {
            wcl.CancelAsync();
        }

        if (cancelled || exception != null)
        {
            // delete partially downloaded file if any (note - need to do with retry, might not work with a first try, because CancelAsync is not immediate)
            int fails = 0;
            while (true)
            {
                try
                {
                    File.Delete(output);
                    break;
                }
                catch
                {
                    fails++;
                    if (fails >= 10)
                        break;

                    await Task.Delay(1000);
                }
            }
        }
        if (exception != null)
        {
            throw new Exception("Failed to download file", exception);
        }
        if (cancelled)
        {
            throw new Exception($"Failed to download file (timeout reached: {timeout})");
        }
    }
}
静态异步任务下载文件(字符串url、字符串输出、时间跨度超时)
{
使用(var wcl=new WebClient())
{
wcl.Credentials=System.Net.CredentialCache.DefaultNetworkCredentials;
DateTime?lastReceived=null;
wcl.DownloadProgressChanged+=(o,e)=>
{
lastReceived=DateTime.Now;
};
var download=wcl.DownloadFileTaskAsync(url,输出);
//等待两个任务-下载和延迟,以先完成者为准
//直到下载失败、完成或超时为止
while(lastReceived==null | | DateTime.Now-lastReceived=10)
打破
等待任务。延迟(1000);
}
}
}
if(异常!=null)
{
抛出新异常(“下载文件失败”,异常);
}
如果(取消)
{
抛出新异常($“未能下载文件(达到超时:{timeout})”;
}
}
}

就我个人而言,如果我要制作一个健壮的下载解决方案,我会添加一个网络连接监视器,因为这正是我们真正在等待的。为了简单起见,像这样的东西就足够了

online = true;

NetworkChange.NetworkAvailabilityChanged += NetworkChange_NetworkAvailabilityChanged;
_isNetworkOnline = NetworkInterface.GetIsNetworkAvailable();

void NetworkChange_NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
{
    online  = e.IsAvailable;
}
然后,您可以实际检查网络可用性,并根据需要等待,然后再尝试下载或继续。。。我一定会接受这一点