1

I have a created a WPF gui (netframework 3.5) that is used to download files. Some of them come archived and I would like to have them automatically extracted after the download is complete. As the internet speed will vary from system to system, the only wait to tell when the file is completed is to use a IsFileReady method I have found here. The problem with it? It freezes my UI. Any way to bypass this issue, while remaining on netframework 3.5 based project?

I used the next piece of code and even modified it a bit with a background worker, but no go. still freezes UI

public static bool IsFileReady(string filename)
        {

            try
            {
                using (FileStream inputStream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.None))
                    return inputStream.Length > 0;
            }
            catch (Exception)
            {
                return false;
            }
        }
        public static void WaitForFile(string filename)
        {
            BackgroundWorker tb = new BackgroundWorker();
            tb.DoWork += new DoWorkEventHandler(delegate (object o, DoWorkEventArgs args)
            {
                while (!IsFileReady(filename)) ;
            });
            tb.RunWorkerAsync();

        }

Finally used WaitForFile function inside a button press method. It does the job, but as I said it freezes the UI.

scorpion86
  • 21
  • 2
  • 3
    You have a dangerous leak in your code. You basically instruct one of your CPU cores: please dear CPU core, *as fast as you can*, open and close a file with this path. If that file will never be created, or if the path is too long, or whatever other error, you CPU core will be loaded at 100% with this useless operation *forever*, until you kill your app. – dymanoid Sep 09 '19 at 16:16
  • Can't you just use the callback event on the download class so you are notified when the download is complete? If not, you should put a Thread.Sleep(1000) in your while loop so you only check the file status every second or so. This will prevent the massive CPU loop from happening right now which is spamming your hard drive and CPU 100% until the file is ready. – Jon Sep 09 '19 at 16:18

2 Answers2

0

You need to put the downloading-code itself into a background worker thread. Checking the file progress is not freezing your UI, it's the download itself (How to use a BackgroundWorker?)

public Form1()
{
    InitializeComponent();

    backgroundWorker1.DoWork += backgroundWorker1_DoWork;
    backgroundWorker1.ProgressChanged += backgroundWorker1_ProgressChanged;
    backgroundWorker1.WorkerReportsProgress = true;
}

private void button1_Click(object sender, EventArgs e)
{
    backgroundWorker1.RunWorkerAsync();
}

private void backgroundWorker1_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
    //Long-running process goes here
    for (int i = 0; i < 100; i++)
    {
        Thread.Sleep(1000);
        backgroundWorker1.ReportProgress(i);
    }
}

private void backgroundWorker1_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
{
    progressBar1.Value = e.ProgressPercentage;
}

Besides conducting the download on a background worker thread (not on the same thread as the UI), another thing to do is... depending on how you're downloading the file, query the file size before downloading, and compare how many bytes have been downloaded to full size. There may not be a perfectly smooth way to do that if you're using a library over which you have no control, but at the very least you could check the download progress every 100 milliseconds.

while(!FileIsReady(filename))
{
      Thread.Sleep(100);
}

Finally, you may need to put both the downloading and the progress-checking into two separate background worker threads.

Skarab
  • 51
  • 1
  • 6
0

You want to download a file asynchronously but you're restricted to .NET Framework 3.5 and so don't have access to Task? In that case, you don't need the IsFileReady function and you might not need the BackgroundWorker. System.Net.WebClient provides methods for downloading files asynchronously. Namely, System.Net.WebClient.DownloadFileAsync to do the download, and System.Net.WebClient.DownloadFileCompleted to report when the download has completed.

// MainWindow.xaml
<Window x:Class="DownloadFileAsync.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
      <TextBlock>Uri:</TextBlock>
      <TextBox x:Name="UriTxt"></TextBox>
      <TextBlock>File Name:</TextBlock>
      <TextBox x:Name="FileNameTxt"></TextBox>
      <Label x:Name="StatusLbl">Waiting...</Label>
      <Button x:Name="DownloadBtn" Click="DownloadBtn_Click">Download</Button>
    </StackPanel>
</Window>
// MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace DownloadFileAsync
{
   /// <summary>
   /// Interaction logic for MainWindow.xaml
   /// </summary>
   public partial class MainWindow : Window
   {
      public MainWindow()
      {
         InitializeComponent();

         _client = new System.Net.WebClient();
         _client.DownloadFileCompleted += _client_DownloadFileCompleted;

         // I was using https://github.com/danielmiessler/SecLists/raw/master/Passwords/bt4-password.txt as the file I was testing which
         // required TLS1.2. This line enables TLS1.2, you may or may not need it.
         ServicePointManager.SecurityProtocol = (SecurityProtocolType)0x00000C00;
      }

      void _client_DownloadFileCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
      {
         if (e.Error != null)
         {
            // If there was an error downloading the file, show it.
            MessageBox.Show(e.Error.Message);
         }
         else if (e.Cancelled)
         {
            StatusLbl.Content = "Cancelled!";
         }
         else
         {
            // There weren't any errors and the download wasn't cancelled, so then it must have completed successfully.
            // If the file is an archive, this is where you could extract it. Do be aware that you're back on the UI thread
            // at this point, so the UI will block while the file is extracting.
            //
            // If extraction is going to take some time and you don't want it to block, you'll either need to use an async
            // API to extract the archive or start a BackgroundWorker here to extract the archive.
            StatusLbl.Content = "File Downloaded!";
         }
         DownloadBtn.IsEnabled = true;
      }

      private void DownloadBtn_Click(object sender, RoutedEventArgs e)
      {
         if (string.IsNullOrEmpty(FileNameTxt.Text))
         {
            MessageBox.Show("You must enter a file name");
            return;
         }
         Uri uri;
         if (!Uri.TryCreate(UriTxt.Text, UriKind.Absolute, out uri))
         {
            MessageBox.Show("You must enter a valid uri");
            return;
         }
         StatusLbl.Content = "Downloading...";
         DownloadBtn.IsEnabled = false;
         _client.DownloadFileAsync(uri, FileNameTxt.Text);
      }

      private readonly System.Net.WebClient _client;
   }
}

I made a note of it in the comments, but the delegate you provide to DownloadFileCompleted is executing on the UI thread. So, if you need to extract an archive there the UI will block until the extraction is complete. You'll either need to use an asynchronous API for extracting the archive, or start a BackgroundWorker to extract the archive if you need to avoid blocking the UI during the extraction.

Joshua Robinson
  • 3,399
  • 7
  • 22