Click here to Skip to main content
Click here to Skip to main content

Directory Analysis with Custom Events and Threading

, 16 Sep 2007 CPOL
Rate this:
Please Sign up or sign in to vote.
A simple directory scanner which operates on a separate worker thread and updates a status label without causing the UI to hang.

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
{
    // Define method to be called any time a file has been scanned. 
    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.

// Keeps track of the last file name scanned and the total files scanned
class FileAnalysisEventArgs : EventArgs
{
    private string _message;
    public string Message
    {
	get { return _message; }
    }
    // set the counter to static so we can keep a running total instead of 
    // resetting the count to 0 each time this is called.
    private static int _count = 0;
    public int Count
    {
	get { return _count; }
    }
    // This is from the enum mentioned previously
    private FileAnalysisMessageType _messageType; 
    public FileAnalysisMessageType MessageType
    {
	get { return _messageType; }
    }
    public FileAnalysisEventArgs
	(string message ,FileAnalysisMessageType messageType)
    {
	// if the type of message is a "file complete" message, 
	// increment the file counter.
	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.

// Main File Analyzer class to perform lengthy file analysis
public class FileAnalyzer
{
    // Define event to be registered in our main Form class
    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()
    {
	// No constructor logic needed for this example
    }

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?)

// Main analyze function - will operate on a separate thread in this example
public void Analyze()
{
    // Make sure the appropriate paths have been set
    if (this.OutputPath != "" && this.FolderToAnalyze != "")
    {  
	// Trigger "File Analysis Started" event 
	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();
    // Create a new text file in the specified output directory 
    // to log directory stats
    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();
    } 
    // Trigger "File Analysis Finished" event
    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;
            // Default to include subdirectories - 
            // method could be update to accept this as a parameter
            bool IncludeSubfolders = true;
            DirectoryInfo dirInfo = new DirectoryInfo(path);
            try
            {
                foreach (FileInfo fInfo in dirInfo.GetFiles())
                {
                    dirSize += fInfo.Length;
                    // Trigger "Current File Analysis Complete" event
                    if (FileAnalysisOpComplete != null)
                         FileAnalysisOpComplete(this, new FileAnalysisEventArgs
			(fInfo.Name, FileAnalysisMessageType.FileComplete));
                }
                if (IncludeSubfolders)
                {
                    foreach (DirectoryInfo dirInfoSubFolder in dirInfo.GetDirectories())
                    {
                        // Use recursion to drill down into the top level directory
                        dirSize += GetFolderSize(dirInfoSubFolder.FullName);
                    }
                }
            }
            catch (Exception ex)
            {
                // Handle any exceptions here
            }
            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.

// This delegate holds a method signature that will allow us to pass
// any messages generated by the File Analyzer methods to our local methods.
private delegate void UpdateStatusLabel(FileAnalysisEventArgs e);

public Form1()
{
    InitializeComponent();
    // Register to receive events raised in our Analyzer class.
    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)
    {
        // Three message types can be stored in the custom FileAnalysisEventArgs class
        //      "FileComplete", "AnalysisStarted" and "AnalysisComplete"
        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)
{
    // Show the Windows folder browser dialog and set the selected
    // path to the textbox on the form.
    fbDialog.ShowDialog();
    FileAnalyzer.FolderToAnalyze = fbDialog.SelectedPath;
    txtSource.Text = FileAnalyzer.FolderToAnalyze;
}

private void btnChooseOutput_Click(object sender, EventArgs e)
{
    // Show the Windows folder browser dialog and set the selected
    // path to the textbox on the form.
    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;
            }         
            // Start a new thread for executing the file analysis function
            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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

David Federspiel
Web Developer
United States United States
No Biography provided

Comments and Discussions

 
GeneralThank you! Pinmemberkrillgar20-Feb-12 11:34 
GeneralWhy not use BackgroundWorker Pinmemberlextm23-Sep-07 20:47 
GeneralRe: Why not use BackgroundWorker PinmemberDavid Federspiel24-Sep-07 17:14 
GeneralAsyncCompletedEventArgs Pinmemberr.chiodaroli17-Sep-07 2:12 
GeneralRe: AsyncCompletedEventArgs PinmemberDavid Federspiel17-Sep-07 5:24 
GeneralEvents PinmemberSteve Hansen16-Sep-07 22:23 
GeneralRe: Events Pinmemberfedermonkey17-Sep-07 4:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.141216.1 | Last Updated 16 Sep 2007
Article Copyright 2007 by David Federspiel
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid