2

development environment:

C#, visual studio 2010 (.net 4.0), win7 x64

codes in winform project:

private void Form1_Load(object sender, EventArgs e)
    {
        string path = "c:\\1.jpg";
        for (int i = 0; i < 10; i++)
        {
            string url = "http://...." + i.ToString() + ".jpg";//i'm sure the http file does exist
            
            using (WebClient wc = new WebClient())
            {                     
                wc.DownloadFileCompleted += new AsyncCompletedEventHandler(wc_DownloadFileCompleted);
                wc.DownloadFileAsync(new Uri(url), path);
                
                Thread.Sleep(3000);//i'm sure the download will be finished in 3s

                WriteLog("C:\\1.log", "main function\r\n");
                
            }  
        }
    }

    static void wc_DownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
    {
        WriteLog("C:\\1.log", "callback function\r\n");
    }

    static void WriteLog(string LogName, string log)
    {
        StreamWriter sw = new StreamWriter(LogName, true);

        if (sw == null)
            return;

        sw.Write(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss") + " " + log);
        
        sw.Close();
    }

then the log will be :

2017/11/03 19:04:48 main function

2017/11/03 19:04:51 main function

2017/11/03 19:04:54 main function

2017/11/03 19:04:57 main function

2017/11/03 19:05:00 main function

2017/11/03 19:05:03 main function

2017/11/03 19:05:06 main function

2017/11/03 19:05:09 main function

2017/11/03 19:05:12 main function

2017/11/03 19:05:15 main function

2017/11/03 19:05:15 callback function

2017/11/03 19:05:15 callback function

2017/11/03 19:05:15 callback function

2017/11/03 19:05:15 callback function

2017/11/03 19:05:15 callback function

2017/11/03 19:05:15 callback function

2017/11/03 19:05:15 callback function

2017/11/03 19:05:15 callback function

2017/11/03 19:05:15 callback function

2017/11/03 19:05:15 callback function

if same codes in ConsoleApplication:

static void Main(string[] args)
    {
        string path = "c:\\1.jpg";
        for (int i = 0; i < 10; i++)
        {
            string url = "http://...." + i.ToString() + ".jpg";//i'm sure the http file does exist

            using (WebClient wc= new WebClient())
            {
                wc.DownloadFileCompleted += new AsyncCompletedEventHandler(wc_DownloadFileCompleted);
                wc.DownloadFileAsync(new Uri(url), path);

                Thread.Sleep(3000);//i'm sure the download will be finished in 3s

                WriteLog("C:\\1.log", "main function\r\n");

            }
        }
    }

    static void wc_DownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
    {
        WriteLog("C:\\1.log", "callback function\r\n");
    }

    static void WriteLog(string LogName, string log)
    {
        StreamWriter sw = new StreamWriter(LogName, true);

        if (sw == null)
            return;

        sw.Write(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss") + " " + log);

        sw.Close();
    }

then the log will be:

2017/11/03 19:13:50 callback function

2017/11/03 19:13:52 main function

2017/11/03 19:13:53 callback function

2017/11/03 19:13:55 main function

2017/11/03 19:13:56 callback function

2017/11/03 19:13:58 main function

2017/11/03 19:13:59 callback function

2017/11/03 19:14:01 main function

2017/11/03 19:14:02 callback function

2017/11/03 19:14:04 main function

2017/11/03 19:14:05 callback function

2017/11/03 19:14:08 main function

2017/11/03 19:14:08 callback function

2017/11/03 19:14:11 main function

2017/11/03 19:14:11 callback function

2017/11/03 19:14:14 main function

2017/11/03 19:14:14 callback function

2017/11/03 19:14:17 main function

2017/11/03 19:14:17 callback function

2017/11/03 19:14:20 main function

Obviously, the second result is right.

But in the first project, why the DownloadFileCompleted event isn't called untill all the downloads are finished?

How to call the DownloadFileCompleted event immediately after each download is done?

Community
  • 1
  • 1
JQY
  • 91
  • 3
  • Actually both are right :) – BugFinder Nov 03 '17 at 11:45
  • It seems the DownloadFileCompleted callback was blocked by something in the first project, and the result is not what i want. How to get the second result in a winform project? – JQY Nov 03 '17 at 11:49
  • @JQY why is your `wc_DownloadFileCompleted` handler static in winform project? – SᴇM Nov 03 '17 at 11:51
  • Actually when I added a Console.ReadKey at the end to keep the console open - it barfed saying the log file was in use. Changing the behavior again.. – BugFinder Nov 03 '17 at 12:04
  • 1
    The DownloadFileCompleted event will make an effort to keep your event handler thread-safe. So you can do stuff in that event handler that won't crash your program, like updating controls. It can do that in a Winforms app, thanks to a non-null SynchronizationContext.Current, it cannot do that in a console mode app. Consequence is that as long as you make the UI thread go catatonic on a Thread.Sleep() call and a for-loop, nothing will happen. Until your Load event handler completes, now those events rapidly fire. Feature, not a bug. Never hang the UI thread. – Hans Passant Nov 03 '17 at 13:26
  • @SeM - ՍեՄ Because in the console project wc_DownloadFileCompleted should be static, so I copy that to the winform project in order to keep the codes same. Actually I'm not very familiar with the static type in C# o(╯□╰)o – JQY Nov 04 '17 at 09:48
  • 1
    @Hans Passant thank you for your detailed reply, it's very helpful for me to find out where the problem is. – JQY Nov 04 '17 at 10:12

2 Answers2

2

I think the problem is that the DownloadFileCompleted event is put into the same event queue as the Load event of the form, so it cannot be handled until Form1_Load completes.

In general, blocking the UI thread (like sleeping in Form1_Load) is a bad practice. Time-consuming code should be run in a background thread. Below is one way to do that:

private void Form1_Load(object sender, EventArgs e)
{
    new Thread(() =>
    {
        string path = "c:\\1.jpg";
        for (int i = 0; i < 10; i++)
        {
            string url = "http://...." + i.ToString() + ".jpg";//i'm sure the http file does exist

            using (WebClient wc = new WebClient())
            {
                wc.DownloadFileCompleted += new AsyncCompletedEventHandler(wc_DownloadFileCompleted);
                wc.DownloadFileAsync(new Uri(url), path);

                Thread.Sleep(3000);//i'm sure the download will be finished in 3s

                WriteLog("C:\\1.log", "main function\r\n");

            }
        }
    }).Start();
}

Using this method, the background thread will not exit even after the form is closed (and the application will terminate only after the background thread finishes its work), which may or may not be what you want.

If you want the background thread to terminate when the form is closed, you can use a BackgroundWorker, which also gives you other convenient functionalities like reporting progress and canceling:

private void Form1_Load(object sender, EventArgs e)
{
    BackgroundWorker worker = new BackgroundWorker();
    worker.DoWork += (sender_, e_) =>
    {
        string path = "c:\\1.jpg";
        for (int i = 0; i < 10; i++)
        {
            string url = "http://...." + i.ToString() + ".jpg";//i'm sure the http file does exist

            using (WebClient wc = new WebClient())
            {
                wc.DownloadFileCompleted += new AsyncCompletedEventHandler(wc_DownloadFileCompleted);
                wc.DownloadFileAsync(new Uri(url), path);

                Thread.Sleep(3000);//i'm sure the download will be finished in 3s

                WriteLog("C:\\1.log", "main function\r\n");

            }
        }
    };
    worker.RunWorkerAsync();
}
Imperishable Night
  • 1,503
  • 9
  • 19
  • Get it. The answer is very helpful and the BackgroundWorker is very useful. Thank you so much ^_^ – JQY Nov 04 '17 at 09:33
  • //i'm sure the download will be finished in 3s - code like that makes it into production too often – Thomas Weller Jul 09 '20 at 09:38
  • @ThomasWeller Well, it's not bad practice to put a timeout on download tasks. Although this is probably the wrong way to do it... (Works well as a minimal working example though; better than giving a URL that actually takes 3s to download.) – Imperishable Night Jul 20 '20 at 22:28
1

I recommend using an approach like from this other question "How to implement a Timeout on WebClient.DownloadFileAsync"

It looks like the thread.sleep is causing the thread writing the log to sleep. This way you won't have that happening, and it is non blocking so no thread is ever stuck busy waiting.

        CancellationTokenSource source = new CancellationTokenSource();
        source.CancelAfter(TimeSpan.FromSeconds(5));

        await Task.Factory.StartNew(() =>
        {
            wc.DownloadFileCompleted += new AsyncCompletedEventHandler(wc_DownloadFileCompleted);
            wc.DownloadFile(new Uri("MyInternetFile"), filePath);

        }, source.Token);
Ryan Donahue
  • 48
  • 1
  • 4
  • oh yes maybe the await function is good way to control the processing time of a async approach, thank you ^_^ – JQY Nov 04 '17 at 09:38