Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Directory Comparer - A recursive tool to compare 2 folders

0.00/5 (No votes)
9 Jan 2012 1  
Directory Comparer is an extensible tool that could be used to compare 2 folders

Introduction 

Directory comparer, as the name implies, is a tool to compare folders. I have discussed this application in a way which I feel would be pretty helpful. It tries to introduce a few concepts apart from providing some tips. When I started with this project, it had limited features, but as time porgressed, I incorporated a lot more features. I have also added my future thoughts about this project in the points of interest section. Directory comparer is extensible so you always have the option of creating your own comparers or own UI for representing the results of the default comparers provided.

1.jpg

Background

Yes, out there are a few directory comparers. But I just wanted something that is simple and straight-forward. This is what I define as "simple" - Choose folder 1, choose folder 2, click on button, I should be done. So, I just came up with this recurive comparison tool. I also wanted it to be extensible and so the core functionality is implemented using interfaces, so that it is truly extensible.

3.jpg

Core Logic behind Directory Comparer

At the heart of it, Directory Comparer has the RecursiveComparer. It implements the ITwoPassComparer interface, which, as the name suggests handles the comparison in 2 phases. They are (1) Comparison with respect to the left folder and (2) Comparison with respect to the right folder. I would say any complex problem could be simplified in to multiple simple problems. If you think of it, left comparison would do bulk of the comparison efforts. The first pass comparison is in turn split in to multiple simple parts. The only thing right comparison has to do is to deal with files that are available only in the right. I have also made sure that code is not repeated by sharing code between the left and right comparisons, for example how a file is processed. And an important point to note in this case is that everything that the recursive comparer does starts with the file name and it's case sensitive. I will try to simplify the logic I use here (assuming the "Recusive" checkbox is checked):

  • Start with the left folder. Get a list of all files and folders in the root directory
  • Loop through the items one by one. If it's a file, call the method to process it. This method goes like
    • For the current file see if a corresponding file exists in the other side
    • If a file exits, compare the 2 files, create an entry and add it to a list that tracks comparisons (discussion about the compare operation follows)
    • If there is no match, create an entry to indicate this and add it to a list that tracks comparisons
  • If it's a folder, call a method which is a recusrive method to get comparison information about more files / folders within the current folder
    • For files within the folder, it's processed as decribed above
    • For folders, the recursive process described here is followed
    • If any folder is an empty folder, an entry is added even for this case. This is indicated by folder icon to the extreme left (show in the image in the next section, more discussion about this follows)
  • Once the folder comparison with respect to the right folder starts I just have to deal with files/folders that exists only on the right
Comparison of files is performed in 2 stages. First stage is finding a match by file name. Second stage is by computing a hash (md5) of the left and right files. If either the left or right side of the file is not present, hash is not calculated at all, so as to save some time.

Few words about the UI 


The UI layout is pretty simple. At the top I have a menu bar with various menus and the corresponding drop-down items. Next to this I have a list view which is set to fill the entire parent, which is the main form. Apart from this, I have added SaveFileDialog to enable the use to save the comparison results as xml or csv and a ContextMenuStrip control which is used to provide the user with some options when any of items in the list view is selected. I felt that the ListView is the best option to present the user with the comparison information.

I would like to discuss a few points about 2 of the menu items. View and the Filter menu items are those. By default, the UI displays the combined results of the comparison between the left and right folders. It also has options to display only the left side or the right side results. Filter menu operates on the elements currently displayed in the UI. So if Left Results Only is selected, the filter menu would be operating only on those results and not the entire result data set.

7.jpg

Extending Directory Comparer

Let's say you don't want to use my logic I explained above. You could always extend Directory Comparer in a way you want. You could do this in a few simple steps listed below:

  • Implement the IResults interface (let's call this ImprovedComparisonResults)
  • Implement the ITwoPassCompiler interface (let's call this ImprovedComparer)
  • Implement the IDirectoryComparer interface (let's call this ImprovedDirectoryComparer)

Once you are done with creating the above mentioned classes, you can make the UI to use that to render the results in the following way by modifying comparerWorker_DoWork method in the frmMain form.

private void comparerWorker_DoWork(object sender, DoWorkEventArgs e)
{
	ITwoPassComparer comparer = new ImprovedComparer(this);
	IDirectoryComparer improvedComparer = new ImprovedDirectoryComparer(comparer);
	IResults results = improvedComparer.CompareDirectories();

	this.ReportProgress(100);

	Thread.Sleep(1000);

	e.Result = results;
}

Thus, in case you wished to play around with the Directory Comparer by using its UI rendering, you could follow the above procedure.

Even the other way is possible. That is, you could write your own UI using the following method. Instead of instantiating the frmCompareResults form in the comparerWorker_RunWorkerCompleted method, you could instantiate your own form to display the results as intended. An example is given below:

private void comparerWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
	IResults results = (IResults)e.Result;
	
	_frmNewCompareResults = new frmNewCompareResults();
	_frmNewCompareResults.Results = results;
	_frmNewCompareResults.mainReference = this;
	this.Hide();
	_frmNewCompareResults.Show();
}

Using BackgroundWorkers

At the core, directory comparer passes an instance of RecusrsiveComparer to RecursiveDirectoryComparer. Then it invokes CompareDirectories method of the RecursiveDirectoryComparer to do the actual comparison. As you may notice, this is a time consuming operation. You could pretty much do this on the button click event of "Start". But this would block the main form, thereby rendering the form unresponsive (You would often have noticed the "Not Responding" message in the title bar of some applications). So, in order to not block the main form and make it available for other actions, we use BackgroundWorkers. To use this, you just need to add a BackgroundWorker component to the form (Open Toolbox in the sidebar and scroll to the "Components" section. From there, drag and drop a BackgroundWorker on to the form.

In order to use this BackgroundWorker, you will have to implement the following 3 event handlers:

  • DoWork
  • ProgressChanged
  • RunWorkerCompleted

Before discussing this, we need a way to indicate the progress to the user. For this let's add a ProgressBar to the form. To do this, open Toolbox in the sidebar and scroll to the "Common Controls" section. From there, drag and drop a ProgressBar control on to the form. Now lets see how we can go about setting up the 3 events. First step would be to wire up these events to the BackgroundWorker control, as shown below:

this.comparerWorker.DoWork += new DoWorkEventHandler(comparerWorker_DoWork);
this.comparerWorker.ProgressChanged += new ProgressChangedEventHandler(comparerWorker_ProgressChanged);
this.comparerWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(comparerWorker_RunWorkerCompleted);

The bulk of implementing a BackgroundWorker resides in the DoWork event. This is where I instantiate a RecursiveComparer and pass it to the RecusiveDirectoryComparer. In order to actually initiate the comparison, I then call the CompareDirectories method. RecusiveComparer is created with a reference to the main form. This is to facilitate the comparer to update the status. I feel I should find a better way to do this as I don't feel this to be an elegant way. I could probably have a static method in the main form for the comparer to update, but that doesn't seem to work for some reason. Also, the way the completiong percentage is reported is also not up to the mark, as I expected. I am working on these things.

private void comparerWorker_DoWork(object sender, DoWorkEventArgs e)
{
	ITwoPassComparer comparer = new RecursiveComparer(this);
	IDirectoryComparer recursiveComparer = new RecursiveDirectoryComparer(comparer);
	IResults results = recursiveComparer.CompareDirectories();

	this.ReportProgress(100);

	Thread.Sleep(1000);

	e.Result = results;
}

As we saw erlier, the RecursiveComparer takes in a reference of the main form. As we use a ProressBar control to indicate the status, the "ProgressChanged" event would be used to update the current status percentage of the operation carried out. It's pretty straighforward, I use the ReportProgress method of the BackgroundWorker thread to update the status.

Once the task of the BackgroundWorker is completed in the DoWork event, the RunWorkerCompleted event is raised. This is where we have to get the results and pass them on to the next form, which is frmCompareResults. RunWorkerCompletedEventArgs.e passed to the event handler contains a Result property. This could be cast to IResults which is needed by the frmCompareResults form which presents the results.

private void comparerWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
	IResults results = (IResults)e.Result;
	
	_frmCompareResults = new frmCompareResults();
	_frmCompareResults.Results = results;
	_frmCompareResults.mainReference = this;
	this.Hide();
	_frmCompareResults.Show();
}

Using Context Menus with ListView's

I felt it wouldn't be enough to just display the comparison restuls. It would certainly be helpful to provide some options like opening the left / right files or folders. To do this, it would be better to have a context menu instead of having another menu item in the application's main menu. For this, open the Toolbox menu and move to the "Menus & Toolbars" section. From here I dragged and dropped a ContextMenu control on to the form. When a list item is clicked, you could notice that the entire item was selected. The MouseDown event has to be implemented to show the context menu for the selected item. Following code shows how the event is registered.

private void InitializeListOperations()
{
	this.listView1.MouseDown += new MouseEventHandler(listView1_MouseDown);
}

The next step would be implement the registered event handler. In this case we need to make sure that we are displaying the form only when the user clicks on the right mouse button. Because it would be annoying to have the context menus open for any mouse down event. To achieve this, MouseEventArgs.e passed on to the handler is used. This property would indicate which mouse button was down. The code for this is shown below:

void listView1_MouseDown(object sender, MouseEventArgs e)
{
	selectedItem = listView1.GetItemAt(e.X, e.Y);
	if (selectedItem != null && e.Button == System.Windows.Forms.MouseButtons.Right)
	{
		ManipulateContextMenuItems(selectedItem);
		contextMenuStrip1.Show(listView1.PointToScreen(e.Location));
	}
}

Using Resource Files

In order to include and use images within the application, there are few choices. One way is to have a folder with all the images that will be used by various forms, which will be distributed along with the application and then it could be used in the forms as:

Image image = Image.FromFile("Images/folder.png");

But this isn't an ideal way to do this. Resource files would be a better option as we don't have to distribute the application with image files floating around. By using this option the images are embedded in the application itself, thereby reducing the need to distribute images seperately apart from the executable. In this case 2 type of resources are needed.

  • Project wide resources
  • Form specific resources

Project wide resources are the images that are displayed on the title bar of various forms. At this point I just use an icon for the title bar and it's added to the project by choosing the following steps:

  • Rgiht click on the project and choose "Properties"
  • Choose the "Build" tab
  • Under "Resources" section, choose "Icon and manifest"
  • Choose the "..." button and navigate to the folder with the icon and add it

Doing the above step would also add it to your porject in the root directory. Note that this would just make the application icon to the chosen file (executable's icon). In order to have the form's title bar display the same icon, right click on the form, find the "Icon" property, and choose "..." and then finally, choose the same icon you chose for the application. Repeat the steps for all the forms.

Now, the next step would be to add the icons that's going to be used in various forms. I have used 2 approaches in this case. There are 3 forms in the project frmMain, frmCompareResults, frmPrefernces. frmMain does not use any resources (apart from the project wide icon). frmCompareResults uses 1 icon at this point, which is a folder icon, that is displayed in the UI to indicate that an entry is a folder. In future there could be many more icons in use which may in turn be shared between various forms. So I created a common DirectoryComparerIcons.resx file which will hold these icons. Once the icons are added to this resx file, it could be used this way:

public static ImageList GetImages()
{
	ResourceManager manager = new ResourceManager(typeof(DirectoryComparerIcons));
	ImageList imageList = new ImageList();

	Image folder = (Image)manager.GetObject("folder");            
	imageList.Images.Add(folder);

	return imageList;
}

Here an instance of ResourceManager is created by passing in the common type we created - DirectoryComparerIcons. Now, the manager object can be used to get a reference to the folder icon we added to the resource file. In our case the name of the image file is "folder.png". So passing just the name "folder" excluding the extension would get us the image. This is far better than having the image files loaded using Image.FromFile.

The next approach is to add the image to the respective form itself. frmAbout form uses this approach. To use this method, the frmAbout.resx file should be opened. Then click on "Add Resource" split button - which gives you a drop-down. From among the options choose "Add Existing File" and then navigate to the appropriate folder and finally add the image you wish to use.

Managing the Settings

The settings module currently include saving and restoring the following keys needed for the project.

  • Left folder path
  • Right folder path
  • Column display settings
5.jpg

Settings are stored in the registry. These settings would help save time taken to choose the left and right folders every time. It also has a setting to save/restore how the application chooses to show/hide some optional columns. Given below is how the screen looks after the preferences were saved and the app restarted. At this point, the application requires a restart, but in future I would be updating the application to immediately repaint the screen once the Save Preferences button is clicked. Give below is an image after the restart:

RegManager class deals with managing these settings. A corresponding RegistryKeyMap class deals with supplying the keys in the registry that stores these information. I intend to provide an interface that would eliminate the need of the application being tied to the registry as sepcified in the points of interest. Given below is a screen shot of the registry section after settings have been saved once:

8.jpg

Some Tips

This is just a small list of few minor things I thought would be helpful to others. I am just listing them out here:

  • Say you need to make a control to fit the entire parent. For this you just need to set the Control.Dock property to true
  • To display a check mark before a menu item set the ToolStripMenuItem.Checked property to true
  • In order items to a ListView follow the sequence. This is just to make sure the items added to the list are not displayed until all the items are added. All of the method calls discussed below are instance methods.
    • Call ListView.Items.Clear method
    • Call ListView.BeginUpdate method
    • Add the items to the ListView
    • Call the ListView.EndUpdate method
  • The second level menu items of any first level menu item (ex. File, View, Filter etc) can be retrieved using the ToolStripMenuItem.DropDownItems. The first level menu items View and Filter, if you notice, has seperators. In order to loop through the drop down items in this case, you will have to do the following:
// Iterate over the items as ToolStripItem. If ToolStripMenuItem is used, it will break
// as there are seperators in the drop-down items
foreach (ToolStripItem mnuItem in menuItem.DropDownItems)
{
	// My intention was to reset all items in the drop-down items
	// So I have to see if its a ToolStripMenuItem and then 
	// set the Checked property to false
	if (mnuItem is ToolStripMenuItem)
		((ToolStripMenuItem)mnuItem).Checked = false;
}

Points of Interest 

I wish to spend some of my time developing this application and so, while I was finishing up coding for this application, a lot of new features came up to my mind. I am just laying out them below:

  • An extension to the application that would give you a count og lines of code
  • A command line version to take 2 folders as params, compare and give the output as xml (or csv)
  • Instead of just providing an option to open with notepad, provide a way to open with appropriate application
  • A way to provide extensions to how preferences related to various tasks in Directory Comparer are stored
  • Provide interfaces to do the actual comparison of files (as of now md5 hash is calculated for the files)
  • Evaluate the possibility of using MEF (see Extending Directory Comparer)

History

Version 1.0 released

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here