Introduction
This project was built to do bulk/batch file renaming out of my frustration with trying to manually rename dozens of files at a time. It demonstrates several handy methods such as how to enumerate your drives and files and then how to batch rename files. This project also demonstrates how to create, generate, and handle events in your program including built-in utility events for disk drives and files.
In order to keep the display of drives, folders, and files up to date I hooked to system events and created some events of my own to signal the program to rescan the filenames, folders, and drives. With these active, the program can keep the display of drives and files up to date in real time. I'll explain the creation and usage of these event classes and how they "know" that a change has occurred.
Background
I use a digital camera these days like most folks. When the camera names files on the memory chip it uses and then reuses the same lame generic names for files - P1010078.JPG, P1010078.JPG, P1010078.JPG, etc. When you go to download the next set of pictures from another vacation or occasion, you get a name collision plus the fact you have no idea what the heck the pictures were about with names like that.
I tried manually renaming pictures and files with this issue, but when it comes to fifty or so files it is a major pain in the neck. Annoyances like this are often the spur to creating a solution. This is my approach to overcoming the problem with an easy to use interface in a C# Windows Forms based application.
The Project uses the following libraries:
- System
- System.Collections.Generic
- System.IO
- System.Management
- System.Windows.Forms
Table of Contents
The Application Code
The project contains the following classes/forms:
frm_Main
- contains start up and user interface code Actions
- the methods for changing the filenames GettingDrivesNotice
- a popup used whenever we're scanning for drives DiskChangeAlerter
- the event delegate and the handler code for sensing USB drive connect/disconnect FileChangeWatcher
- the event delegate and the handler code for file changes
The first thing unusual in the code is the program start up. After the frm_Main()
constructor runs, I put a short timeout in the frm_Main_Shown()
event handler. This is to prevent a racing condition where the directory and drive list display controls are not quite finished being rendered. There seem to be two schools of thought on this. One group likes to launch a new thread and use Thread.Sleep() to create a delay. In this example I chose instead to use a timer object and use the timer_Tick()
event handler to execute what I needed to do next.
private void frm_Main_Shown(object sender, EventArgs e)
{
p.frmParent = this;
x.frmParent = this;
chkUSBdrives.Checked = Properties.Settings.Default.DisplayUSBdrives;
frmNotice.Show(this);
tTime = new Timer();
tTime.Interval = 100;
tTime.Tick += new EventHandler(tTime_Tick);
tTime.Start();
}
void tTime_Tick(object sender, EventArgs e)
{
try
{
tTime.Stop();
tTime.Tick -= new EventHandler(tTime_Tick);
tTime.Dispose();
tTime = null;
DAlerter = new DiskChangeAlerter();
GetDrives();
frmNotice.Hide();
if (chkUSBdrives.Checked)
DAlerter.DiskChangeEvent += new EventChangedAlertHandler(DAlerter_DiskChangeEvent);
FileWatcher = new FileChangeWatcher();
FileWatcher.FolderPath = sCurrentDirectoryPath;
FileWatcher.eFileChanged += new FileChangedEventHandler(FileWatcher_eFileChanged);
FileWatcher.Start();
}
catch (Exception te)
{
Debug.WriteLine("tTime_Tick(): " + te);
}
}
The GetDrives()
method uses the System.IO.Directory
object to enumerate the drives and list them in the combobox as well as adding them to the dictionary: LastDirListByDrive
. The System.IO.DriveInfo
object is used to collect useful information about the drives like drive type, name, and volume label that is displayed in the combo box drive list.
GetDrives()
next calls the GetFolders()
method to enumerate all the folders into the treeview object. Next it calls DrillDownToCurrentDir()
method to expand the tree down to our current directory.
Finally, it calls GetFiles()
which then lists the contents of the current directory into the System.Windows.Forms.DataGridView
object.
From this point on user actions trigger methods in the main form. If you select a different drive, then the GetFolders(
) and GetFiles()
sequence will be called to update the display. A newly clicked folder will call the GetFiles()
method.
The user then selects the type of name change(s) they want and can view the expected results without actually modifying the filenames by clicking "Preview" button. Here's an example:
Clicking the "Execute" button does exactly that. You can manually refresh the file list if you think it is out of date for some reason and there are buttons for select all and select none. I did add one bit of code to the DataGridView_CellContentClick()
event handler to make selecting multiple files easier. When you 'click' a checkbox using the mouse or the spacebar, the handler moves the focus to the cell below. This way you can just hold down the spacebar for as many files as you like to select them.
private void DataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
if (e.ColumnIndex != 0) return;
int iIndex = e.RowIndex + 1;
if (iIndex == DataGridView1.Rows.Count)
iIndex = 0;
DataGridView1[0, iIndex].Selected = true;
}
Perhaps you will have noticed that we consumed/handled several events in the example code just shown. Now we'll get down to the information of using and creating events in your program and two practical examples of how they might be used.
Links
Events
Now let's talk about events. Altogether an event and its consumer require the following parts:
- an arguments class
- a delegate
- an event type variable
- an event handler method
Somewhere in your code or the code of the class raising the event you will find all of these. If any are missing you will not raise and catch an event and process it.
What An Event Actually Is
On the face of it events look like someone raises a flag, passes along some information and then a remote object catches the flag and executes its handler for it. Well that's not it at all. In .NET world an event is like an abstract method contained within the scope of its declaration (i.e. local or global). It does nothing itself. To raise an event, an object calls this method like calling any other method.
Listener objects subscribe to this abstract method by listing their handler method's address with the event method so that an actual handler method now exists to act when the event method is called. Think of it like overriding an abstract method by providing an actual implementation except these methods can be global in scope and not limited to being contained within a class or its children.
With that in mind, if you don't have a listener overriding the abstract event method and an object tries to raise the event, like an actual abstract method you'll get a null error and an exception. I'll show you a very simple way to handle that.
Argument Class
Here's an example of an argument class declaration:
public class DriveChangedArgs
{
public string DriveLetter = "";
public string InterfaceType = "";
public DriveChangeType ChangeType = DriveChangeType.Other;
}
You do not HAVE to declare a unique arguments class, but it is actually necessary to make it easy to discriminate between one event and another. Because "events" are actually methods you need a unique signature for yours. You could name every event "EventHandler" so long as your parameter(s) were different. In C# and C++ the entire header including parameter list is a part of the signature. If you have created a unique arguments class to pass as the parameter of your event, the event method will be unique.
It takes only the code required to declare your argument variables so it's not much overhead in programming time. Of course, if you are not passing any information with the event you can always use the System.EventArgs.Empty
built-in class and value in your other declarations.
Argument classes must be able to be seen by all the objects that will either raise your event or handle it. Generally that means a global declaration - inside the namespace, but outside any classes. If your event and handler are all inside one class object like a Windows form object, then you can do the declarations inside that class object.
Enums
You will note the DriveChangeType
in this declaration. It is always good to use an enum to restrict return values to those you expect. Otherwise users will concoct all sorts of values and you will have handling errors. Here's the enum created in this instance:
public enum DriveChangeType { Create, Remove, MediaChange, Other };
The Delegate
The delegate declaration is the prototype of the event handler method declaration and is used in the declaration of the event that will be raised. It is similar to an abstract method declaration in that it does nothing itself but patterns the method and gives it a signature. Here's an example:
public delegate void EventChangedAlertHandler(DriveChangedArgs e);
The delegate must be declared so that it can be seen by all the class objects that need to raise and handle the event. Generally you will declare them as global objects, in the namespace but outside any classes. Once again, if the entire event and handler code all occur in one single class you can put the declarations inside the class.
Raising an event
Now comes the good stuff! First we have to raise an event so something can catch it. The event
is declared inside the class that will raise it. If your listeners are in code external to the class declaring the 'event' then you need to make it 'public
' so it shows. If you wish to handle the event by an object that does not contain an instantiated copy of the class with the event in it, then the event will need to be 'static'. Here's a declaration of the event in this code:
public event EventChangedAlertHandler DiskChangeEvent;
Notice that it uses the name of the previously declared delegate. This is important. You must have a delegate declared for your event or you can't raise it. [Makes sense, yes?] So we have declared an object type of "event" and passed in the delegate name as a type and then given this event variable a name - DiskChangeEvent
.
Now how do we go about actually raising an event? Here's an example:
if (DiskChangeEvent != null)
DiskChangeEvent(a1);
The argument passed in the event, a1
, is just an instance of the DriveChangedArgs
class.
DriveChangedArgs a1 = new DriveChangedArgs();
This is where it gets a little weird. You declared an event type and gave it the name DiskChangeEvent
. So how could it be null? Well, just like an abstract method, the delegate has no implementation. With events, the way you do it is by having a listener attach it's implementation to the event, much like overriding a method. That's what the "is it null" check in the example is looking at. Is there a listener? If not and you go ahead and raise the event you'll get an exception error. Your event raising code should always have this "is null" trap in it. Remember, an event starts out in life like an abstract method. Until you attach an actual method to it it will be null.
Event handler
Well, now that you've raised an event we need to trap it and handle it. There may be more than one object listening for the event and then doing something. In addition, events are often not happening on the same thread as the object catching the event. We'll look at these things right now.
First we need to create a handler method before we can attach it to the event. It has to follow the same signature as the delegate, however it will have a unique name - not the name of the delegate, just the signature. Here's our example:
void DAlerter_DiskChangeEvent(DriveChangedArgs e)
{
.
.
.
}
We'll get to what it does in a minute. But first, we have to let the events know there's a listener out there by attaching our listener to the event. Events are raised by class objects. Somewhere in our code we had to instantiate at least one instance of the event raising class object so that it could then raise an event as needed. In our example program we did this in the main form program when it started up. Here's the code:
DAlerter = new DiskChangeAlerter();
The DiskChangeAlerter
class contains all the event raising code along with the event
object. Now that we have a class object instantiated we're going to attach our handler to its event. Here's the code to do that:
DAlerter.DiskChangeEvent += new EventChangedAlertHandler(DAlerter_DiskChangeEvent);
Remember, our event raising class declared its event as "public". That way any object holding an instance of the event raising class can "see" the event. Then we use the "+=" operator to add our handler to the raiser's list of listeners. However, if you want all handlers to "see" the same event then you should consider making the event variable 'static'.
Caution!
If you instantiate separate instances of the event raising class they will raise different instances of the event, so listeners will hear separate events not 'the event'. If you want all listeners to see the same event use a static event in the raiser class and have all listeners attach to it.
Cross Thread Event Handlers
Due to the way classes are instantiated in .Net, very often different class objects run on different threads. If this is the case then you cannot modify the value of an object on another thread. Your handler method is actually called and runs from the event raising class. You can easily see this if you attach your event handler and when the event is raised you try to perhaps modify the value of a display object like a textbox. If you get a threading safety error you are dealing with the pernicious cross thread safety issue.
All is not lost. There is a very simple code trick that will take care of this silliness. Here's an example in an event handler:
void DAlerter_DiskChangeEvent(DriveChangedArgs e)
{
try
{
if (this.InvokeRequired)
{
MethodInvoker del = delegate { DAlerter_DiskChangeEvent(e); };
this.Invoke(del);
return;
}
else
{
.
-some code here-
.
}
}
catch (Exception de)
{
frmNotice.Hide();
Application.DoEvents();
MessageBox.Show("rescan error: " + de);
}
if (frmNotice != null)
{
frmNotice.Hide();
Application.DoEvents();
}
}
As you can see, we actually launch a local call to the handler method using the 'this.invoke(del)
' command and pass it the arguments. Now it's running in the local thread so we can molest anything we like without the dreaded cross thread exception. It's a bit clumsy, but it works.
Review
Well now you're an expert on events. Just remember that in .Net World an event is really like an abstract method. It declares a prototype but has no implementation. Until you attach an implementation to the event it will return an error so you need to trap for that in your event raising code.
Events consist of four things: arguments, delegates, events, handler methods. Somewhere within the application you will find all four of those items.
Links
Disk change event class
The purpose of this class and its event is to notify the application that a disk drive has been mounted or dismounted. Classically this is for USB memory sticks, but any drive coming or going will trip this.
Sensing a drive mount/dismount
This is a bit complex but my code will show you the way. I'll explain without trying to get too deep into the internals. Like most programmers, I just wanted to sense the event and then use it. Unfortunately, nothing in Windows is that straight forward. We are going to monitor system management events and then pick off the disk drive events for inspection. So we'll need to instantiate the ManagementEventWatcher
class and then attach our event handler to the event we want to listen for.
ManagementEventWatcher Watcher = new ManagementEventWatcher();
Next we'll hook things up in the DiskChangeAlerter
class constructor:
public DiskChangeAlerter()
{
WqlEventQuery q1 = new WqlEventQuery("SELECT * FROM __InstanceOperationEvent WITHIN 1 "
+ "WHERE TargetInstance ISA 'Win32_DiskDrive' Or TargetInstance isa 'Win32_MappedLogicalDisk'");
Watcher.Query = q1;
Watcher.EventArrived += new EventArrivedEventHandler(Watcher_EventArrived);
Watcher.Start();
}
The query tells the watcher what we are interested in and then we tell the watcher where our event handler is (by name). Our handler, of course, has to match the signature of the delegate for this event.
I won't publish all the code involved here but if you download the source code you can follow the trail as the event is deconstructed to see what happened and what drive letter is involved. The exact code to do all that is a bit arcane, but there are other examples and mine is pretty straightforward. Once we have received an event that there's a drive change and we've drilled in and gotten the information we want, we stuff that into our args creation and raise our event - DiskChangeEvent(a1)
.
Our application has the following code located in the namespace above the code for the DiskChangeAlerter
class (thus global in scope):
public enum DriveChangeType { Create, Remove, MediaChange, Other };
public class DriveChangedArgs
{
public string DriveLetter = "";
public string InterfaceType = "";
public DriveChangeType ChangeType = DriveChangeType.Other;
}
public delegate void EventChangedAlertHandler(DriveChangedArgs e);
Within the class we have these classwide variables:
ManagementEventWatcher Watcher = new ManagementEventWatcher();
public event EventChangedAlertHandler DiskChangeEvent;
Once we've deconstructed the information in the management event with our handler we initialize the values of the DriveChangedArgs
variable, a1, and then call the event method:
if (DiskChangeEvent != null)
DiskChangeEvent(a1);
Back up in our application class we attached a handler to the EventChangedAlertHandler
event, DiskChangeEvent
, after we instantiated a copy of this DiskChangeAlerter
class:
if (chkUSBdrives.Checked)
DAlerter.DiskChangeEvent += new EventChangedAlertHandler(DAlerter_DiskChangeEvent);
When the ManagementWatcher
fires its change event and then the DiskChangeAlerter
class fires its change event we need to clear the user presentation objects and then call the methods to relist the drives, folders, and files. You'll notice we put in the code to handle cross thread issues.
void DAlerter_DiskChangeEvent(DriveChangedArgs e)
{
try
{
if (this.InvokeRequired)
{
MethodInvoker del = delegate { DAlerter_DiskChangeEvent(e); };
this.Invoke(del);
return;
}
else
{
frmNotice.Show();
Application.DoEvents();
bDriveRescan = true;
cbDriveList.Text = "";
cbDriveList.Items.Clear();
tvFolderTree.Nodes.Clear();
DataGridView1.Rows.Clear();
GetDrives();
bDriveRescan = false;
}
}
catch (Exception de)
{
frmNotice.Hide();
Application.DoEvents();
MessageBox.Show("rescan error: " + de);
}
if (frmNotice != null)
{
frmNotice.Hide();
Application.DoEvents();
}
}
The Key to the watcher is the WqlEventQuery
. The syntax is a bit arcane. You can scan the web and the Visual Studio help files for info on the ManagementEventWatcher
class, but the real nitty gritty is in the query you create. There are quite a few good examples on the web if you scan for the WqlEventQuery
.
That's all there is to it. You create a ManagementEventWatcher
class instance and attach your handler to it. You tell the watcher you are interested in disk drive events (or whatever else you like). When it raises the event you drill for whatever info you need based on the event args passed to you. In my case I then created another class to wrap this thus hiding the arcane code to get to the information I really needed. Then I raise my own event and pass along this information to the application where its handler erases the old display data and fetches the newest and displays it.
Links
File Change Event
Let's look at sensing folder content or file info changes so we can update our user display. There's a very useful built-in class for sensing changes in files - System.IO.FileSystemWatcher
. Amongst other things it will tell you if a file was: created, deleted, or renamed. In our case we need to know any of those so as to keep the display up to date.
We wrapped that event thrower in our own class and throw a custom event so as to hide the catching and filtering of information and send along only what we need to know for our application to operate. The FileChangeWatcher
class in my application contains all that code. Here's the global declarations for our custom event:
public class FileChangeEventArgs
{
public string FolderPath = "";
public string ChangeType = "";
}
public delegate void FileChangedEventHandler(object sender, FileChangeEventArgs e);
Inside our custom thrower class we declare an instance of the FileSystemWatcher
class and an event instance of our FileChangedEventHandler
delegate:
FileSystemWatcher Watcher = new FileSystemWatcher();
public event FileChangedEventHandler eFileChanged;
In our class constructor we initialize some of the values and attach our listeners to the FileSystemWatcher
:
Watcher.Filter = "*.*";
Watcher.IncludeSubdirectories = true;
Watcher.NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.FileName;
Watcher.Created += new FileSystemEventHandler(Watcher_Created);
Watcher.Deleted += new FileSystemEventHandler(Watcher_Deleted);
Watcher.Renamed += new RenamedEventHandler(Watcher_Renamed);
From this point on if the file watcher raises an event, we sort out what happened, put the useful information into our arguments class and then execute our custom event. I won't bother listing all that code here. You can easily follow what happens looking into the source code file.
Back up in the application class we handle the custom file change event. All it really does is erase the current files display and then go fetch a fresh copy of the file information and display it. Here's the code. Notice that we use our cross thread event handler code to prevent any cross thread security issues.
void FileWatcher_eFileChanged(object sender, FileChangeEventArgs e)
{
if (this.InvokeRequired)
{
MethodInvoker del = delegate { FileWatcher_eFileChanged(sender, e); };
try
{
this.Invoke(del);
}
catch { };
return;
}
else
GetFiles();
}
Review
There's a very handy built in file change watcher class - System.IO.FileSystemWatcher
. With it you can get real time notices of any changes of files in any given directory/folder. I use it to feed to a custom event in order to hide the busywork code and simplify what the main application class has to deal with. The custom event filters out what happened and sends a simplified set of arguments built for the application. The application in turn uses this to trigger a refresh of the files display.
Links