Click here to Skip to main content
Click here to Skip to main content

C# .NET Background File Downloader

By , 2 May 2009
 
FileDownloaderDemo-downloading.gif

Introduction

This class enables you to easily download multiple files in the background (via a separate thread), and will provide information about the amount of downloaded data, the percentage of completion, and the download speed. On top of this, you can cancel downloading, pause it, and of course, also resume.

Background

I started working on this class after someone on a programming help forum asked how to best download files in the background. I originally wrote it in VB.NET, but have now created a C# implementation. Check the VB.NET Background File Downloader article for the original code. Another difference with the original VB.NET implementation is that this code uses a WPF demo application, and not a Windows Forms based one.

Using the Code

Once you added the class to your project, you should be able to access it via the project's namespace.

The first thing you need to do when using this class is (logically) create a new instance, and then add the files you want to download. You'll also need to set the local directory to which you want to download. This is pretty straight forward.

// Creating a new instance of a FileDownloader
private FileDownloader downloader = new FileDownloader();

You'll also want to add some files to the list that need to be downloaded. This example demonstrates how to read each line of a WPF RichTextBox and add it to the list:

// A simple implementation of setting the directory path, 
// adding files from a textbox and starting the download
private void btnStart_Click(object sender, RoutedEventArgs e)
{
    System.Windows.Forms.FolderBrowserDialog openFolderDialog = 
			new System.Windows.Forms.FolderBrowserDialog();
     if (openFolderDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
    {
        // Set the path to the local directory where the files will be downloaded to
        downloader.LocalDirectory = openFolderDialog.SelectedPath;
         // Clear the current list of files (in case it's not the first download)
        downloader.Files.Clear();
         // Get the contents of the rich text box
        string rtbContents = new TextRange(rtbPaths.Document.ContentStart, 
					rtbPaths.Document.ContentEnd).Text;
        foreach (string line in rtbContents.Split('\n'))
        {
            String trimmedLine = line.Trim(' ', '\r');
            if (trimmedLine.Length > 0)
            {
                // If the line is not empty, assume it's a valid URL 
	       // and add it to the files list
                // Note: You could check if the URL is valid before adding it, 
	       // and probably should do this is a real application
                downloader.Files.Add(new FileDownloader.FileInfo(trimmedLine));
            }
        }
         // Start the downloader
        downloader.Start();
    }
}

Note: The example code in this article is for a C# WPF application, and will be slightly different for a C# forms application.

The code needed to then pause, resume or cancel the downloads couldn't be simpler:

private void btnPause_Click(object sender, RoutedEventArgs e)
{
    // Pause the downloader
    downloader.Pause();
}
private void btnResume_Click(object sender, RoutedEventArgs e)
{
    // Resume the downloader
    downloader.Resume();
} 
private void btnStop_Click(object sender, RoutedEventArgs e)
{
    // Stop the downloader
    // Note: This will not be instantaneous - the current requests need to be 
    // closed down, and the downloaded files need to be deleted
    downloader.Stop();
} 

The downloader also provides a few properties to indicate its current state: IsBusy, IsPaused, CanStart, CanStop, CanPause and CanResume (all booleans). Here you have an example of how to use these to set your interface:

// This event is fired every time the paused or busy state is changed, 
// and used here to set the controls of the interface
// This makes it equivalent to a void handling both 
// downloader.IsBusyChanged and downloader.IsPausedChanged
private void downloader_StateChanged(object sender, EventArgs e)
{
    // Setting the buttons
    btnStart.IsEnabled = downloader.CanStart;
    btnStop.IsEnabled = downloader.CanStop;
    btnPause.IsEnabled = downloader.CanPause;
    btnResume.IsEnabled = downloader.CanResume;
     // Enabling or disabling the setting controls
    rtbPaths.IsReadOnly = downloader.IsBusy;
    cbUseProgress.IsEnabled = !downloader.IsBusy;
} 

This is the demo code to display the progress information:

// Occurs every time of block of data has been downloaded, 
// and can be used to display the progress with
// Note that you can also create a timer, 
// and display the progress every certain interval
// Also note that the progress properties return a size in bytes, 
// which is not really user friendly to display
// The FileDownloader class provides static functions to format 
// these byte amounts to a more readable format, either in binary or decimal notation 
private void downloader_ProgressChanged(object sender, EventArgs e)
{
    pBarFileProgress.Value = downloader.CurrentFilePercentage();
    lblFileProgress.Content = String.Format("Downloaded {0} of {1} ({2}%)", 
		FileDownloader.FormatSizeBinary(downloader.CurrentFileProgress), 
		FileDownloader.FormatSizeBinary(downloader.CurrentFileSize), 
		downloader.CurrentFilePercentage()) + String.Format(" - {0}/s", 
		FileDownloader.FormatSizeBinary(downloader.DownloadSpeed));
   
    if (downloader.SupportsProgress)
    {
        pBarTotalProgress.Value = downloader.TotalPercentage();
        lblTotalProgress.Content = String.Format("Downloaded {0} of {1} ({2}%)", 
		FileDownloader.FormatSizeBinary(downloader.TotalProgress), 
		FileDownloader.FormatSizeBinary(downloader.TotalSize), 
		downloader.TotalPercentage());
    }
} 

Another noteworthy snippet of code is how to set the SupportsProgress property.

// Setting the SupportsProgress property - if set to false, 
// no total progress data will be available!
private void cbUseProgress_Checked(object sender, RoutedEventArgs e)
{
    downloader.SupportsProgress = (Boolean)cbUseProgress.IsChecked; 
} 

When the SupportProgress property is set to true, the file sizes will be calculated before any download is started. This can take a while, definitely when you have a large amount of files. The FileDownloader class fires an event every time it starts checking the size of a file, which can be used to display the progress.

// Show the progress of file size calculation
// Note that these events will only occur when the total file size is 
// calculated in advance, in other words when the SupportsProgress is set to true
private void downloader_CalculationFileSize(object sender, Int32 fileNr)
{
    lblStatus.Content = String.Format("Calculating file sizes - 
			file {0} of {1}", fileNr, downloader.Files.Count);
}

FileDownloaderDemo-calculating.gif

Points of Interest

The main reasons for translating my VB.NET class to C# were to both fully familiarize myself with C#, and its differences compared to VB.NET, and to exercise some WPF basics.

I'm hoping to implement some more features soon, including cancellation without deleting the files and the option to resume downloading afterwards, and the ability to download multiple files simultaneously, on separate threads.

History

  • May 2nd 2009: Published the C# class and this article
  • April 22nd 2009: Published an article about the VB.NET class
  • April 21st 2009: Published the VB.NET class
    • For the code the original VB.NET class is based upon (can be seen as an older version), see this article.

References

  • Dutch support for this class can be found here.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)

About the Author

Jeroen De Dauw
Software Developer
Belgium Belgium
Member
I am a free and open source software enthusiast and freelance software developer with multiple years of experience in both web and desktop development. Currently my primarily focus is on MediaWiki and Semantic MediaWiki work. I'm in the all time top 10 MediaWiki comitters and am one of the WikiWorks consultants. You can contact me at jeroendedauw at gmail for development jobs and questions related to my work.
 
More info can be found on my website [0] and my blog [1]. You can also follow me on twitter [2] and identi.ca [3].
 
[0] http://www.jeroendedauw.com/
[1] http://blog.bn2vs.com/
[2] https://twitter.com/#!/JeroenDeDauw
[3] http://identi.ca/jeroendedauw

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionNeed some infomemberTridip Bhattacharjee27 Mar '13 - 8:03 
just tell me one thing does your code download two or three file at same time or one after one.
tbhattacharjee

SuggestionFile In Use Exception [modified]memberwannabeuk11 Dec '12 - 3:55 
Just to note, There is a slight bug with the code, if the download fails for some reason (504 for example) the downloader leaves an empty file in place which causes issues if you attempt to re-download again, as the file will be in use.
 
EDIT: forgot the post the solution!
 
Simply a case of now closing the file when an error occurs, might not be the best solution but the easiest is to simply do:
 
if (exc != null)
{
   writer.Close();
   bgwDownloader.ReportProgress((Int32)InvokeType.FileDownloadFailedRaiser, exc);
}
 
you notice i've added writer.Close(); to line 391 (inside the downloadFile method)
This clears up that error

modified 11 Dec '12 - 10:04.

Questionwin 7 64-bit freezesmemberNoel Buenaventura2 Dec '12 - 8:27 
Hi!
 
I am just wondering why it freezes in win 7 64-bit. Actually, only the gui is freezing but the downloading are still working even the gui is being freezed. Any idea why?
 
Thanks and regards,
Weng
Questiondownloader classmembereranotz5017 Nov '12 - 5:46 
why does your class derive from System.Object , well i know why ,
it just seems kinda redundant...
 
does it have any thing to do with the capital letter Object
?
Questionhow to auto connect with internt Urlmembermanojmadhur28 Apr '12 - 10:20 
here..how can i auto connect to an internet url through downloader...?when i go to a youtube downloader should auto connect with video url and ask should ask to download this..?
GeneralGood workmemberjirikadlec230 May '11 - 10:41 
Thank you for the article.
This is almost what I've been looking for.
Only small disadvantage is, that sometimes after clicking "Cancel" it takes a couple of second to stop the download.
 
I tried using BeginGetResponse and DownloadAsync, but the approach in this article seems to be much more robust and responsive when dealing with multiple files.
 
I was able to slightly modify the code to call sOAP web services instead of downloading files.
GeneralDoes not work.memberiJam_j6 Feb '11 - 22:22 
I created new console project. Add FileDownloader.cs into the project. Edited my main file according to the article. The objective is to create simple application that uses FileDownloader class. My code look like this.
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FileDownloaderApp;
using System.IO;
 
namespace WebserverDownload
{
	class Program
	{
		private static FileDownloader downloader = new FileDownloader();
 
		static void Main(string[] args)
		{
			downloader.LocalDirectory = Directory.GetCurrentDirectory();
			downloader.Files.Clear();
			downloader.Files.Add(new FileDownloader.FileInfo(@"http://localhost:280/FileServer/crypto.bin"));
			downloader.Start();
			Console.ReadKey();
		}
	}
}
 
It seems to me that downloader.Start doesnt do anything. To further see things up, I did create this application in WPF form. The only new significant things i added was the ProgressChanged events. Code look likes this.
 
using System;
using System.Collections.Generic;
using System.Linq;
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;
using FileDownloaderApp;
using System.IO;
 
namespace WpfWebserverDownload
{
	public partial class MainWindow : Window
	{
		private FileDownloader downloader = new FileDownloader();
		public MainWindow()
		{
			InitializeComponent();
			CommandBindings.Add(new CommandBinding(ApplicationCommands.Close, new ExecutedRoutedEventHandler(delegate(object sender, ExecutedRoutedEventArgs args) { this.Close(); })));
 
			downloader.ProgressChanged += new EventHandler(downloader_ProgressChanged);
		}
 
		void downloader_ProgressChanged(object sender, EventArgs e)
		{
			pbProgress.Value = downloader.CurrentFilePercentage();
			lblProgress.Content = String.Format("Downloaded {0} of {1} ({2}%)", FileDownloader.FormatSizeBinary(downloader.CurrentFileProgress), FileDownloader.FormatSizeBinary(downloader.CurrentFileSize), downloader.CurrentFilePercentage()) + String.Format(" - {0}/s", FileDownloader.FormatSizeBinary(downloader.DownloadSpeed));
		}
 
		private void btnStart_Click(object sender, RoutedEventArgs e)
		{
			downloader.LocalDirectory = Directory.GetCurrentDirectory();
			downloader.Files.Clear();
			downloader.Files.Add(new FileDownloader.FileInfo(@"http://localhost:280/FileServer/crypto.bin"));
			downloader.Start();
		}
	}
}
 
downloader.Start really doesnt do anything. Please help.
Thanks.
GeneralRe: Does not work.memberJeroen De Dauw7 Feb '11 - 5:49 
Looks correct to me... Make sure downloader.LocalDirectory is set to something valid. I recommend you put in some breakpoints to see what's actually happening. I've switched to Linux, so can't really help you debugging this Smile | :)
Jeroen De Dauw

Blog ; Wiki

GeneralRe: Does not work.memberiJam_j7 Feb '11 - 19:20 
Works okay now. Dont know whats wrong.
GeneralAdded support for more complex urls [modified]memberQRoX29 Jun '10 - 11:38 
Currently FileDownloader only works with URLs ended with the name of the file you want to download. This is not always the case, for instance, this url:
 
http://www.mysite.com/files/my-file.exe?download
 
won't work properly.
 
The problem lies in the way the url is parsed in FileInfo constructor.
To solve the problem (at least partially) you can use the following method to parse the url correctly:
    private string GetFileName(string url) {
        Uri u = new Uri(url);
        string filePath = u.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped);
 
        return Path.GetFileName(filePath);
    }
 
To be able to use this method in the constructor, it is necesary to make FileInfo a class.
 
This does not help us in the case of the urls with the following format:
 
http://www.mysite.com/files/download.php?fileid=1234
 
but is a little improvement I think.
 
Btw, is there any way to determine the real file name being downloaded (useful in the latter example)?

modified on Tuesday, June 29, 2010 5:51 PM

GeneralA little improvement / bug fixmemberQRoX29 Jun '10 - 8:14 
I found a bug in FileDownloder class. Actually it's not a bug, but in certain situations it behaves like one.
 
In the method bgwDownloader_DoWork (around line 254) you create the LocalDirectory directory if it doesn't exist. I think it's better to move that single line of code to IsBusy setter, right before calling bgwDownloader.RunWorkerAsync().
 
If there is a problem creating the directory (insufficient permissions, malformed path, disk full, etc) the exception will propagate and the user/developer will know there is a problem. If the exception is thrown inside bgwDownloader_DoWork method, it gets swallowed and hidden since that code is running in a background thread.
GeneralFTP EnhancementmemberMustafa Tümer TAN25 Apr '10 - 0:42 
Thanks for the good work.
I added an FTP functionality to the code in a simple way. More elegant ways of coding can be done for my enhancement I believe. Changed functions are:
 
1. CalculateFileSizes
 
private void calculateFilesSize()
        {
            fireEventFromBgw(Event.CalculationFileSizesStarted);
            m_totalSize = 0;
 
            for (Int32 fileNr = 0; fileNr < this.Files.Count; fileNr++)
            {
                bgwDownloader.ReportProgress((Int32)InvokeType.CalculatingFileNrRaiser, fileNr + 1);
                try
                {
 
                    if (this.Files[fileNr].Path.Contains("ftp://"))
                    {
                        FtpWebRequest FtpRequest = (FtpWebRequest)WebRequest.Create(this.Files[fileNr].Path);
                        FtpRequest.Credentials = new NetworkCredential("anonymous", "11111");
                        FtpRequest.KeepAlive = false;
                        FtpRequest.UseBinary = false;
                        FtpRequest.UsePassive = true;
                        FtpRequest.Method = WebRequestMethods.Ftp.GetFileSize;
                        FtpWebResponse FtpResponse = (FtpWebResponse)FtpRequest.GetResponse();
                        m_totalSize += FtpResponse.ContentLength;
                        FtpWebResponse .Close();
                    }
                    else
                    {
                        HttpWebRequest webReq = (HttpWebRequest)WebRequest.Create(this.Files[fileNr].Path);
                        HttpWebResponse webResp = (HttpWebResponse)webReq.GetResponse();
                        m_totalSize += webResp.ContentLength;
                        webResp.Close();
                    }
                }
                catch (Exception) { }
            }
            fireEventFromBgw(Event.FileSizesCalculationComplete);
        }
 
2. downloadFile(Int32 fileNr)
 
private void downloadFile(Int32 fileNr)
        {
            m_currentFileSize = 0;
            fireEventFromBgw(Event.FileDownloadAttempting);
 
            FileInfo file = this.Files[fileNr];
            Int64 size = 0;
 
            Byte[] readBytes = new Byte[this.PackageSize];
            Int32 currentPackageSize;
            System.Diagnostics.Stopwatch speedTimer = new System.Diagnostics.Stopwatch();
            Int32 readings = 0;
            Exception exc = null;
            
            FileStream writer = new FileStream(this.LocalDirectory + "\\" + file.Name, System.IO.FileMode.Create);
 
            FtpWebRequest FtpRequest;
            FtpWebResponse FtpResponse = null;
            HttpWebRequest HttpRequest;
            HttpWebResponse HttpResponse = null;
 
            if (file.Path.Contains("ftp://"))
            {
              try
                {
                    FtpRequest = (FtpWebRequest)WebRequest.Create(file.Path);
                    FtpRequest.Credentials = new NetworkCredential("anonymous", "11111");
                    FtpRequest.KeepAlive = false;
                    FtpRequest.UseBinary = false;
                    FtpRequest.UsePassive = true;
                    FtpRequest.Method = WebRequestMethods.Ftp.GetFileSize;
                    FtpResponse = (FtpWebResponse)FtpRequest.GetResponse();
                    size = FtpResponse.ContentLength;
                    
                    FtpRequest = null;
                    FtpResponse = null;
                    FtpRequest = (FtpWebRequest)WebRequest.Create(file.Path);
                    FtpRequest.Credentials = new NetworkCredential("anonymous", "11111");
                    FtpRequest.Method = WebRequestMethods.Ftp.DownloadFile;
                    FtpResponse = (FtpWebResponse)FtpRequest.GetResponse();
                }
                catch (Exception ex) 
                {
                    exc = ex;
                }
            }
            else
            {
                try
                {
                    HttpRequest = (HttpWebRequest)WebRequest.Create(file.Path);
                    HttpResponse = (HttpWebResponse)HttpRequest.GetResponse();
                    size = HttpResponse.ContentLength;
                }
                catch (Exception ex)
                {
                    exc = ex;
                }
            }
 
            m_currentFileSize = size;
            fireEventFromBgw(Event.FileDownloadStarted);
 
            if (exc != null)
            {
                bgwDownloader.ReportProgress((Int32)InvokeType.FileDownloadFailedRaiser, exc);
            }
            else
            {
                m_currentFileProgress = 0;
                while (m_currentFileProgress < size && !bgwDownloader.CancellationPending)
                {
                    while (this.IsPaused) { System.Threading.Thread.Sleep(100); }
 
                    speedTimer.Start();
 
                    if (file.Path.Contains("ftp://"))
                    {
                        currentPackageSize = FtpResponse.GetResponseStream().Read(readBytes, 0, this.PackageSize);
                    }
                    else
                    {
                        currentPackageSize = HttpResponse.GetResponseStream().Read(readBytes, 0, this.PackageSize);
                    }
 
                    
                    m_currentFileProgress += currentPackageSize;
                    m_totalProgress += currentPackageSize;
                    fireEventFromBgw(Event.ProgressChanged);
                    writer.Write(readBytes, 0, currentPackageSize);
                    readings += 1;
 
                    if (readings >= this.StopWatchCyclesAmount)
                    {
                        m_currentSpeed = (Int32)(this.PackageSize * StopWatchCyclesAmount * 1000 / (speedTimer.ElapsedMilliseconds + 1));
                        speedTimer.Reset();
                        readings = 0;
                    }
                }
 
                speedTimer.Stop();
                writer.Close();
                if (file.Path.Contains("ftp://"))
                {
                    FtpResponse.Close();
                }
                else
                {
                    HttpResponse.Close();
                }
 
                
                if (!bgwDownloader.CancellationPending) { fireEventFromBgw(Event.FileDownloadSucceeded); }
            }
            
 
            fireEventFromBgw(Event.FileDownloadStopped);
        }

GeneralUnable to read data from the transport connection: The connection was closedmemberIzisin26 Mar '10 - 13:54 
I'm downloading files from a CDN and when I put the downloading into pause and wait a few minutes; I receive the error: "Unable to read data from the transport connection: The connection was closed"
 
Any idea what might cause this issue?
GeneralRe: Unable to read data from the transport connection: The connection was closedmemberjeroen de dauw26 Mar '10 - 19:04 
Yeah, the connection drops after a while, and this class does not check for that, and re-initiate it when needed. Fixing that would not be to difficult though Wink | ;)
Jeroen De Dauw
---Forums ; Blog ; Wiki---
70 72 6F 67 72 61 6D 6D 69 6E 67 20 34 20 6C 69 66 65!

GeneralRe: Unable to read data from the transport connection: The connection was closedmemberswaby21 Mar '12 - 17:06 
How do i fixed it... please help
GeneralThanks for the article.memberoddessa3714 Jan '10 - 0:34 
Excellent article.
 
I have developed a Forum specific WebBrowser and have included your Download Manager which intercepts the default IE download function.
 
I have made a few changes, dialogs/ taskbar progress and Icon Overlays for Windows 7.
 
I thought it best to ask permission to make the app available as it is using your Download Manager, of course full credit will be given to you for the original app.
 
Thankyou.
GeneralRe: Thanks for the article.memberjeroen de dauw14 Jan '10 - 1:30 
Hey,
 
Feel free to put this in your app, that's why I created the article Smile | :)
 
Cheers!
 
Jeroen De Dauw
---
Forums ; Blog ; Wiki
---
70 72 6F 67 72 61 6D 6D 69 6E 67 20 34 20 6C 69 66 65!

QuestionRe: Thanks for the article. [modified]memberoddessa3714 Jan '10 - 4:12 
Many thanks.Thumbs Up | :thumbsup:
 
One other thing, i'd like to post an article in relation to this Downloader & Windows 7, basically giving the details on, Taskbar progress/Overlays using Net 4.0. To do this i'd be showing references to your code.
 
Is it OK to go ahead?
 
modified on Thursday, January 14, 2010 10:26 AM

Generalexceptionmembermalac13 Jul '09 - 2:16 
hi, your component is great, but there is a little bug:
1. start download
2. pause download
3. cancel download
4. start download again
5. exception occurs: bgwDownloader is busy
GeneralRe: exceptionmemberbn2vs14 Jul '09 - 12:29 
Hey,
 
I just checked, and I'm unsuccessful in recreating the bug you describe. Are you still having problems with it?
 
Cheers
BN
 
GSoC 2009 student for SMW!
---
My little forums: http://code.bn2vs.com
---
70 72 6F 67 72 61 6D 6D 69 6E 67 20 34 20 6C 69 66 65!

QuestionDownload HTML file from web?memberMicrotoby29 Jun '09 - 7:53 
I've notice that when I use this class to download a html file, I got a 0KB file. Could you tell me how to support download a html file from web. Sniff | :^)
AnswerRe: Download HTML file from web?memberbn2vs14 Jul '09 - 12:31 
Hey,
 
I just tried downloading an html file, and had no problems with it - all 7kb received. Are you still having problems with this?
 
Cheers
BN
 
GSoC 2009 student for SMW!
---
My little forums: http://code.bn2vs.com
---
70 72 6F 67 72 61 6D 6D 69 6E 67 20 34 20 6C 69 66 65!

GeneralRe: Download HTML file from web?memberMicrotoby15 Jul '09 - 22:53 
some website got webResponse.ContentLength has correct value, but the other got -1, I think this poblem is.
when got wrong size, the "while (m_currentFileProgress < size && !bgwDownloader.CancellationPending)" ended.
GeneralRe: Download HTML file from web?memberbn2vs17 Jul '09 - 9:07 
Hey,
 
Any idea why it has length -1?
 
EDIT: apparently someone else has already come across this problem (no solution though): Downloading Files in .NET With All Information: Progressbar, Download Speed, Supports Cancel and Resume[^]
 
If it has that length, it should not create a file though. I'll fix this in the next version.
 
Cheers
BN
 
GSoC 2009 student for SMW!
---
My little forums: http://code.bn2vs.com
---
70 72 6F 67 72 61 6D 6D 69 6E 67 20 34 20 6C 69 66 65!

QuestionWhy not use BITS?memberSoundman32.24 May '09 - 21:26 
While this looks like a good article and was probably done for finding out how it's done, it does seem to re-invent the downloading wheel again.
 
All these features (and more) are available in BITS.
 
http://msdn.microsoft.com/en-us/library/aa362708(VS.85).aspx

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130523.1 | Last Updated 2 May 2009
Article Copyright 2009 by Jeroen De Dauw
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid