Introduction
This utility is meant to give a simple, yet concise, demonstration of how to successfully implement custom events and threading in a .NET forms application. I will cover the entire application, as well as explain why using a separate thread and events are necessary to obtain more desirable functionality. Although the application could be achieved without events or threading, a separate worker thread allows the user interface to run efficiently instead of locking the form until the analysis is complete. Additionally, by creating events, we can keep the user informed while the analysis does its job.
Background
This began as a quick and dirty solution to scanning and reporting the size of hundreds of user folders. I was pretty happy about having a working application after 15 minutes that would be sufficient for meeting the requirements. However, I noticed that a label, which was supposed to show the last file scanned, was not updating until after the analysis was complete.
After a weekend of scratching my head over why this wouldn't work, I stumbled across threading and how it might be able to help me with my issue. In researching this possible solution, and finally implementing it properly, another issue crept up in my design. It was an issue with cross-threading which was disallowing me from updating the simple little label sitting on my form. Not only was it disallowing me from updating the label control but because the worker thread was separate from the Form, I had no way to reference it at all.
A little more research yielded some insight into my newly discovered problem. I found that it is possible to "Invoke" a method from a separate thread which bridges the gap between the two threads and allows me to pass information from the worker thread into my main form. By using events to communicate back to my form, I was able to pass custom messages into a function that updates the information successfully.
This proved to be quite a task to undergo, and I felt the number of examples available on the net were somewhat sparse. However, I learned a great deal more where my knowledge was limited. Hopefully this will help others with similar troubles.
Using the Code
I have made the source available which can be downloaded at the top of the page. I would encourage anyone to download this to further aid in my explanation.
I'll start with the Analyzer
class and finish with the main code in order to tie everything together. The concept is simple: Loop through all of the directories to keep a running list of all the contained files' sizes.
Since the application utilizes events, we need to make use of delegates. This provides us with a way to trigger events throughout the lifetime of the worker process. The delegates will come into play when we need to forward an event to the appropriate function. Here's the first part of the Analyzer
class :
using System.IO;
using System;
using System.Threading;
namespace FederNET.FileUtils
{
public delegate void FileAnalysisOperation(object sender, FileAnalysisEventArgs e);
Next, I want to implement something I find to be very handy when working the different types of events. I have defined an enum
which will serve the purpose of identifying the message type sent to the event handler. The reason I use this will become clear in my explanation.
enum FileAnalysisMessageType { FileComplete, AnalysisStarted, AnalysisComplete }
The delegate made a reference to FileAnalysisEventArgs
. This is a custom class which derives from the base EventArgs
class. This will store information about the current operation. I am interested in tracking the number of files scanned, the current file name and the message type.
class FileAnalysisEventArgs : EventArgs
{
private string _message;
public string Message
{
get { return _message; }
}
private static int _count = 0;
public int Count
{
get { return _count; }
}
private FileAnalysisMessageType _messageType;
public FileAnalysisMessageType MessageType
{
get { return _messageType; }
}
public FileAnalysisEventArgs
(string message ,FileAnalysisMessageType messageType)
{
if (messageType == FileAnalysisMessageType.FileComplete)
_count++;
_message = message;
_messageType = messageType;
}
}
Now that we have defined the necessary code to handle the event messages, we can begin writing our Analyzer
class. Apart from the usual properties, I have defined an event which we will use to let the user know when the analysis has started, which file it scanned last, and when the analysis has finished. As noted above, the message type enumeration helps us here because we can store the message type inside FileAnalysisEventArgs
. This keeps us from needing to deal with multiple events and can channel all messages through a single event declaration.
Notice the event declaration points to the FileAnalysisOperation
delegate defined previously.
public class FileAnalyzer
{
public event FileAnalysisOperation FileAnalysisOpComplete;
private string folderToAnalyze = "";
public string FolderToAnalyze
{
get { return folderToAnalyze; }
set { this.folderToAnalyze = value; }
}
private string outputPath = "";
public string OutputPath
{
get { return outputPath; }
set { this.outputPath = value; }
}
public FileAnalyzer()
{
}
Next, we can define our main function, Analyze
. After making sure the user has set a path to scan and a path for the output, we need to trigger an event that our analysis has started. We do this by calling FileAnalysisOpComplete
as defined in our event declaration above. (with a message type of AnalysisStarted
). After first checking to make sure the event isn't a null
reference, we call the event and pass a reference of the Analyzer
object (this
) as well as the FileAnalysisEventArgs
. This is expecting a message and message type (remember the enum
?)
public void Analyze()
{
if (this.OutputPath != "" && this.FolderToAnalyze != "")
{
if(FileAnalysisOpComplete != null)
FileAnalysisOpComplete(this, new FileAnalysisEventArgs
("File Analysis Started", FileAnalysisMessageType.AnalysisStarted));
You might be wondering what happens next... well, potentially nothing. All we told the application to do was to raise an event. That does not mean that we care about it or plan to do anything with it. Of course, we DO care and we will do something with it but for now, we only want to make sure an event is being triggered. We will register for the events later when we take a look at the code in the main Form.
Moving on, we want to get a list of all the directories within the directory specified and then loop through each one to get the desired directory info. This is pretty straight forward. The function, GetFolderSize
, will be shown next. Notice that I have triggered another event, FileAnalysisOpComplete
with a message type of AnalysisComplete
. It is important to know that when we trigger the event, the event references the delegate which was defined at the top of our code file. All of these calls must use the same format for the information to be passed appropriately.
DirectoryInfo dirInfo = new DirectoryInfo(this.FolderToAnalyze);
DirectoryInfo[] diArray = dirInfo.GetDirectories();
using (TextWriter writer = new StreamWriter(this.OutputPath + "directoryinfo.txt"))
{
for (int i = 0; i < diArray.Length; i++)
{
string folderName = diArray[i].Name;
string time = diArray[i].LastWriteTime.ToString();
double size = GetFolderSize(diArray[i].FullName);
writer.WriteLine(folderName + ";" + time + ";" + (size / 1024) / 1024);
}
writer.Close();
}
if(FileAnalysisOpComplete != null)
FileAnalysisOpComplete(this, new FileAnalysisEventArgs
("File Analysis Complete", FileAnalysisMessageType.AnalysisComplete));
}
}
The GetFolderSize
function is a recursive function which drills down into each subfolder contained within the specified path. During this process, I want the user to be shown each file being scanned so I will trigger the last of three events inside this function. Again, notice the consistent format of the event triggering which needs to match the format of the delegate, FileAnalysisOpComplete
with a message type of FileComplete
.
private long GetFolderSize(string path)
{
long dirSize = 0;
bool IncludeSubfolders = true;
DirectoryInfo dirInfo = new DirectoryInfo(path);
try
{
foreach (FileInfo fInfo in dirInfo.GetFiles())
{
dirSize += fInfo.Length;
if (FileAnalysisOpComplete != null)
FileAnalysisOpComplete(this, new FileAnalysisEventArgs
(fInfo.Name, FileAnalysisMessageType.FileComplete));
}
if (IncludeSubfolders)
{
foreach (DirectoryInfo dirInfoSubFolder in dirInfo.GetDirectories())
{
dirSize += GetFolderSize(dirInfoSubFolder.FullName);
}
}
}
catch (Exception ex)
{
}
return dirSize;
}
}
}
At this point, we are finished with the file analysis class. This could, of course, be extended with much greater functionality but what started as a simple solution to a simple problem became a good example for something far more important, events, delegation and what I am about to explain, threading.
In our main form, we can make use of this class and its events. One of the problems I had faced during this simple application was that I could not successfully update the status label text while the functions were iterating over the directories and files. This is because the operation can be fairly large when scanning through possibly thousands of files. In order to allow the label text to be updated with status messages, I had to create a separate thread for this to run on. It turns out, this is a pretty simple thing to do.
Here's the start of our main application. In the main Form constructor, we want to register for the events we created in the Analyzer
class. This is the point where we care about the events and what is happening while the directories are being scanned.
One thing you should note is that we only need to register for one event. Since we defined an enumeration to hold the message type (AnalysisStarted
, AnalysisComplete
and FileComplete
), we can look at the type to determine what kind of information is being passed to us. We could define three separate events, and in fact my original design had three events, but with the use of the enumeration it would be redundant.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.Threading;
using FederNET.FileUtils;
namespace SimpleFileAnalyzer
{
public partial class Form1 : Form
{
FileAnalyzer FileAnalyzer = new FileAnalyzer();
The following is a delegate
declaration. We will need this in a moment when our events are triggered.
private delegate void UpdateStatusLabel(FileAnalysisEventArgs e);
public Form1()
{
InitializeComponent();
FileAnalyzer.FileAnalysisOpComplete
+= new FileAnalysisOperation(FileAnalyzer_FileAnalysisOpComplete);
}
Now that we have registered to receive the events from our Analyzer
class, we need to define the handler function to do something with these events.
Please note the use of "Invoke
" - this is required because when we call the analyze function, we will be starting it on a thread separate from the main form. In order for the messages to be handled properly, we must establish a bridge between the two threads or we will be dead in the water... so to speak.
The following also makes use of the delegate
we define at the top of our code file. This will "delegate
" the FileAnalysisEventArgs
to the UpdateStatus
function.
void FileAnalyzer_FileAnalysisOpComplete(object sender, FileAnalysisEventArgs e)
{
this.Invoke(new UpdateStatusLabel(UpdateStatus), e);
}
The Update
status function simply takes the FileAnalysisEventArgs
, as defined by the delegate
, and looks at the message type, the message, and the total file count. Depending on what type of message it is, the status label and/or groupbox text is updated appropriately. I have put the status label in a groupbox control on the form.
private void UpdateStatus(FileAnalysisEventArgs message)
{
switch (message.MessageType)
{
case FileAnalysisMessageType.FileComplete:
lblStatus.Text = message.Message;
break;
case FileAnalysisMessageType.AnalysisStarted :
gbStatus.Text = message.Message;
break;
case FileAnalysisMessageType.AnalysisComplete :
gbStatus.Text = message.Message;
lblStatus.Text = "Total files scanned :" + message.Count.ToString();
break;
}
}
We need to handle the click events of the buttons on the main form - this is built in event functionality and should not require any explanation, but I have included it for completeness.
private void btnChooseSource_Click(object sender, EventArgs e)
{
fbDialog.ShowDialog();
FileAnalyzer.FolderToAnalyze = fbDialog.SelectedPath;
txtSource.Text = FileAnalyzer.FolderToAnalyze;
}
private void btnChooseOutput_Click(object sender, EventArgs e)
{
fbDialog.ShowDialog();
FileAnalyzer.OutputPath = fbDialog.SelectedPath;
txtOutput.Text = FileAnalyzer.OutputPath;
}
Last but certainly not least is the call to the Analyze
function. We have done a fair amount of work to get this set up and I commend anyone who has stayed with me thus far. The major thing to note here is the Thread
function. Because the analysis is a potentially heavy operation, I don't want it to bog down my UI form. If I don't execute this function on a separate thread, my UI will stop moving and will not update the status messages as needed. This makes the interface look like it has locked up when it truly hasn't.
private void btnAnalyze_Click(object sender, EventArgs e)
{
if (FileAnalyzer.FolderToAnalyze == "" && FileAnalyzer.OutputPath == "")
{
FileAnalyzer.FolderToAnalyze = txtSource.Text;
FileAnalyzer.OutputPath = txtOutput.Text;
}
Thread newThread = new Thread(new ThreadStart(FileAnalyzer.Analyze));
newThread.Start();
}
}
}
Hopefully this helps anyone struggling with the topics covered. It turns out we have covered a fair amount beyond what I would consider to be the basics. If you have any questions, feel free to let me know.
Points of Interest
It took a long time for me to grasp the idea behind delegates and I'm still learning. It's difficult to understand when a call to a function should simply go to the said function, right? However, after dealing with events, I have gained a much greater understanding of delegation and have a much deeper appreciation of how useful and cool they can be.
History
- 17th September, 2007 - I want to thank Steve Hansen for his suggestion to consolidate the events in my application. As mentioned in the updated article, by using an enumeration to specify the type of message, I didn't have a need to define multiple events. Rather, I could look at the message type and determine how to proceed with the event handling. I have posted both versions if anyone is interested in seeing what I have changed.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.