Creating an advanced download manager in C#

In this tutorial we are extending a previously created download manager with progress indicator. Among improvements to the previous code, we are adding the code that allows the user to pause and resume a download.

This is a continuation of the tutorial entitled “Creating a download manager in C#” in which we created a Windows application that was capable of downloading files via the http:// protocol and showing the progress of retrieving the bytes from the server to the local computer. In this tutorial we’ll see how we can put additional functionality into this application, by adding code that allows the user to pause and resume a download.

If you didn’t read the previous tutorial, you should, otherwise you will not understand much of this one since we’ll be developing on top of the code explained in the first tutorial. For the sake of consistency, we will name the application we build in this tuorial “DownloadManager2” as to not be confused with the one that we wrote in the first tutorial, which was entitled “DownloadManager”. However, you should feel free to add the code from this tutorial to the old application.

To allow the user to pause and resume the download, we can’t just suspend and resume the thread in which the downloading process is running, because that would leave an open connection between the PC and the server from where the file is download. The approach that we’re looking for is to close the file and close the stream when the user presses the Pause button. Then, when he presses Resume, we check for the file path and if the file exists (and it should if he paused, because the partially downloaded file got written to the hard-drive), we get its size. Now that we know its size, we know from what point to move on with the downloading. Therefore we open a new connection to the server providing the file and ask it to stream the data to us, but starting from a certain point (using AddRange()), and that point is the same as the file size. The logic here is simple, we downloaded until let’s say byte 314,159 and paused. This means the file on the hard drive now has 314,159 bytes. When it is resumed, the file size is the same as the point where we stopped, so we continue getting the bits of data after the byte 314,159 and append them to the partially downloaded file until the stream is complete orthe user pauses the download again.

The only visible change done to the form is the addition of a “Pause” button which will change itself to “Resume” when clicked. The button is named btnPauseResume. Set its Enabled property to False, since we don’t want the button enabled until a download has been started.

Now that you added this button switch to code view and the first changes we need to make to the previous Dowload Manager project is to create a boolean variable that when set to true, the loop that downloads from the server will stop. You should define this variable next to the other variables from the previous project (the old code is colored gray, you can ignore it):

// The thread inside which the download happens

private Thread thrDownload;

// The stream of data retrieved from the web server

private Stream strResponse;

// The stream of data that we write to the harddrive

private Stream strLocal;

// The request to the web server for file information

private HttpWebRequest webRequest;

// The response from the web server containing information about the file

private HttpWebResponse webResponse;

// The progress of the download in percentage

private static int PercentProgress;

// The delegate which we will call from the thread to update the form

private delegate void UpdateProgessCallback(Int64 BytesRead, Int64 TotalBytes);

// When to pause

bool goPause = false;

By default the variable is set to false so that the download starts and continues normally.
The next in our code came the btnDownload_Click event where the download thread was being started. Since we’re doing some changes to the code for the pause/resume functionality, we’re also adding an if/else condition that checks wether or not a download is already in progress. If it is, we don’t want the user to press Download again. He can either pause or stop the download, but he should not be able to start another download while one is already in progress. Since we’re changing pretty much all the code inside btnDownload_Click, erase all the content inside this event and use this new one:

if (thrDownload != null && thrDownload.ThreadState == ThreadState.Running)

{

    MessageBox.Show("A download is already running. Please either the stop the current download or await for its completion before starting a new one.", "Download in progress", MessageBoxButtons.OK, MessageBoxIcon.Error);

}

else

{

    // Let the user know we are connecting to the server

    lblProgress.Text = "Download Starting";

    // Create a new thread that calls the Download() method

    thrDownload = new Thread(new ParameterizedThreadStart(Download));

    // Start the thread, and thus call Download(); start downloading from the beginning (0)

    thrDownload.Start(0);

    // Enable the Pause/Resume button

    btnPauseResume.Enabled = true;

    // Set the button's caption to Pause because a download is in progress

    btnPauseResume.Text = "Pause";

}

First we check to see if the thread was created and if it’s started. If it is, a download must already be in progress, so we let the user know about that.
Otherwise, what we’re doing different in this event from the previous project is that we start the thread differently (using ParameterizedThreadStart), because now we need to pass a parameter to it. The parameter specifies the point at which we want to start the download. Because when the user presses Download, the download is always started from the beginning (unlike when he presses Resume), the parameter passed is 0, and you can see that on the next line. The last lines are simply enabling the Pause/Resume button so that the user can Pause the download; we also set its caption to “Pause” because that’s what the button will be doing at this point.

The next comes the UpdateProgress method that is left unchanged from the previous project. Now let’s have a look at the new Download method that suffered the most changes, and which does the actual resuming:

private void Download(object startPoint)

{

    try

    {
        // Put the object argument into an int variable

        int startPointInt = Convert.ToInt32(startPoint);

        // Create a request to the file we are downloading

        webRequest = (HttpWebRequest)WebRequest.Create(txtUrl.Text);

        // Set the starting point of the request

        webRequest.AddRange(startPointInt);

 

        // Set default authentication for retrieving the file

        webRequest.Credentials = CredentialCache.DefaultCredentials;

        // Retrieve the response from the server

        webResponse = (HttpWebResponse)webRequest.GetResponse();

        // Ask the server for the file size and store it

        Int64 fileSize = webResponse.ContentLength;

 

        // Open the URL for download

        strResponse = webResponse.GetResponseStream();

 

        // Create a new file stream where we will be saving the data (local drive)

        if (startPointInt == 0)

        {

            strLocal = new FileStream(txtPath.Text, FileMode.Create, FileAccess.Write, FileShare.None);

        }

        else

        {

            strLocal = new FileStream(txtPath.Text, FileMode.Append, FileAccess.Write, FileShare.None);

        }

 

        // It will store the current number of bytes we retrieved from the server

        int bytesSize = 0;

        // A buffer for storing and writing the data retrieved from the server

        byte[] downBuffer = new byte[2048];

 

        // Loop through the buffer until the buffer is empty

        while ((bytesSize = strResponse.Read(downBuffer, 0, downBuffer.Length)) > 0)

        {

            // Write the data from the buffer to the local hard drive

            strLocal.Write(downBuffer, 0, bytesSize);

            // Invoke the method that updates the form's label and progress bar

            this.Invoke(new UpdateProgessCallback(this.UpdateProgress), new object[] { strLocal.Length, fileSize + startPointInt });

 

            if (goPause == true)

            {

                break;

            }

        }

    }

    finally

    {

        // When the above code has ended, close the streams

        strResponse.Close();

        strLocal.Close();

    }

}

First, we drop the WebClient object from the previous project since instead of using the OpenRead method of the WebClient, we are now using the GetResponseStream method of the HttpWebResponse object to get the data from the URL into a stream. This might not be necessary, however it is an improvement that we create an object less in our new version.

A new line in the Download method is webRequest.AddRange(startPointInt); which does most of the work: it tells the server the range from which the download should start and end. This will be different than 0 only when we resume an existing download, otherwise we want the file from the very beginning. The variable that specified the starting point is startPointInt which was passed to the method as a parameter. You’ll see later when we call this method with the parameter that the starting point is the same as the size of the existing file. If there’s no existing file, there’s no resume, so we start from 0. We don’t specify an ending point in the range, because we’re interested in the entire file.

The next change that is very important to note is that we check if the starting point of the download is 0, and if it is we create the file stream normally, like we did in the previous project. However, if it’s not, we specify a different parameter when creating the FileStream object: FileMode.Append – because we want to append the data from the server to the existing file.

Last thing to be noted in this method is the new if condition in the while loop, that checks each and every loop if the goPause variable has been set to true. It if has, we break out of the loop because this variable is set to true only when we wish to stop (pause) the download. Therefore, even though the button is named Pause, what it actually does is to stop the download completely, not leave it in a hanging state. Leaving connections open with the server would be bad, and they would eventually time out. So pressing Pause is similar to pressing the Stop button – but then why do they take similar approaches to stopping the download? If you look in the btn_Stop event, you’ll see that instead of breaking out of the while loop that retrieves bytes of data from the server, we are aborting the thread all together. This can be a harsh approach, and data could get corrupted, we would not know for sure when the thread has actually stopped, at which exact loop; and since we’re planning to resume the file later, we need to be sure the download stopped in good terms.

Next comes the btnStop_Click event that it’s changed just a little: the line that aborts the method is put first, and then the streams are closed. This prevents the streams from being closed before the thread has really aborted. The thread might still try to access the streams and we don’t want that. One other thing we do is to disable the Pause / Resume button, which makes sence, since the download was fully stopped, there is no such functionality anymore.

// Abort the thread that's downloading

thrDownload.Abort();

// Close the web response and the streams

webResponse.Close();

strResponse.Close();

strLocal.Close();

// Set the progress bar back to 0 and the label

prgDownload.Value = 0;

lblProgress.Text = "Download Stopped";

// Disable the Pause/Resume button because the download has ended

btnPauseResume.Enabled = false;

Next comes a big piece of code that is the new btnPauseResume_Click event. So you should double click the button in the form designer to create the new event automatically, and inside it you should use the following code:

private void btnPauseResume_Click(object sender, EventArgs e)

{

    // If the thread exists

    if (thrDownload != null)

    {

        if (btnPauseResume.Text == "Pause")

        {

            // The Pause/Resume button was set to Pause, thus pause the download

            goPause = true;

 

            // Now that the download was paused, turn the button into a resume button

            btnPauseResume.Text = "Resume";

 

            // Close the web response and the streams

            webResponse.Close();

            strResponse.Close();

            strLocal.Close();

            // Abort the thread that's downloading

            thrDownload.Abort();

        }

        else

        {

            // The Pause/Resume button was set to Resume, thus resume the download

            goPause = false;

 

            // Now that the download was resumed, turn the button into a pause button

            btnPauseResume.Text = "Pause";

 

            long startPoint = 0;

 

            if (File.Exists(txtPath.Text))

            {

                startPoint = new FileInfo(txtPath.Text).Length;

            }

            else

            {

                MessageBox.Show("The file you choosed to resume doesn't exist.", "Could not resume", MessageBoxButtons.OK, MessageBoxIcon.Error);

            }

 

            // Let the user know we are connecting to the server

            lblProgress.Text = "Download Resuming";

            // Create a new thread that calls the Download() method

            thrDownload = new Thread(new ParameterizedThreadStart(Download));

            // Start the thread, and thus call Download()

            thrDownload.Start(startPoint);

            // Enable the Pause/Resume button

            btnPauseResume.Enabled = true;

        }

    }

    else

    {

        MessageBox.Show("A download does not appear to be in progress.", "Could not pause", MessageBoxButtons.OK, MessageBoxIcon.Error);

    }

}

The code along with the comments should be self explanatory. We first check to see if the buttons are set to pause or resume the download by looking at their caption, and then take the proper action.

Hoping that we improved the application enough in this tutorial, I’m going to end it here. You should be aware that if you are looking to turn this into a fully functional and reliable download manager, there are several checks and tweaks that you need to apply to the code, since there are a couple of known problems that can break the application. For example if the Pause/Resume button is clicked repeatedly very fast, it can result in an exception. This will rarely happen, though you’ll want to prevent or catch this exception.

If there’s sufficient demant, a third part of this tutorial will follow in which we manage a list of downloads and allow multiple downloads at once.

Nathan Pakovskie is an esteemed senior developer and educator in the tech community, best known for his contributions to Geekpedia.com. With a passion for coding and a knack for simplifying complex tech concepts, Nathan has authored several popular tutorials on C# programming, ranging from basic operations to advanced coding techniques. His articles, often characterized by clarity and precision, serve as invaluable resources for both novice and experienced programmers. Beyond his technical expertise, Nathan is an advocate for continuous learning and enjoys exploring emerging technologies in AI and software development. When he’s not coding or writing, Nathan engages in mentoring upcoming developers, emphasizing the importance of both technical skills and creative problem-solving in the ever-evolving world of technology. Specialties: C# Programming, Technical Writing, Software Development, AI Technologies, Educational Outreach

Leave a Reply

Your email address will not be published. Required fields are marked *

Back To Top