Simple File Search Class
A simple search class to search a directory and all sub-directories in the background for files with a defined search pattern.
Introduction
This is a class I created to recursively search a directory and all sub-directories.
Background
I needed to search for all files in a chosen directory and sub-directories and display them in a list box. But I also needed my Form to be responsive and tell the user that it's working. So I built a class which accepts a BackgroundWorker
from a form and passes back detailed progress information to the form.
Using the Code
Basically, there are a bunch of counters, a "UserState
" struct to hold all status information to be passed back to the form, some properties, and the Search
method does all the heavy lifting.
There's also an EstimateNumSubDirectories
function which loops through all sub-directories up to 3 levels deep. This is so that the percent complete passed back to the BackgroundWorker.ReportProgress
method won't always be nearly 100%.
The Search Method
NOTE: This could be MUCH more easily done by using "System.IO.Directory.GetFiles(directoryToSearch, searchPattern, System.IO.SearchOption.AllDirectories)
". The SearchOption.AllDirectories
does all of the recursive stuff for you, but it doesn't send back update progress reports on a BackgroundWorker
so this didn't work out for me.
Basically it checks to see if the passed directory exists, makes sure the search pattern isn't empty, if a BackgroundWorker
is being used, it checks to see if you've cancelled processing, and if this is the first pass in the loop it runs the EstimateNumSubDirectories
method.
It then searches the directory passed to it for any files with the search pattern also passed to it, gets a list of sub-directories, and each sub-directory gets passed back into the method for another pass until there are no sub-directories left. So it will go all the way down a tree, and then keep going up and down the tree until no branches are left.
IMPORTANT: Since the EstimateNumSubDirectories
method has already got the number of directories found 3 levels deep, the Search
method only adds to the number of directories found if you're more than 3 levels deep.
Here's the code:
/// <summary>
/// Searches the specified directory.
/// </summary>
/// <param name="directoryToSearch">The directory path to search.</param>
/// <param name="searchPattern">The search pattern to use, e.g. "*.doc"</param>
public void Search(string directoryToSearch, string searchPattern)
// recursively look in every subdirectory under the directory passed for documents
{
// check data input
if (!System.IO.Directory.Exists(directoryToSearch))
{
MessageBox.Show("The directory: " + directoryToSearch + " doesn't exist.",
"Directory Not Found", MessageBoxButtons.OK, MessageBoxIcon.Stop);
return;
}
if (searchPattern == string.Empty)
{
MessageBox.Show("You must enter a search pattern.", "No Search Pattern",
MessageBoxButtons.OK, MessageBoxIcon.Stop);
return;
}
// check to see if the user hit the Cancel button
if (_bw != null && _bw.CancellationPending)
{
//bail out
return;
}
/// if this is the first loop, you need to estimate how many directories
/// you will have to search
if (DirectoriesFound == 0)
{
/// if the BackGround worker has been specified and supports updating
/// send a message in the userstate that you are estimating remaining time
if (_bw != null && _bw.WorkerReportsProgress)
{
UserState us = new UserState(Operations.Estimating);
_bw.ReportProgress(bwPercentComplete, us);
}
// estimate number of sub-directories up to 2 levels deep
EstimateNumSubDirectories(directoryToSearch);
_CurrentNumDirectoryLevels = 0;
if (_bw != null && _bw.WorkerReportsProgress)
{
UserState us = new UserState(Operations.Searching, directoryToSearch);
_bw.ReportProgress(bwPercentComplete, us);
}
}
try
{
/// use the GetFiles method to search the provided directory for
/// a file with "*.doc" in the filename and process every file found
foreach (string fileName in System.IO.Directory.GetFiles
(directoryToSearch, searchPattern))
{
// increment the number of files found
_NumFilesFound++;
/// if you can, send each filename back to the BackgroundWorker's
/// ProgressChanged event method via the UserState. Otherwise,
/// create a list of file names.
if (_bw != null && _bw.WorkerReportsProgress)
{
UserState us = new UserState
(Operations.Searching, directoryToSearch, fileName);
_bw.ReportProgress(bwPercentComplete, us);
}
else
{
// add every found file to the list box
_FoundFileNames.Add(fileName);
}
}
// this directory has been searched, increment counters
//_NumDirectoryLevels++;
// not counting the top level, increment the number of directories processed
if (_CurrentNumDirectoryLevels > 0)
_NumDirectoriesProcessed++;
// if you can, report back to backgroundworker
if (_bw != null && _bw.WorkerReportsProgress && _NumDirectoriesFound != 0)
{
// report progress back to form after every document has been processed
bwPercentComplete = (int)Math.Round(((decimal)_NumDirectoriesProcessed /
(decimal)_NumDirectoriesFound) * 100);
UserState us = new UserState(Operations.Searching, directoryToSearch);
_bw.ReportProgress(bwPercentComplete, us);
}
// get the subdirectories
string[] dirsFound = System.IO.Directory.GetDirectories(directoryToSearch);
// if you are in at least two levels, add to the initial estimate of directories
if (_CurrentNumDirectoryLevels > _NumDirectoryLevelsToEstimate)
{
_NumDirectoriesFound += dirsFound.GetLength(0);
}
foreach (string subDir in dirsFound)
{
_CurrentNumDirectoryLevels++;
Search(subDir, searchPattern);
}
_CurrentNumDirectoryLevels -= 1;
}
catch (System.UnauthorizedAccessException)
{
// can't look in unauthorized directory. just ignore and do nothing
_NumDirectoriesFound -= 1;
_CurrentNumDirectoryLevels -= 1;
}
}
The EstimateNumSubDirectories
method adds the number of sub-directories found under the directory passed but only 3 levels deep. The recursive part works the same as the Search
method in that it works its way down the tree passing each sub-directory back into the method:
private void EstimateNumSubDirectories(string directoryToSearch)
// estimates the number of directories up to 3 levels deep
{
try
{
if (_CurrentNumDirectoryLevels <= _NumDirectoryLevelsToEstimate)
{
string[] dirsFound = System.IO.Directory.GetDirectories(directoryToSearch);
_NumDirectoriesFound += dirsFound.GetLength(0);
foreach (string subDir in dirsFound)
{
_CurrentNumDirectoryLevels++;
EstimateNumSubDirectories(subDir);
}
_CurrentNumDirectoryLevels -= 1;
}
else
{
_CurrentNumDirectoryLevels -= 1;
}
}
catch (System.UnauthorizedAccessException)
{
// can't look in unauthorized directory. just ignore and do nothing
_CurrentNumDirectoryLevels -= 1;
}
}
The UserState
struct contains different status messages, the current operation (estimating or searching), the directory currently being processed, if a file was just found the file path, and 3 constructors:
public struct UserState
{
/// <summary>
/// "Estimating" status message.
/// </summary>
public string estimatingMessage;
/// <summary>
/// "Searching" status message.
/// </summary>
public string searchingMessage;
/// <summary>
/// Name of the directory currently being processed.
/// </summary>
public string directoryInProcess;
/// <summary>
/// The type of the current operation.
/// </summary>
public Operations currentOperation;
/// <summary>
/// The file name of a found file.
/// </summary>
public string foundFileName;
public UserState(Operations _currentOperation, string _directoryInProcess,
string _foundFileName)
// Structure constructor
{
estimatingMessage = "Estimating remaining time...";
searchingMessage = "Searching for documents...";
currentOperation = _currentOperation;
directoryInProcess = _directoryInProcess;
foundFileName = _foundFileName;
}
public UserState(Operations _currentOperation, string _directoryInProcess)
// Structure constructor
{
estimatingMessage = "Estimating remaining time...";
searchingMessage = "Searching for documents...";
currentOperation = _currentOperation;
directoryInProcess = _directoryInProcess;
foundFileName = string.Empty;
}
public UserState(Operations _currentOperation)
// Structure constructor
{
estimatingMessage = "Estimating remaining time...";
searchingMessage = "Searching for documents...";
currentOperation = _currentOperation;
directoryInProcess = string.Empty;
foundFileName = string.Empty;
}
}
To use the status updates back on the form, you can convert the UserState
back to the FileSearchClass.UserState
as shown below:
if (e.UserState != null && e.UserState is FileSearchClass.UserState)
{
FileSearchClass.UserState state = (FileSearchClass.UserState)e.UserState;
switch (state.currentOperation)
{
case FileSearchClass.Operations.Estimating:
this.xStatusLbl.Text = string.Format("Status: {0}", state.estimatingMessage);
break;
case FileSearchClass.Operations.Searching:
this.xStatusLbl.Text = string.Format("Status: {0}", state.searchingMessage);
if (state.foundFileName != string.Empty)
{
this.xFilesListBox.Items.Add(state.foundFileName);
this.xFilesListBox.Refresh();
}
break;
}
}
Points of Interest
Learning how to have a method loop back into itself was fun and could be useful in many scenarios. The main advantage to this solution is that it accepts a BackgroundWorker
and can send back progress updates to the form.
Another interesting point for me was to get a progress bar to work back on the form. But the progress bar would be nearly full all the time because for every directory you find, it gets processed right away. Using the estimation technique, you can get a good idea of how many directories you'll need to process and add to your estimation until you're finished.
History
- 2nd September, 2009: My first revision
- 30th June, 2010: Article updated