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: