Reputation: 2700
I'm using DownloadData from WebClient object to download favicons from couple of websites.
I'm receiving the response by a Byte array and everything works out well, except for one thing: when the DownloadData method gets executed, it'll freeze my Form until the method returns.
Now, I have solved this by using a BackgroundWorker object to get the job done, but I'm curious how would I achieve the same thing using System.Threading.Thread.
I tried creating another Thread that does download the favicons, and then looped my mainThread until the Thread is finished processing and then used Abort() method to abort the thread, but so far my Form gets frozen during the execution of the other Thread.
This is the code I used to create the other Thread:
bool downloadFavIcon_Completed = false;
private void downloadFavIcon()
{
downloadFavIcon_Completed = false;
Byte[] dl;
System.IO.MemoryStream dlMem;
Bitmap favCollection = new Bitmap(96, 64);
Graphics g = Graphics.FromImage(favCollection);
Bitmap dlImg;
String[] addr = new String[24];
addr[0] = @"http://google.com/favicon.ico";
addr[1] = @"http://microsoft.com/favicon.ico";
addr[2] = @"http://freesfx.com/favicon.ico";
addr[3] = @"http://yahoo.com/favicon.ico";
addr[4] = @"http://downloadha.com/favicon.ico";
addr[5] = @"http://hp.com/favicon.ico";
addr[6] = @"http://bing.com/favicon.ico";
addr[7] = @"http://webassign.com/favicon.ico";
addr[8] = @"http://youtube.com/favicon.ico";
addr[9] = @"https://twitter.com/favicon.ico";
addr[10] = @"http://cc.com/favicon.ico";
addr[11] = @"http://stackoverflow.com/favicon.ico";
addr[12] = @"http://vb6.us/favicon.ico";
addr[13] = @"http://facebook.com/favicon.ico";
addr[14] = @"http://flickr.com/favicon.ico";
addr[15] = @"http://linkedin.com/favicon.ico";
addr[16] = @"http://blogger.com/favicon.ico";
addr[17] = @"http://blogfa.com/favicon.ico";
addr[18] = @"http://metal-archives.com/favicon.ico";
addr[19] = @"http://wordpress.com/favicon.ico";
addr[20] = @"http://metallica.com/favicon.ico";
addr[21] = @"http://wikipedia.org/favicon.ico";
addr[22] = @"http://visualstudio.com/favicon.ico";
addr[23] = @"http://evernote.com/favicon.ico";
for (int i = 0; i < addr.Length; i++)
{
using (System.Net.WebClient client = new System.Net.WebClient())
{
try
{
dl = client.DownloadData(addr[i]);
dlMem = new System.IO.MemoryStream(dl);
dlImg = new Bitmap(dlMem);
}
catch (Exception)
{
dlImg = new Bitmap(Properties.Resources.defaultFavIcon);
}
}
g.DrawImage(dlImg, (i % 6) * 16, (i / 6) * 16, 16, 16);
}
passAddDisplay.Image = favCollection;
downloadFavIcon_Completed = true;
}
private void button2_Click(object sender, EventArgs e)
{
Thread downloader = new Thread(new ThreadStart(downloadFavIcon));
downloader.Start();
while (!downloader.IsAlive) ;
while (!downloadFavIcon_Completed) ;
downloader.Abort();
}
NOTE: passAddDisplay is a pictureBox already placed on my form.
How can I improve my application to avoid getting frozen during the execution of WebClient.DownloadData? (I don't want to use Application.DoEvents())
Upvotes: 2
Views: 852
Reputation: 117010
I would look at using Microsoft's Reactive Framework for this. It automatically handles background threading, and will very efficiently clean up all disposable references. NuGet "Rx-Main" & "Rx-WinForms"/"Rx-WPF".
First, start with your array of addresses:
var addr = new []
{
"http://google.com/favicon.ico",
// DELETED FOR BREVITY
"http://evernote.com/favicon.ico",
};
Now, define a query to asynchronously go get your images:
var query =
from a in addr.ToObservable().Select((url, i) => new { url, i })
from dl in Observable
.Using(
() => new System.Net.WebClient(),
wc => Observable.FromAsync(() => wc.DownloadDataTaskAsync(a.url)))
from bitmap in Observable
.Using(
() => new System.IO.MemoryStream(dl),
ms => Observable.Start(() => new Bitmap(ms)))
.Catch(ex => Observable.Return(new Bitmap(Properties.Resources.defaultFavIcon)))
select new { x = (a.i % 6) * 16, y = (a.i / 6) * 16, bitmap };
Finally, wait for all of the images to come in, then, on the UI thread, create the composite image and assign it to the passAddDisplay
control.
query
.ToArray()
.ObserveOn(passAddDisplay)
.Subscribe(images =>
{
var favCollection = new Bitmap(96, 64);
using(var g = Graphics.FromImage(favCollection))
{
foreach (var image in images)
{
g.DrawImage(image.bitmap, image.x, image.y, 16, 16);
image.bitmap.Dispose();
}
}
passAddDisplay.Image = favCollection;
});
I tested the query and it works fine.
Upvotes: 0
Reputation: 12683
Welcome to Stack Overflow...
By examining your loop it is apparent that you are locking the UI thread in your loops.
while (!downloader.IsAlive) ;
while (!downloadFavIcon_Completed) ;
Is the reason why your UI locks up. Ideally this would be a job for the background worker as it is designed to run in the background and provides events to plumb back to your UI thread. This is possible to write using a Thread however the background worker should be used.
Now if you really wanted to write this using a Thread
object then I would suggest you make a class for your downloader and create events. Therefore we can write simple events such as
I DONT RECOMMEND THIS APPROACH (read further down)
Simply start by creating a new class Downloader
and here is a sample (bare minimum)
public class Downloader
{
/// <summary>
/// Delegate Event Handler for the downloading progress
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public delegate void DownloaderProgressEventHandler(Downloader sender, DownloaderProgressEventArgs e);
/// <summary>
/// Delegate Event Handler for the completed event
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public delegate void DownloaderCompletedEventHandler(Downloader sender, DownloaderCompletedEventArgs e);
/// <summary>
/// The completed event
/// </summary>
public event DownloaderCompletedEventHandler Completed;
/// <summary>
/// The cancelled event
/// </summary>
public event EventHandler Cancelled;
/// <summary>
/// the progress event
/// </summary>
public event DownloaderProgressEventHandler Progress;
/// <summary>
/// the running thread
/// </summary>
Thread thread;
/// <summary>
/// the aborting flag
/// </summary>
bool aborting = false;
//the addresses
String[] addr = new String[] {
"http://google.com/favicon.ico",
"http://microsoft.com/favicon.ico",
"http://freesfx.com/favicon.ico",
"http://yahoo.com/favicon.ico",
"http://downloadha.com/favicon.ico",
"http://hp.com/favicon.ico",
"http://bing.com/favicon.ico",
"http://webassign.com/favicon.ico",
"http://youtube.com/favicon.ico",
"https://twitter.com/favicon.ico",
"http://cc.com/favicon.ico",
"http://stackoverflow.com/favicon.ico",
"http://vb6.us/favicon.ico",
"http://facebook.com/favicon.ico",
"http://flickr.com/favicon.ico",
"http://linkedin.com/favicon.ico",
"http://blogger.com/favicon.ico",
"http://blogfa.com/favicon.ico",
"http://metal-archives.com/favicon.ico",
"http://wordpress.com/favicon.ico",
"http://metallica.com/favicon.ico",
"http://wikipedia.org/favicon.ico",
"http://visualstudio.com/favicon.ico",
"http://evernote.com/favicon.ico"
};
/// <summary>
/// Starts the downloader
/// </summary>
public void Start()
{
if (this.aborting)
return;
if (this.thread != null)
throw new Exception("Already downloading....");
this.aborting = false;
this.thread = new Thread(new ThreadStart(runDownloader));
this.thread.Start();
}
/// <summary>
/// Starts the downloader
/// </summary>
/// <param name="addresses"></param>
public void Start(string[] addresses)
{
if (this.aborting)
return;
if (this.thread != null)
throw new Exception("Already downloading....");
this.addr = addresses;
this.Start();
}
/// <summary>
/// Aborts the downloader
/// </summary>
public void Abort()
{
if (this.aborting)
return;
this.aborting = true;
this.thread.Join();
this.thread = null;
this.aborting = false;
if (this.Cancelled != null)
this.Cancelled(this, EventArgs.Empty);
}
/// <summary>
/// runs the downloader
/// </summary>
void runDownloader()
{
Bitmap favCollection = new Bitmap(96, 64);
Graphics g = Graphics.FromImage(favCollection);
for (var i = 0; i < this.addr.Length; i++)
{
if (aborting)
break;
using (System.Net.WebClient client = new System.Net.WebClient())
{
try
{
byte[] dl = client.DownloadData(addr[i]);
using (var stream = new MemoryStream(dl))
{
using (var dlImg = new Bitmap(stream))
{
g.DrawImage(dlImg, (i % 6) * 16, (i / 6) * 16, 16, 16);
}
}
}
catch (Exception)
{
using (var dlImg = new Bitmap(Properties.Resources.defaultFacIcon))
{
g.DrawImage(dlImg, (i % 6) * 16, (i / 6) * 16, 16, 16);
}
}
}
if (aborting)
break;
if (this.Progress != null)
this.Progress(this, new DownloaderProgressEventArgs
{
Completed = i + 1,
Total = this.addr.Length
});
}
if (!aborting && this.Completed != null)
{
this.Completed(this, new DownloaderCompletedEventArgs
{
Bitmap = favCollection
});
}
this.thread = null;
}
/// <summary>
/// Downloader progress event args
/// </summary>
public class DownloaderProgressEventArgs : EventArgs
{
/// <summary>
/// Gets or sets the completed images
/// </summary>
public int Completed { get; set; }
/// <summary>
/// Gets or sets the total images
/// </summary>
public int Total { get; set; }
}
/// <summary>
/// Downloader completed event args
/// </summary>
public class DownloaderCompletedEventArgs : EventArgs
{
/// <summary>
/// Gets or sets the bitmap
/// </summary>
public Bitmap Bitmap { get; set; }
}
}
Now that is allot of code but lets quickly look at it. To start with we have defined 2 delegates for our Completed and Progress events. These delegates accept the downloader instance as the sender and special event arg classes listed at the bottom. Followed by our 3 events (as listed above) these events will be used to signal changes to the downloader.
Next we have our fields defined.
Thread thread;
This is a reference to the thread that will be created when the 'Start()` methods are called.
bool aborting = false;
This is a simple flag to signal to the thread that we should abort. Now I have decided to use a flag and let the thread gracefully finish as opposed to calling the Thread.Abort()
method. This ensures all the clean up can properly take place.
string[] addres =....
Our initial addresses.
Now this is all simple so far. Next comes our Start()
methods. I have provided two different methods. One of the methods accepts a new string[]
of addresses to download (not that important).
You will notice in this method
/// <summary>
/// Starts the downloader
/// </summary>
public void Start()
{
if (this.aborting)
return;
if (this.thread != null)
throw new Exception("Already downloading....");
this.aborting = false;
this.thread = new Thread(new ThreadStart(runDownloader));
this.thread.Start();
}
The first thing we do is check if the aborting flag is set. If it is then ignore the start call (you could throw an exception). Next we check if the thread is not null. If the thread is not null then our downloader is running. Finally we simply reset our aborting flag to false
and start our new Thread
.
Moving down to the Abort()
method. This method will first check if the aborting
flag is set. If so then do-nothing. Next we set our aborting
flag to true. The next step, and I will warn you this WILL block your calling thread is call the method Thread.Join()
which will join the thread to our calling thread. Basically waits for the thread to exit.
Finally we simply set the thread instance to null, reset the aborting
flag to false and trigger the Cancelled
event (if subscribed).
Next comes the main method that does the downloading. First off you will notice I moved around your variables and used using
statements for the disposable objects. (That is another topic).
The big piece to the runDownloader()
method is that it periodically checks the 'aborting' flag. If this flag is ever set to true
the downloader
stops there. Now note you may have a situation where the abort is called while the WebClient
is downloading an image. Ideally you would let your WebClient
finish the request, dispose of correctly then exit the loops.
After each download of the image the progress event is fired (if subscribed to). Finally when the iterations are finished and all images downloaded the "Completed" event is fired with the compiled bitmap image.
PAUSE FOR A BREATHE
Now this is all great.. BUT how do you use it. Well simple I created a form with a button, progress bar and picturebox. The button will be used to start and stop the downloader, progress bar to handle the progress events and the picture box for the finished image.
Here is a sample program i have commented parts of it.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
this.progressBar1.Visible = false;
this.progressBar1.Enabled = false;
}
Downloader downloader;
/// <summary>
/// starts \ stop button pressed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
//if downloader is not null then abort it
if (downloader != null)
{
downloader.Abort();
return;
}
//setup and start the downloader
this.progressBar1.Value = 0;
this.progressBar1.Minimum = 0;
this.progressBar1.Enabled = true;
this.progressBar1.Visible = true;
this.downloader = new Downloader();
this.downloader.Progress += downloader_Progress;
this.downloader.Completed += downloader_Completed;
this.downloader.Cancelled += downloader_Cancelled;
this.downloader.Start();
}
/// <summary>
/// downloader cancelled event handler
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void downloader_Cancelled(object sender, EventArgs e)
{
this.unhookDownloader();
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate
{
this.progressBar1.Enabled = false;
this.progressBar1.Visible = false;
MessageBox.Show(this, "Cancelled");
});
else
{
this.progressBar1.Enabled = false;
this.progressBar1.Visible = false;
MessageBox.Show(this, "Cancelled");
}
}
/// <summary>
/// downloader completed event handler
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void downloader_Completed(Downloader sender, Downloader.DownloaderCompletedEventArgs e)
{
this.unhookDownloader();
if (this.InvokeRequired)
this.Invoke((MethodInvoker)delegate
{
this.progressBar1.Enabled = false;
this.progressBar1.Visible = false;
this.pictureBox1.Image = e.Bitmap;
MessageBox.Show(this, "Completed");
});
else
{
this.progressBar1.Enabled = false;
this.progressBar1.Visible = false;
this.pictureBox1.Image = e.Bitmap;
MessageBox.Show(this, "Completed");
}
}
/// <summary>
/// downloader progress event handler
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void downloader_Progress(Downloader sender, Downloader.DownloaderProgressEventArgs e)
{
if (this.progressBar1.InvokeRequired)
this.progressBar1.Invoke((MethodInvoker)delegate
{
this.progressBar1.Value = e.Completed;
this.progressBar1.Maximum = e.Total;
});
else
{
this.progressBar1.Value = e.Completed;
this.progressBar1.Maximum = e.Total;
}
}
/// <summary>
/// unhooks the events handlers and sets the downloader to null
/// </summary>
void unhookDownloader()
{
this.downloader.Progress -= downloader_Progress;
this.downloader.Completed -= downloader_Completed;
this.downloader.Cancelled -= downloader_Cancelled;
this.downloader = null;
}
}
This is a simple implementation of how you could use the Thread
object to do your work. In my opinion this is way too much work. Now lets do it in a Background Worker.
I STRONGLY RECOMMEND THIS APPROACH
Why you might say? Well with the Background Worker provides us with the tested and supported methods (plus more) that we tried to implement. You will note that the actual work of downloading the images and detecting cancellation is relatively the same. However you will notice that I am not worried (as much) about cross thread issues when posting the completed and progress events.
private void button2_Click(object sender, EventArgs e)
{
if (this.worker != null && this.worker.IsBusy)
{
this.worker.CancelAsync();
return;
}
string[] addr = new string[] {".... our full addresss lists" };
this.progressBar1.Maximum = addr.Length;
this.progressBar1.Value = 0;
this.progressBar1.Visible = true;
this.progressBar1.Enabled = true;
this.worker = new BackgroundWorker();
this.worker.WorkerSupportsCancellation = true;
this.worker.WorkerReportsProgress = true;
this.worker.ProgressChanged += (s, args) =>
{
this.progressBar1.Value = args.ProgressPercentage;
};
this.worker.RunWorkerCompleted += (s, args) =>
{
this.progressBar1.Visible = false;
this.progressBar1.Enabled = false;
if (args.Cancelled)
{
MessageBox.Show(this, "Cancelled");
worker.Dispose();
worker = null;
return;
}
var img = args.Result as Bitmap;
if (img == null)
{
worker.Dispose();
worker = null;
return;
}
this.pictureBox1.Image = img;
MessageBox.Show(this, "Completed");
worker.Dispose();
worker = null;
};
this.worker.DoWork += (s, args) =>
{
Bitmap favCollection = new Bitmap(96, 64);
Graphics g = Graphics.FromImage(favCollection);
for (var i = 0; i < addr.Length; i++)
{
if (worker.CancellationPending)
break;
using (System.Net.WebClient client = new System.Net.WebClient())
{
try
{
byte[] dl = client.DownloadData(addr[i]);
using (var stream = new MemoryStream(dl))
{
using (var dlImg = new Bitmap(stream))
{
g.DrawImage(dlImg, (i % 6) * 16, (i / 6) * 16, 16, 16);
}
}
}
catch (Exception)
{
using (var dlImg = new Bitmap(Properties.Resources.defaultFacIcon))
{
g.DrawImage(dlImg, (i % 6) * 16, (i / 6) * 16, 16, 16);
}
}
}
if (worker.CancellationPending)
break;
this.worker.ReportProgress(i);
}
if (worker.CancellationPending)
{
g.Dispose();
favCollection.Dispose();
args.Cancel = true;
return;
}
args.Cancel = false;
args.Result = favCollection;
};
worker.RunWorkerAsync();
}
I hope this helps understand a few possible ways to implement what you would like to achieve.
-Nico
Upvotes: 0
Reputation: 367
Yes, it's a synchronous method, which causes the thread to wait without processing messages until it returns; you should try its Async version, which doesn't.
Upvotes: 1