In this short tutorial I’ll explain how to write a ListBox that is databound
to a list of download tasks. The sourcecode for this tutorial is in C# 3.5 (WPF)
XAML. Let’s start by writing our DownloadTaskItem class.
internal class DownloadTaskItem : INotifyPropertyChanged, IDisposable
The INotifyPropertyChanged event can be used to signal
the parent the one of our property values has changed.
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyCanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Now let’s add a fue priviate members.
private WebClient webClient;
This WebClient object will use for our async download.
private string fileName;
private Uri uriData;
private long fileSize;
private Guid _guid;
private string _progressText;
private string _buttonText;
private string _associatedGroupText;
To implement this in our properties and make this all
work we’ll have to call OnPropertyChanged in our properties.
public string AssociatedGroupText
{
get { return _associatedGroupText; }
set
{
_associatedGroupText = value;
OnPropertyCanged("AssociatedGroupText");
}
}
public string ProgressText
{
get { return _progressText; }
set
{
_progressText = value;
OnPropertyCanged("ProgressText");
}
} // DETAIL_BYTES
public string ButtonText
{
get { return _buttonText; }
set
{
_buttonText = value;
OnPropertyCanged("ButtonText");
}
}
The rest of the properties can be write as usual.
public string OriginalUrl { get { return uriData.OriginalString; } } // DETAIL_URI
public string TaskGuid
{
get
{
if (null != _guid)
_guid = new Guid();
return _guid.ToString();
}
}
public long ReceivedFileSize { get { return fileSize; } }
Now how does the parent object know if this downloadtask
has completed. Well the best way to do this is by adding an other event.
First thing we need is an EventArgs class:
public class DownloadTaskEventArgs : EventArgs
{
private readonly Uri uri;
private readonly System.ComponentModel.AsyncCompletedEventArgs args;
//Constructor.
public DownloadTaskEventArgs(Uri uri, System.ComponentModel.AsyncCompletedEventArgs e)
{
this.uri = uri;
this.args = e;
}
public Uri DataUri
{
get { return uri; }
}
public System.ComponentModel.AsyncCompletedEventArgs Args
{
get { return args; }
}
// Notifying Text property that contains a status message.
//
public string Text
{
get
{
if (args.Error == null || args.Cancelled)
{
return ("completed");
}
else
{
return ("failed");
}
}
}
}
Now adding the event to our DownloadTaskItem class:
public delegate void CompletedEventHandler(object sender, DownloadTaskEventArgs e);
public event CompletedEventHandler Completed;
// The protected OnCompleted method raises the event by invoking
// the delegates. The sender is always this, the current instance
// of the class.
//
protected virtual void OnCompleted(DownloadTaskEventArgs e)
{
CompletedEventHandler handler = Completed;
if (handler != null)
{
// Invokes the delegates.
handler(this, e);
}
}
DownloadTaskItem Class:
internal class DownloadTaskItem : INotifyPropertyChanged, IDisposable
{
#region Task Completed events
public delegate void CompletedEventHandler(object sender, DownloadTaskEventArgs e);
public event CompletedEventHandler Completed;
// The protected OnCompleted method raises the event by invoking
// the delegates. The sender is always this, the current instance
// of the class.
//
protected virtual void OnCompleted(DownloadTaskEventArgs e)
{
CompletedEventHandler handler = Completed;
if (handler != null)
{
// Invokes the delegates.
handler(this, e);
}
}
#endregion
#region Private fields
private WebClient webClient;
private string fileName;
private Uri uriData;
private long fileSize;
private Guid _guid;
private string _progressText;
private string _buttonText;
private string _associatedGroupText;
#endregion
#region Great Properties
public string AssociatedGroupText
{
get { return _associatedGroupText; }
set
{
_associatedGroupText = value;
OnPropertyCanged("AssociatedGroupText");
}
}
public string ProgressText
{
get { return _progressText; }
set
{
_progressText = value;
OnPropertyCanged("ProgressText");
}
} // DETAIL_BYTES
public string OriginalUrl { get { return uriData.OriginalString; } } // DETAIL_URI
public string TaskGuid
{
get
{
if (null != _guid)
_guid = new Guid();
return _guid.ToString();
}
}
public string ButtonText
{
get { return _buttonText; }
set { _buttonText = value; OnPropertyCanged("ButtonText"); }
}
public long ReceivedFileSize { get { return fileSize; } }
#endregion
#region DownloadTaskItem Constructors
///
/// Initializes a new instance of the class.
///
public DownloadTaskItem(Uri uriData, string path)
{
#region Prepare for download
this.uriData = uriData;
fileName = this.uriData.Segments[this.uriData.Segments.Length - 1];
fileName = path.Equals(string.Empty) ? fileName : string.Format("{0}\\{1}", path, fileName);
webClient = new WebClient();
#endregion
#region Assign call backs
webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler((sender, e) =>
{
#region Check if additional info. is required
#endregion
#region Show progress info
fileSize = e.TotalBytesToReceive;
AssociatedGroupText = String.Format("{0} - {1}%", fileName, e.ProgressPercentage);
ProgressText = String.Format("Downloaded {0}/{1}", GetHumanReadableFileSize(e.BytesReceived), GetHumanReadableFileSize(fileSize));
#endregion
});
webClient.DownloadFileCompleted += new System.ComponentModel.AsyncCompletedEventHandler((sender, e) =>
{
#region Check if additional info. is required
#endregion
#region Process error state
if (e.Error == null)
{
AssociatedGroupText = String.Format("{0} - completed", fileName);
ProgressText = String.Format("File size - {0}", GetHumanReadableFileSize(fileSize));
}
else if (e.Cancelled == false && e.Error != null)
{
AssociatedGroupText = String.Format("{0} - Failed", fileName);
ProgressText = e.Error.Message;
}
#endregion
#region Process canceled download
if (e.Cancelled == true)
{
AssociatedGroupText = String.Format("{0} - Canceled", fileName);
}
#endregion
ButtonText = "Remove";
OnCompleted(new DownloadTaskEventArgs(uriData, e));
});
AssociatedGroupText = String.Format("{0} - starting ... ", fileName);
ProgressText = "Downloaded 0/? bytes";
ButtonText = "Cancel";
#endregion
#region Start file download
webClient.DownloadFileAsync(uriData, fileName, this);
#endregion
}
#endregion
public bool StopDownload(string message)
{
ProgressText = message;
if (webClient.IsBusy == true)
{
webClient.CancelAsync();
return true;
}
else
{
webClient.CancelAsync();
return false;
}
}
#region Helper classes
///
/// Convert bytes to a human readable format
///
/// the number bytes
/// apropriate amount of bytes (Gb, Mb, Kb, bytes)
private string GetHumanReadableFileSize(long fileSize)
{
#region Gb
if ((fileSize / (1024 * 1024 * 1024)) > 0)
{
return String.Format("{0} Gb", (double)Math.Round((double)(fileSize / (1024 * 1024 * 1024)), 2));
}
#endregion
#region Mb
if ((fileSize / (1024 * 1024)) > 0)
{
return String.Format("{0} Mb", (double)Math.Round((double)(fileSize / (1024 * 1024)), 2));
}
#endregion
#region Kb
if ((fileSize / 1024) > 0)
{
return String.Format("{0} Kb", (double)Math.Round((double)(fileSize / 1024), 2));
}
#endregion
#region Bytes
return String.Format("{0} b", fileSize);
#endregion
}
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyCanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
#region IDisposable Members
public void Dispose()
{
if (this.webClient != null)
{
this.webClient.CancelAsync();
this.webClient.Dispose();
}
}
#endregion
}
How to use this class.
First we’ll create a window and set it’s properties
like the sample below.
I chose a grid layout for this tutorial but you could
use canvas if you like.
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc=”http://schemas.openxmlformats.org/markup-compatibility/2006”
Title="My Downloader"
Height="480" Width="640"
mc:Ignorable="d"
xmlns:src="clr-namespace:MyDownloader" WindowStartupLocation="CenterScreen">
Now we can add our ListBox. You have to give it a name
using the x:Name property. Now let’s add a ItemTemplate with a DataTemplate
and add our databound fields in a layout. The binding source of these fields are
the properties of our DownloadTask object. I chose to use StackPanel orientation
vertical to have a first row containing the title and the the second row tobe an
other StackPanel with it’s orientation set to horizontal. The second StackPanel
content is in this case a Textbox showing the text for an Cancel Button and a Textbox showing our download progress.