.NET Forum / Languages / C# / March 2008
Threading issue: Timers.Timer and Forms.BackgroundWorker
|
|
Thread rating:  |
Arthur - 28 Mar 2008 19:27 GMT I developed a simple windows forms application (for controlling a service application).
As there are no events of the ServiceController class (e.g. "State of the service applioaction has changed") the forms application can subscribe to, it polls the state of the service through the ServiceController class every one seconds.
This is done by a Timer object "t" with a "t_elapsed" eventhandler subscribed to the "elapsed" event.
When the eventhandler tries to update controls of the form an exception is thrown, as the controls belong to another thread than the one where the "t_elapsed" eventhandler runs. the eventhandler runs in the "Timer" thread as far as i understood the matter.
So far everything works as we all expected it.
Here is a codeexample for the above described: public partial class Form1 : Form { ServiceController sc; public System.Timers.Timer t;
public Form1() { InitializeComponent();
this.sc = new ServiceController("SomeService"); this.t = new System.Timers.Timer(1000); this.t.Elapsed += new ElapsedEventHandler(t_Elapsed); this.t.Start(); }
protected void t_Elapsed(object sender, ElapsedEventArgs e) { //cannot update controls of the form from here. } }
The msdn2 says, the prefered way to solve cross thread control access issues is to use a backgroundworker. And it plots the way like this:
.add a backgroundworker to the form .register a handler for its "RunWorkerCompleted" event .call the "RunWorkerAsync" method, when something needs to be done .do the updating of the controls safely from the handler that subscribes to the "RunWorkerCompleted" event
...which imediately arouse the question "Why should this work as the handler will as well run in another thread than the one that owns the controls?"
And indeed, it doesn't work this way either.
Here is a code example for the 2nd try:
public partial class Form1 : Form { ServiceController sc; public System.Timers.Timer t;
public Form1() { InitializeComponent();
this.sc = new ServiceController("MyFirstService"); this.t = new System.Timers.Timer(1000); this.t.Elapsed += new ElapsedEventHandler(t_Elapsed); this.t.Start(); }
protected void t_Elapsed(object sender, ElapsedEventArgs e) { this.backgroundWorker1.RunWorkerAsync(); }
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { //cannot access forms controls from here either } }
So what about the backgroundworker? Can anyone explain the pros of the BGWorker to me and why it does not work in my example (or better, how i should put it to work)?
My actual solution - which works :-) - is to use "BeginnInvoke" in "t_elapsed" to access the controls through the UI thread.
But i would like to gather knowledge and wisdom about the BGWorker solution too :-)
Thanks in advance, Arthur
Ignacio Machin ( .NET/ C# MVP ) - 28 Mar 2008 20:01 GMT > I developed a simple windows forms application (for controlling a > service application). [quoted text clipped - 93 lines] > Thanks in advance, > Arthur Hi,
You should use Control.Invoke , you can search the archives of this NG as this is a recurrent question.
adamroot@microsoft.com - 28 Mar 2008 20:25 GMT > I developed a simple windows forms application (for controlling a > service application). [quoted text clipped - 93 lines] > Thanks in advance, > Arthur Do you need to use the System.Timers.Timer instead of the System.Windows.Forms.Timer? The SWF timer supports the component model, so that when your form is disposed, it will properly dispose. I say this because you may run into other synchronization problems (depending when/how do you stop the timer events; ie if your timer elapses after the form is disposed, then it tries to access the controls, you will get an exception). Anyway, assuming you have that under control, you can use something like this in your t_Elapsed function (forget the BackgroundWorker, it is meant for a different purpose, although technically you *could* use it):
protected void t_Elapsed(object sender, ElapsedEventArgs e) { if (InvokeRequired) // If not on the UI thread ... { Invoke(new ElapsedEventHandler(t_Elapsed), sender, e); // ... Invoke the UI thread (can also use the non-blocking BeginInvoke if needed) return; // This thread should not try to access controls }
// If execution got here, this thread is safe to work with controls label1.Text = "Hello World!"; }
adamroot@microsoft.com - 28 Mar 2008 20:28 GMT On Mar 28, 12:25 pm, adamr...@microsoft.com wrote:
> > I developed a simple windows forms application (for controlling a > > service application). [quoted text clipped - 124 lines] > > - Show quoted text - I'm sorry, I'm rusty with Timers... My previous post is valid if you are going to use that specific Timer, but the System.Windows.Forms.Timer class does the Invoke for you! So using that class would clean up your code a bit. Check out this article for in-depth explanations of the differences of the Timer classes in .NET:
http://msdn2.microsoft.com/en-us/magazine/cc164015.aspx
Peter Duniho - 28 Mar 2008 21:02 GMT > [...] > So what about the backgroundworker? Can anyone explain the pros of the > BGWorker to me and why it does not work in my example (or better, how > i should put it to work)? Using BackgroundWorker is in fact preferrable, when it's applicable. However, the situation you're dealing with isn't applicable to the use of BackgroundWorker. As Ignacio says, the most direct solution is to just use Invoke() or BeginInvoke(), as you're doing now.
BackgroundWorker is, as the name implies, more for situations where you have some task you want to execute in the background, without blocking the main GUI thread. It's not required for addressing that situation either, but because the ProgressChanged and RunWorkerCompleted events are automatically invoked back onto the GUI thread (when you create the BackgroundWorker from the GUI thread), it provides a very convenient interface for dealing with the cross-thread issues as long as your processing fits the specific model that BackgroundWorker exposes.
Most long-running tasks do in fact fit, so it's a good solution for most long-running tasks. But that's not the scenario you seem to be addressing.
Finally, you may want to consider Adam's advice as well. You're running into this because of the particular Timer class you chose to use. The issue doesn't come up with the Forms.Timer class, not because the class uses Invoke() on your behalf, but rather because that timer uses the Windows WM_TIMER message and so timer events are automatically dealt with on the GUI thread (since that's where all window messages are handled).
Unless you have a specific need to use the Timers.Timer class, you may be better off just using the Forms.Timer class.
Pete
vvnraman - 28 Mar 2008 21:11 GMT On Mar 29, 1:02 am, "Peter Duniho" <NpOeStPe...@nnowslpianmk.com> wrote:
> > [...] > > So what about the backgroundworker? Can anyone explain the pros of the [quoted text clipped - 29 lines] > > Pete Hi Arthur Its good if you already know about the BeginInvoke() method. Anyways i'll illustrate it again along with the BackgroundWorker.
I'll use an example here. Suppose you have an applilcation where the user can chose any image folder from disk and the app will display the thumbnails of those images one by one. Now thumbnail generation is a relatively slow procedure and if the operation is carried out on the UI thread, the app will hang and will go to the NOT RESPONDING state.
The correct way to do this would be : After the user selects a folder from the disk (using a folderDialog dialog) 1. Get the list of image files which will be a string array of file paths and start a background thread to generate the thumbnail of the image. 2. Report the serial no. of file being processed to the main form from within the background thread by raising the ProgressChanged event. 3. Handle the ProgressChanged event of the background thread and then call BeginInvoke to execute the method which actually displays the progress but runs on the UI thread.
I made a small application and its working fine. I don't know how to upload files here so i'll copy paste the code. Its a simple thumbnail browser. Create a new windows application and 1. Label with Text = "Folder Path" 2. TextBox with name "txtFolderPath" 3. Button Text = "Browse" Name = "btnBrowse" 4. Button Text = "Generate Thumbnails" Name = "btnGenerate" 5. Button Text = "Clear Thumbnails" Name = "btnClear" 6. A FlowLayoutPanel 7. A StatusBar and add a StatusLabel and a ProgressBar to it.
Here's the code
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms;
namespace ThumbnailBrowser { public partial class Form1 : Form { public Form1() { InitializeComponent(); }
private BackgroundWorker bgWorker = new BackgroundWorker();
private void btnBrowse_Click(object sender, EventArgs e) { using (FolderBrowserDialog fbd = new FolderBrowserDialog()) { fbd.ShowNewFolderButton = false; if (fbd.ShowDialog() == DialogResult.OK) { txtFolderPath.Text = fbd.SelectedPath; } } }
private void btnGenerate_Click(object sender, EventArgs e) { try { string[] sImageFiles = System.IO.Directory.GetFiles(txtFolderPath.Text, "*.jpg"); if (sImageFiles != null && sImageFiles.Length > 0) { bgWorker.DoWork += new DoWorkEventHandler(bgWorker_GenerateThumbnails); bgWorker.ProgressChanged += new ProgressChangedEventHandler(bgWorker_ProgressChanged); bgWorker.WorkerReportsProgress = true; bgWorker.WorkerSupportsCancellation = true; bgWorker.RunWorkerAsync(sImageFiles); } } catch (Exception exp) { MessageBox.Show(exp.Message, "Error"); } }
public delegate void UpdateStatusAndAppendThumbnailDelegate(ProgressChangedEventArgs e);
private void bgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { object[] oArgs = new object[] { e }; this.BeginInvoke(new UpdateStatusAndAppendThumbnailDelegate(UpdateStatusAndAppendThumbnail), oArgs); }
private void UpdateStatusAndAppendThumbnail(ProgressChangedEventArgs e) { toolStripProgressBar1.Value = e.ProgressPercentage; toolStripStatusLabel1.Text = e.ProgressPercentage.ToString() + " % complete."; AppendThumbnail((Image)e.UserState); }
private void AppendThumbnail(Image thumbnail) { PictureBox pb = new PictureBox(); pb.SizeMode = PictureBoxSizeMode.Zoom; pb.Image = thumbnail; this.flowLayoutPanel1.Controls.Add(pb); }
public bool ThumbnailCallback() { return false; }
private void bgWorker_GenerateThumbnails(object sender, DoWorkEventArgs e) { try { BackgroundWorker currBgWorker = sender as BackgroundWorker; Size szContentSize = new Size(170, 170); string[] sImageFiles = (string[])e.Argument; int i = 0; int iTotal = sImageFiles.Length; foreach (string sFile in sImageFiles) { i++; Image img = null; img = Image.FromFile(sFile);
// Determine scaling for new rectangle: double xScale = (1.0 * img.Width) / szContentSize.Width; double yScale = (1.0 * img.Height) / szContentSize.Height; Rectangle destRect; if (xScale > yScale) { if (img.Height / xScale < szContentSize.Height) { destRect = new Rectangle(0, 0, (int) (img.Width / xScale), (int)(img.Height / xScale)); } else { destRect = new Rectangle(0, 0, (int) (img.Width / yScale), (int)(img.Height / yScale)); } } else { if (img.Width / yScale < szContentSize.Width) { destRect = new Rectangle(0, 0, (int) (img.Width / yScale), (int)(img.Height / yScale)); } else { destRect = new Rectangle(0, 0, (int) (img.Width / xScale), (int)(img.Height / xScale)); } } Size szThumbnailSize = destRect.Size; Image.GetThumbnailImageAbort myCallback = new Image.GetThumbnailImageAbort(ThumbnailCallback); Image imgThumbnail = img.GetThumbnailImage( szThumbnailSize.Width, szThumbnailSize.Height, myCallback, IntPtr.Zero); object[] oArgs = new object[] { imgThumbnail }; int iProgressPercent = (i*100)/iTotal; currBgWorker.ReportProgress(iProgressPercent, imgThumbnail); } } catch (Exception exp) { MessageBox.Show(exp.Message, "Error"); } }
private void btnClear_Click(object sender, EventArgs e) { foreach (Control ctl in this.flowLayoutPanel1.Controls) { ctl.Dispose(); } this.flowLayoutPanel1.Controls.Clear(); } } }
This is working fine. You can add handlers to cancel the generation of thumbnails and some other code to prevent the user from clicking the Generate Thumbnail button when one operation is already in progress.
Regards Prateek Raman
Free MagazinesGet these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...
|
|
|