Recently I decided to collate minor annoyances or things that I felt annoyed me or disturbed my workflow, or things that could be helpful. The purpose was to come up with fairly simple “little things” that could be fixed with a nice, straightforward tool. Why? So I can write about it here and share the results.
Recently one of those minor annoyances involved Debug Logging. Not so much that logs weren’t being generated or I didn’t know where they were, but rather that, particularly with scheduled tasks, I might not be sure when they are being created. I also thought it might be useful to be able to see when configuration changes were applied.
The result of this was a fairly simple application concept- it sits in the background and will show a Balloon Tip if changes are detected in any of the specified folders. The nice thing about this is that pretty much everything we need is already part of the Framework- NotificationIcon for the Notification Area icons is already a .NET class; we can use FileSystemWatcher to watch the filesystem, etc- it’s merely a case of putting it all together!
The architecture
While in my particular use-case, I really only want to monitor one folder for changes, it seemed prudent to support multiple. Realistically speaking I have to deal with multiple debug folders across several profiles, particularly those that run as admin so I would likely end up needing to do so anyway. Hard-coding the file paths would be an obvious no-no, so we need to devise a reasonable way to deal with the configuration. My first thought was to make use of Elementizer, the XML Serialization library I constructed- if only so I could use it. But adding such a reference seemed unnecessary. I ended up instead sort of emulating that Library’s interface structure, though. I’d argue that this almost makes that library redundant, but much of what makes that library useful is the various helper capabilities it exposes to make adhering to it’s API contract easier- I just didn’t really need them as my needs were simple.
The FileSystemWatcher class can watch one Folder; since we want several, we’ll want to represent multiple monitor-able folders via separate instances of a given class. There are a lot of different approaches that could be taken here- the one I decided on was to have the configuration logic itself separated from the implementation. The configuration data would then be passed to the implementation, which would use the configuration information to construct and initialize the implementation to start “watching” those configured folders as configured. Here is the code for each particular Item:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public class MonitorConfigurationItem { private String _Title; private String _MonitorPath = ""; private bool _Subdirectories = false; private String _Filter = "*"; private bool _Active = true; public String Title { get { return _Title; } set { _Title = value; } } public String MonitorPath { get { return _MonitorPath; } set { _MonitorPath = value; } } public bool Subdirectories { get { return _Subdirectories; } set { _Subdirectories = value; } } public bool Active { get { return _Active; } set { _Active = value; } } public String Filter { get { return _Filter; } set { _Filter = value; } } public MonitorConfigurationItem() { } public MonitorConfigurationItem(XElement Source):this() { foreach(XAttribute attrib in Source.Attributes()) { if (attrib.Name.ToString().Equals("Path", StringComparison.OrdinalIgnoreCase)) _MonitorPath = attrib.Value; else if (attrib.Name.ToString().Equals("Subdirs", StringComparison.OrdinalIgnoreCase)) { bool.TryParse(attrib.Value, out _Subdirectories); } else if (attrib.Name.ToString().Equals("Filter", StringComparison.OrdinalIgnoreCase)) { _Filter = attrib.Value; } } } public XElement Save() { XElement buildelement = new XElement("MonitorItem", new XAttribute("Path", _MonitorPath), new XAttribute("Subdirs", _Subdirectories), new XAttribute("Filter", _Filter), new XAttribute("Active", _Active)); return buildelement; } } |
Basically each configuration item is responsible for saving and loading it’s particular properties from and to a given XElement Node. It’s properties represent the various configurable settings that can be adjusted when constructing a FileSystemWatcher instance. The full configuration object that has a collection of each item is responsible for serializing and reading from an actual XML source file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
public class MonitorConfigurationItem { private String _Title; private String _MonitorPath = ""; private bool _Subdirectories = false; private String _Filter = "*"; private bool _Active = true; public String Title { get { return _Title; } set { _Title = value; } } public String MonitorPath { get { return _MonitorPath; } set { _MonitorPath = value; } } public bool Subdirectories { get { return _Subdirectories; } set { _Subdirectories = value; } } public bool Active { get { return _Active; } set { _Active = value; } } public String Filter { get { return _Filter; } set { _Filter = value; } } public MonitorConfigurationItem() { } public MonitorConfigurationItem(XElement Source):this() { foreach(XAttribute attrib in Source.Attributes()) { if (attrib.Name.ToString().Equals("Path", StringComparison.OrdinalIgnoreCase)) _MonitorPath = attrib.Value; else if (attrib.Name.ToString().Equals("Subdirs", StringComparison.OrdinalIgnoreCase)) { bool.TryParse(attrib.Value, out _Subdirectories); } else if (attrib.Name.ToString().Equals("Filter", StringComparison.OrdinalIgnoreCase)) { _Filter = attrib.Value; } } } public XElement Save() { XElement buildelement = new XElement("MonitorItem", new XAttribute("Path", _MonitorPath), new XAttribute("Subdirs", _Subdirectories), new XAttribute("Filter", _Filter), new XAttribute("Active", _Active)); return buildelement; } } public class MonitorConfiguration { public static String DefaultDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "BASeCamp", "BCFolderMonitor"); public static String DefaultDataFile = Path.Combine(DefaultDataPath, "Monitor.config"); private List<monitorconfigurationitem > ConfigurationElements = new List<MonitorConfigurationItem>(); public static MonitorConfiguration Static = new MonitorConfiguration(DefaultDataFile); private String sSource; public MonitorConfiguration() { } public IList<MonitorConfigurationItem> GetItems() { return ConfigurationElements; } public MonitorConfiguration(String sFilePath):this() { if(File.Exists(sFilePath)) { var acquiredoc = XDocument.Load(sFilePath); LoadItems(acquiredoc); } sSource = sFilePath; } private void LoadItems(XDocument docSource) { ConfigurationElements = new List<MonitorConfigurationItem>(); XElement RootNode = docSource.Root; if (RootNode == null) return; foreach (XElement MonitorItem in RootNode.Descendants("MonitorItem")) { MonitorConfigurationItem mci = new MonitorConfigurationItem(MonitorItem); ConfigurationElements.Add(mci); } } public MonitorConfiguration(XDocument docSource) { LoadItems(docSource); } public void Save() { Save(sSource); } public XDocument SaveNode() { XElement Root = new XElement("Monitor"); foreach (MonitorConfigurationItem mci in ConfigurationElements) { Root.Add(mci.Save()); } return new XDocument(Root); } public void Save(String sFilePath) { XDocument xsaved = SaveNode(); if (!Directory.Exists(Path.GetDirectoryName(sFilePath))) Directory.CreateDirectory(Path.GetDirectoryName(sFilePath)); xsaved.Save(sFilePath); } } |
I feel it important to point out- as I believe I have done before, since I have a sudden feeling of deja-vu… that public interface properties and methods like “GetItems” is typically poor design; by returning the actual List object, the class yields ownership, in some sense, of that compositing instance. An alternative approach could be for MonitorConfiguration itself to extend from List<MonitorConfigurationItem> instead. (I may explore such design-oriented refactorings in future posts).
Otherwise, the purpose is fairly straightforward- it has a list of Configuration items and knows how to save and load them from an XDocument. static fields store the default configuration location, and instance fields store the specific location used by the instance, with a singleton available to access the standard default location (which in this case is a file in the common application data folder).
For the actual implementation, I wrapped each FileSystemWatcher within a different class. The reason I took this approach is because there seems to be some number of articles, bugs, and general issues with people using the FileSystemWatcher. This way, if necessary, I can change the implementation of the wrapper to use say a custom class using the ChangeNotification API functions, or a fixed version. I’ve not had any issues with the FileSystemWatcher in .NET 4.5.3, so it is possible the issues have been addressed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
public class FolderMonitor : IDisposable { public class FileChangeEventArgs : EventArgs { public enum FileChangeType { Created,Deleted,Renamed } private FileSystemEventArgs fse; private FileChangeType _ChangeType; private String _FilePath; public FileChangeType ChangeType { get { return _ChangeType; } } public String FilePath { get { return _FilePath; } set { _FilePath = value; } } public FileSystemEventArgs SourceArgs { get { return fse; } } public FileChangeEventArgs(String pFile,FileSystemEventArgs pfse,FileChangeType pChangeType) { fse = pfse; _FilePath = pFile; _ChangeType = pChangeType; } public FileChangeEventArgs(String pFile,RenamedEventArgs prea) { _FilePath = pFile; _ChangeType = FileChangeType.Renamed; fse = prea; } } private bool _IsStarted = false; private String _FolderPath = null; private String _Filter = null; private bool _SubDirectories = false; public bool Started { get { return _IsStarted; } } public String FolderPath { get { return _FolderPath; } } public String Filter { get { return _Filter; } } public bool Subdirectories { get { return _SubDirectories;} set {_SubDirectories = value;} } public event EventHandler<RenamedEventArgs> Renamed; public event EventHandler<FileSystemEventArgs> Created; public event EventHandler<FileSystemEventArgs> Deleted; public event EventHandler<FileChangeEventArgs> FileEvent; FileSystemWatcher fsw = null; public void RaiseFileEvent(FileChangeEventArgs fse) { var fe = FileEvent; if (fe != null) fe(this, fse); } public void RaiseRenamed(RenamedEventArgs e) { var copied = Renamed; if (copied != null) copied(this, e); RaiseFileEvent(new FileChangeEventArgs(e.FullPath,e)); } public void RaiseCreated(FileSystemEventArgs e) { var copied = Created; if (copied != null) copied(this, e); RaiseFileEvent(new FileChangeEventArgs(e.FullPath,e,FileChangeEventArgs.FileChangeType.Created)); } public void RaiseDeleted(FileSystemEventArgs e) { var copied = Deleted; if (copied != null) copied(this, e); RaiseFileEvent(new FileChangeEventArgs(e.FullPath,e,FileChangeEventArgs.FileChangeType.Deleted)); } public FolderMonitor(String pPath,String pFilter) { _FolderPath = pPath; _Filter = pFilter; } public FolderMonitor(String pPath):this(pPath,"*.*") { } private bool _IsDisposed= false; public void Dispose() { if (_IsDisposed) return; Stop(); _IsDisposed = true; } ~FolderMonitor() { Dispose(); } public void Start() { if(_IsStarted) { fsw.Dispose(); fsw = null; } fsw = new FileSystemWatcher(_FolderPath, _Filter); fsw.Created += fsw_Created; fsw.Deleted += fsw_Deleted; fsw.Renamed += fsw_Renamed; _IsStarted = true; fsw.EnableRaisingEvents = true; fsw.IncludeSubdirectories = _SubDirectories; } public void Stop() { fsw.Created -= fsw_Created; fsw.Deleted -= fsw_Deleted; fsw.Renamed -= fsw_Renamed; fsw.Dispose(); _IsStarted = false; fsw = null; } void fsw_Renamed(object sender, RenamedEventArgs e) { RaiseRenamed(e); } void fsw_Deleted(object sender, FileSystemEventArgs e) { RaiseDeleted(e); } void fsw_Created(object sender, FileSystemEventArgs e) { RaiseCreated(e); } } |
Effectively it wraps the events and properties of the FileSystemWatcher. It manages it such that existing monitor is stopped and torn down and a new instance is created if one of the properties change, which would be easier than trying to maintain a list and replace existing instances in the list when properties change.
The core of the implementation is in a class I’ve titled “MonitorManager”. It’s task is to understand the configuration and be able to map them to the wrapper instances, in addition to capturing the events fired from the wrapper and then fire them back to the client code, passing back the actual firing Wrapper as an argument. This allows code to use the MonitorManager in a fairly straightforward fashion, simplifying the UI code I have in place. Also, because if this it should be fairly straightforward to revise the actual UI code to use different UI frameworks as well.
1 2 3 4 5 6 7 8 9 10 |
private void RestartMonitor() { if(mm!=null) mm.Dispose(); mm = new MonitorManager(config); mm.FileChange += mm_FileChange; int NumFolders = config.GetItems().Count((f) => f.Active); ni.ShowBalloonTip(5000,"BASeCamp Folder Monitor Started","Monitoring " + NumFolders + " Folders.",ToolTipIcon.Info); } |
Restarting the monitor, which effectively disposes of all the file watchers and starts everything up with the given configuration again, is as straightforward as that- Dispose the existing one, then start up the new one and hook the events, then show a balloon tip indicating as much.
I’ve found it fairly useful to monitor a few debug folders. Some added features I think I may consider adding may include capabilities such as logging all such file changes. I suspect I may need to be careful of possible infinite loops whereby some sneaky individual sets the Monitor to monitor it’s own such log folder.
I have placed this new project onto github for posterity. I feel I should break from this to mention how exceptional VS 2013’s built-in git support is. Quite impressive. I only just tried it for the first time today, moving from my typical approach of just using the command-line. I’d even argue that it’s a better user experience due to being far more integrated than VisualSVN, which is also great but effectively just acts as a finite state machine for launcing TortoiseSVN.
Have something to say about this post? Comment!