Friday, January 18, 2013

FileSystemWatcher tips

Recently in one WPF project I needed to monitor multiple folders for the possible file and folder changes.  I remembered in VC++ we had to use Win32 API to create our own thread data and run it in a different thread.  In .NET world it seems FileSystemWatcher is the only and reasonable choice, and you don't have to run multiple threads by yourself.  .NET framework will manage the monitoring thread for you.  However, when I started using it, I found some issues which could eventually affect how and whether you can use it.  I listed some concerns and tips here.  We may reference this tips, which is some basic stuff you may be interested in.  I may have another file to list the related code.

1. Some events will be fired multiple times.

When you rename a file, you could get several events fired.  This is a known issue for file watchers.  If you process the changes in the event handler, you could handle multiple times for one change.  A good choice is to group the changes together, and then only process them once.  So a Timer may be good in this situation.  I will discuss Timer in another paragraph.

2. The monitored folder name change event is not fired.

When I debugged and found there was no events fired when the monitored folder was renamed, I was really frustrated.  Actually, FileSystemWatcher does catch the event and furthermore changes the monitored folder to the new folder and starts monitoring the new folder.  But you just cannot catch it.  So this is the design behaviour, but apparent not what I wanted, because I needed to display the new folder name immediately.  So to monitor the renaming event is a must.

The original thought came from this thread.  The idea is to create another watcher to monitor the folder's parent folder, and only watch the directory name renaming event.  Meanwhile you can specify a filter to only watch the sub folder you are interested in. The following is code to create the parent watcher.

            _parentWatcher = new FileSystemWatcher();
            _parentWatcher.Path = (Directory.GetParent(_watchedFolder)).FullName;
            string filter = _watchedFolder.Substring(_watchedFolder.LastIndexOf('\\') + 1);
            _parentWatcher.Filter = filter;
            _parentWatcher.IncludeSubdirectories = false;
            _parentWatcher.Error += Watcher_Error;
            _parentWatcher.NotifyFilter = NotifyFilters.DirectoryName;
            _parentWatcher.EnableRaisingEvents = true;

3. You cannot rename or delete the monitored folder's parent folders.

Let's say you are watching folder A.  You are OK to change any files under A, rename folder A, and maybe delete A.  But you can not rename A's parent B, B's parent C, and so on.  Check this thread.  When you try to rename it from Windows Explorer, you will get the following infamous message:
The action can't be completed because the folder or a file in it is open in another program

This is ridiculous since no file or folder is opened, just the folder is monitored.  Again this is a design behaviour.

A workaround here could be you go ahead to watch your entire drive, let's say C:\ or D:\.  As long as this guy doesn't have a parent, you don't worry about renaming a parent folder.  But this probably brings performance issue, because you watch many unnecessary changes, especially for network drives.

4. Use a Timer to group multiple events

To be honest, I am not a fan of Timers.  I always feel Timers are low classes in the system and not reliable.  Maybe I am wrong, but I just have that feeling.  But in some cases where missing an event is not that critical, Timers still can do the work.  We should notice there are at least 3 timers:  System.Timers.Timer, System.Threading.Timer, and System.Windows.Threading.DispatcherTimer.  This thread discussing Timers may be useful.  I used System.Timers.Timer in this case.  Somebody mentioned we should use DispatcherTimer, but it turns out DispatcherTimer behaves differently with Timer.

When you respond to every event, you stop and restart the timer to wait for the same period, so this probably can group all changes together and fire the final change request.

Another thing needs to notice is in the Timer elapsed event, we should call

            _uiDispatcher.BeginInvoke(new Action(() => {
                Messenger.Default.Send("StartRefreshing", "StartRefreshing");
            }));

not just simply send the message.
              Messenger.Default.Send("StartRefreshing", "StartRefreshing");
  
The difference is the later will send the message to a background thread but the former will send the event to the main ui thread.  When you want to change UI stuff in the responding function, in the later case you will get
              The calling thread cannot access this object because a different thread owns it
  
Because in WPF only the main GUI thread can change the GUI elements.  We can use Dispatcher.Invoke or BeginInvoke to execute a delegate in the dispatcher thread.   In the middle of this work, I wanted to use DispatcherTimer, but it turned out the timer is not fired.  Check this thread and this thread to see the possible reason why this timer is not fired.  The dispatcher timer is created in one thread and will only fire events in that thread and only the dispatcher of that thread can access these events.

5. Do we need explicit multiple threads?

Here comes another concern users normally have, do we need explicitly to put the file monitor to another thread?  Check this discussion.  The answer is no, because .NET framework will handle it.  The class will create a thread if it needs that.  So unless necessary, you don't have to create a thread to put the file monitor in it.  This is different with the old-time Win32 way and of course a nice improvement.


No comments: