Today I ran across an invaluable resource for Windows Forms development: George Shepherd’s Windows Forms FAQ. The DataGrid section is especially relevant to my recent work.
While the development SQL server is being repaired, I wanted to discuss a class I wrote to assist with writing threaded code in .NET: EventGeneratingThread.
At work I had to create a Windows Forms application which, upon loading, populates a DataGrid using data from a SQL server. Rather than load the data synchronously during the Form.Load event, which would delay the display of the form, I decided to load the data asynchronously using a background thread. However, as I soon found out, if a child thread’s ThreadStart method fails by throwing an Exception, the Exception will be silently lost. This problem manifested itself when the development SQL server recently crashed but no UI error message was displayed.
The simplest solution is to modify your ThreadStart method to catch exceptions and act accordingly:
Thread t = new Thread(new ThreadStart(Process));
t.Start();
void Process()
{
try
{
ReallyProcess();
}
catch (Exception ex)
{
ProcessFailed(ex);
}
}
void ReallyProcess()
{
// Do the actual processing work
}
void ProcessFailed(Exception ex)
{
// Handle a processing failure (display an error dialog box?)
}
The above pattern would then be repeated every time a child thread is created. Therefore, I decided to separate this common code into a class called EventGeneratingThread. EventGeneratingThread strives to duplicate the interface of Thread (unfortunately, it can’t inherit from Thread because Thread is sealed) and adds events to signal whether the provided ThreadStart method completed or generated an exception. In addition to this, I decided to support the ability for a user of EventGeneratingThread to provide an object that implements ISynchronizeInvoke (such as a System.Windows.Forms.Form object), on which the events will be subsequently fired.
Internally, EventGeneratingThread works very similarly to the above code. It takes a ThreadStart method as a parameter, but rather than provide that to the Thread object it creates, it stores it as a member variable and provides its own method, InternalStart, to the created Thread object. InternalStart wraps the call to the stored ThreadStart method in a try/catch block and fires the appropriate event.
While the full Thread interface isn’t implemented yet, here’s EventGeneratingThread as it currently stands:
/// <summary>
/// A thread which generates events which correspond to important
/// events.
/// </summary>
public sealed class EventGeneratingThread
{
private Thread m_t;
private ThreadStart m_realStart;
private ISynchronizeInvoke m_syncInvoke = null;
public EventGeneratingThread(ThreadStart start)
{
m_t = new Thread(new ThreadStart(InternalStart));
m_realStart = start;
}
public event EventHandler Completed;
public event ThreadExceptionEventHandler ThreadException;
public string Name
{
set { m_t.Name = value; }
}
public ThreadPriority Priority
{
set { m_t.Priority = value; }
}
public ISynchronizeInvoke SynchronizeInvoke
{
set { m_syncInvoke = value; }
}
public void Start()
{
m_t.Start();
}
private void InternalStart()
{
try
{
m_realStart();
FireCompleted();
}
catch (Exception ex)
{
FireThreadException(ex);
}
}
private void FireCompleted()
{
Fire(Completed, this, new EventArgs());
}
private void FireThreadException(Exception ex)
{
Fire(ThreadException, this, new ThreadExceptionEventArgs(ex));
}
private void Fire
(
Delegate dlg,
params object[] args
)
{
if (dlg != null)
{
if (m_syncInvoke != null)
{
m_syncInvoke.Invoke(dlg, args);
}
else
{
dlg.DynamicInvoke(args);
}
}
}
}
And here’s an example of its use:
private void MainForm_Load(object sender, System.EventArgs e)
{
this.Cursor = Cursors.WaitCursor;
EventGeneratingThread t = new EventGeneratingThread(new ThreadStart(FillDataSetInBackground));
t.SynchronizeInvoke = this;
t.Completed += new EventHandler(FillDataSetCompleted);
t.ThreadException += new ThreadExceptionEventHandler(FillDataSetFailed);
t.Name = "Populate data grid";
t.Priority = ThreadPriority.BelowNormal;
t.Start();
}
private void FillDataSetInBackground()
{
dsProductList = ... // Fill data set
}
private void FillDataSetCompleted(object sender, EventArgs e)
{
Debug.Assert(!this.InvokeRequired);
dgProductList.DataSource = ... // Set the appropriate data source
ResetCursor();
}
private void FillDataSetFailed(object sender, ThreadExceptionEventArgs t)
{
Debug.Assert(!this.InvokeRequired);
ResetCursor();
string errorMessage = string.Format("Error: Loading data failed.\\n\\nError message:\\n{0}\\n\\nStack trace:\\n{1}", t.Exception.Message, t.Exception.StackTrace);
MessageBox.Show(this, errorMessage, "Error: Loading Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
Application.Exit();
}
Recent Comments