Reporting the percentage progress of large file downloads in C# / WPF

When downloading very large files from an online source, it can sometimes appear that the program has frozen or crashed when in fact it is still busy downloading and/or writing to disk.

This post that shows you how to report the percentage of large file sizes in addition to the overall progress of all downloads and is demonstrated using a WPF application using the MVVM (Model View ViewModel) pattern.

Useful StackOverflow link:

https://stackoverflow.com/questions/21169573/how-to-implement-progress-reporting-for-portable-httpclient
Step 1: Create a new Visual Studio WPF application

Step 2: Create the View

Update the MainWindow.xaml to include the necessary progress bar, button control, text controls etc.

MainWindow.xaml

<Window x:Class="DownloadProgress.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DownloadProgress"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="725">

    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    
    <Grid>
        <TextBlock  
            TextAlignment="Left"
            Width="650"
            Height="40"
            HorizontalAlignment="Left"
            VerticalAlignment="Top"
            Margin="20,20,0,0"
            Text="{Binding DownloadInfo}" />

        <StackPanel 
            VerticalAlignment="Center"
            Orientation="Vertical">

            <ProgressBar 
                Height="30" Width="500"
                Value="{Binding CurrentProgress, Mode=OneWay}"
                Visibility="Visible"
                VerticalAlignment="Center"
                Minimum="0" Maximum="100"
                Name="pbStatus" />

            <TextBlock  
                TextAlignment="Center"
                Width="200"
                HorizontalAlignment="Center"
                Margin="0,40,0,0"
                Text="{Binding DownloadPercentage}" />

            <TextBlock  
                TextAlignment="Center"
                Width="200"
                HorizontalAlignment="Center"
                Margin="0,20,0,0"
                Text="{Binding TotalPercentage}" />
        </StackPanel>

        <Button 
            Margin="0,0,0,20"
            Content="Download"
            VerticalAlignment="Bottom"
            HorizontalAlignment="Center"
            Command="{Binding Download}"
            Height="30" Width="80" />

    </Grid>
</Window>

Step 3: Create the ViewModel and Model

The ViewModel we use as part of the MVVM architecture to expose the public properties and commands.

MainWindowViewModel.cs

using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Windows.Input;

namespace DownloadProgress
{
   public class MainWindowViewModel : BaseViewModel
   {
      private int _currentProgress;
      private ICommand _download;
      private BackgroundWorker _worker = new BackgroundWorker();
      private List<string> _urls;
      private string _downloadPercentage;
      private MainWindowModel _model;
      private string _downloadInfo;
      private string _totalPercentage;

      public MainWindowViewModel()
      {
         _model = new MainWindowModel();
         _model.PercentageChanged += OnPercentageChanged;

         _worker.DoWork += DoWork;
         _worker.ProgressChanged += ProgressChanged;
         _worker.WorkerReportsProgress = true;
         _worker.WorkerSupportsCancellation = true;

         CurrentProgress = 0;

         _urls = new List<string>
         {
            "http://www.chestech-support.com/ctisupport/downloadDoc.asp?id=2411",
            "http://212.183.159.230/20MB.zip",
            "http://212.183.159.230/5MB.zip",            
            "http://www.chestech-support.com/ctisupport/downloadDoc.asp?id=2355"
         };
      }

      private void OnPercentageChanged(object sender, EventArgs<string> e)
      {
         DownloadPercentage = e.Value + " " + "%" + " of file complete";
      }

      public int CurrentProgress
      {
         get { return _currentProgress; }
         set
         {
            if (_currentProgress != value)
            {
               _currentProgress = value;
               OnPropertyChanged("CurrentProgress");
            }
         }
      }

      public string DownloadPercentage
      {
         get { return _downloadPercentage; }
         set
         {
            if (_downloadPercentage != value)
            {
               _downloadPercentage = value;
               OnPropertyChanged("DownloadPercentage");
            }
         }
      }

      public string TotalPercentage
      {
         get { return _totalPercentage; }
         set
         {
            if (_totalPercentage != value)
            {
               _totalPercentage = value;
               OnPropertyChanged("TotalPercentage");
            }
         }
      }

      public string DownloadInfo
      {
         get { return _downloadInfo; }
         set
         {
            if (_downloadInfo != value)
            {
               _downloadInfo = value;
               OnPropertyChanged("DownloadInfo");
            }
         }
      }

      public ICommand Download
      {
         get
         {
            return _download ?? (_download = new RelayCommand(x =>
            {
               DownloadContent();               
            }));
         }
      }

      private void DownloadContent()
      {
         _worker.RunWorkerAsync();
      }

      private void ProgressChanged(object sender, ProgressChangedEventArgs e)
      {
         CurrentProgress = e.ProgressPercentage;
      }

      private async void DoWork(object sender, DoWorkEventArgs e)
      {
         if (CurrentProgress >= 100)
         {
            CurrentProgress = 0;
         }

         TotalPercentage = CurrentProgress + " %" + " downloaded in total";

         var total = _urls.Count;

         var count = 0;

         foreach (var url in _urls)
         {
            //  Do the download  
            DownloadInfo = "Downloading: " + url;
            await _model.HttpGetForLargeFile(url, "", new CancellationTokenSource().Token);

            CurrentProgress = 100 / (total - count);
            TotalPercentage = CurrentProgress + " %" + " downloaded in total";
            count++;
         }
      }
   }
}

BaseViewModel.cs

using System;
using System.ComponentModel;
using System.Diagnostics;

namespace DownloadProgress
{
   public abstract class BaseViewModel : INotifyPropertyChanged
   {
      #region INotifyPropertyChanged Members

      public event PropertyChangedEventHandler PropertyChanged;

      #endregion

      protected void OnPropertyChanged(string propertyName)
      {
         VerifyPropertyName(propertyName);
         PropertyChangedEventHandler handler = PropertyChanged;
         if (handler != null)
         {
            handler(this, new PropertyChangedEventArgs(propertyName));
         }
      }

      protected void OnPropertyChanged(int propertyValue)
      {
         VerifyPropertyName(propertyValue);
         PropertyChangedEventHandler handler = PropertyChanged;
         if (handler != null)
         {
            handler(this, new PropertyChangedEventArgs(propertyValue.ToString()));
         }
      }

      [Conditional("DEBUG")]
      private void VerifyPropertyName(string propertyName)
      {
         if (TypeDescriptor.GetProperties(this)[propertyName] == null)
            throw new ArgumentNullException(GetType().Name + " does not contain property: " + propertyName);
      }

      [Conditional("DEBUG")]
      private void VerifyPropertyName(int propertyValue)
      {
         if (TypeDescriptor.GetProperties(this)[propertyValue] == null)
            throw new ArgumentNullException(GetType().Name + " does not contain property: " + propertyValue.ToString());
      }
   }
}

EventArgs.cs

using System;

namespace DownloadProgress
{
   public class EventArgs<T> : EventArgs
   {
      public EventArgs(T value)
      {
         Value = value;
      }

      public T Value { get; private set; }
   }
}

EventRaiser.cs

using System;

namespace DownloadProgress
{
   public static class EventRaiser
   {
      public static void Raise(this EventHandler handler, object sender)
      {
         if (handler != null)
         {
            handler(sender, EventArgs.Empty);
         }
      }

      public static void Raise<T>(this EventHandler<EventArgs<T>> handler, object sender, T value)
      {
         handler?.Invoke(sender, new EventArgs<T>(value));
      }

      public static void Raise<T>(this EventHandler<T> handler, object sender, T value) where T : EventArgs
      {
         if (handler != null)
         {
            handler(sender, value);
         }
      }

      public static void Raise<T>(this EventHandler<EventArgs<T>> handler, object sender, EventArgs<T> value)
      {
         handler?.Invoke(sender, value);
      }
   }
}

RelayCommand.cs

using System;
using System.Windows.Input;

namespace DownloadProgress
{
   public class RelayCommand<T> : ICommand
   {
      private readonly Predicate<T> _canExecute;
      private readonly Action<T> _execute;

      public RelayCommand(Action<T> execute)
          : this(execute, null)
      {
         _execute = execute;
      }

      public RelayCommand(Action<T> execute, Predicate<T> canExecute)
      {
         if (execute == null)
         {
            throw new ArgumentNullException("execute");
         }
         _execute = execute;
         _canExecute = canExecute;
      }

      public bool CanExecute(object parameter)
      {
         return _canExecute == null || _canExecute((T)parameter);
      }

      public void Execute(object parameter)
      {
         _execute((T)parameter);
      }

      public event EventHandler CanExecuteChanged
      {
         add { CommandManager.RequerySuggested += value; }
         remove { CommandManager.RequerySuggested -= value; }
      }
   }

   public class RelayCommand : ICommand
   {
      private readonly Predicate<object> _canExecute;
      private readonly Action<object> _execute;

      public RelayCommand(Action<object> execute)
          : this(execute, null)
      {
         _execute = execute;
      }

      public RelayCommand(Action<object> execute, Predicate<object> canExecute)
      {
         if (execute == null)
         {
            throw new ArgumentNullException("execute");
         }
         _execute = execute;
         _canExecute = canExecute;
      }

      public bool CanExecute(object parameter)
      {
         return _canExecute == null || _canExecute(parameter);
      }

      public void Execute(object parameter)
      {
         _execute(parameter);
      }

      // Ensures WPF commanding infrastructure asks all RelayCommand objects whether their
      // associated views should be enabled whenever a command is invoked 
      public event EventHandler CanExecuteChanged
      {
         add
         {
            CommandManager.RequerySuggested += value;
            CanExecuteChangedInternal += value;
         }
         remove
         {
            CommandManager.RequerySuggested -= value;
            CanExecuteChangedInternal -= value;
         }
      }

      private event EventHandler CanExecuteChangedInternal;

      public void RaiseCanExecuteChanged()
      {
         CanExecuteChangedInternal.Raise(this);
      }
   }
}

MainWindowModel.cs

This class we use to do the actual downloading, and hive the low-level stuff away from the View Model class.

I use the buffer size of 4096 in this example.

using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace DownloadProgress
{
   public class MainWindowModel
   {
      public event EventHandler<EventArgs<string>> PercentageChanged;
      public async Task HttpGetForLargeFile(string path, string filename, CancellationToken token)
      {
         using (HttpClient client = new HttpClient())
         {
            using (HttpResponseMessage response = await client.GetAsync(path,
               HttpCompletionOption.ResponseHeadersRead))
            {
               var total = response.Content.Headers.ContentLength.HasValue ? 
                  response.Content.Headers.ContentLength.Value : -1L;

               var canReportProgress = total != -1;

               using (var stream = await response.Content.ReadAsStreamAsync())
               {
                  var totalRead = 0L;
                  var buffer = new byte[4096];
                  var moreToRead = true;

                  do
                  {
                     token.ThrowIfCancellationRequested();

                     var read = await stream.ReadAsync(buffer, 0, buffer.Length, token);

                     if (read == 0)
                     {
                        moreToRead = false;
                     }
                     else
                     {
                        var data = new byte[read];
                        buffer.ToList().CopyTo(0, data, 0, read);

                        // TODO: write the actual file to disk

                        // Update the percentage of file downloaded
                        totalRead += read;

                        if (canReportProgress)
                        {                           
                           var downloadPercentage = ((totalRead * 1d) / (total * 1d)) * 100;
                           var value = Convert.ToInt32(downloadPercentage);

                           PercentageChanged.Raise(this, (value.ToString()));
                        }
                     }
                  }
                  while (moreToRead);
               }
            }
         }
      }
   }
}

A demonstration

On building and running the program, see how both the percentage progress of the overall download (all files) is communicated in addition to the progress of individual files. Possibly useful in scenarios where very large file downloads are anticipated.

The program is demonstrated by the following video: