Click here to Skip to main content
Click here to Skip to main content

File Flitter

, 15 Jan 2011
Rate this:
Please Sign up or sign in to vote.
Monitor files and when they change, copy them to specified folders.

Screenshot.png

Introduction

File Flit is a simple utility that watches for changes to existing files and, when a change is detected, copies the file to an existing target folder. I wrote this utility because I didn't really find anything out there in the Internet ether that did just this and was simple to set up. So, when I started writing the code, I had the idea that, let's write the article from the perspective of how this utility was developed in about four hours. In fact, the article has taken at least as long, if not longer, than the code! You won't see elegant code here or snazzy technologies like WPF, WCF, Silverlight, MVVM, and so forth. Those just aren't needed. What you will see is a brain-dead simple piece of code that does, well, what it's intended to do, and some stories about each piece of code.

Requirements Statement

Whether I'm looking for an application that already exists, or some code, or considering the idea that I might roll my own, I like to start with a requirements statement, something that harks back to the days of Grady Booch and other early object-oriented design methodologies. When teaching programming, I also start with the requirements statement--put in words what the program should do. It's amazing how difficult people find this task! So here's my requirements statement:

The program should monitor when one or more files is updated (via its change date) and then copy the file to one or more target locations. The source and target are to be persisted in a lightweight XML format in a configuration file, and a simple UI should allow for the editing of the file list. Each file being monitored can specify a time delay between when the file change is detected and when the copy operation is performed, so that the program can compensate for the file update time. The UI should also show a log of activity and errors that occur when trying to copy the file. The source is always a file (wildcards are not allowed at the moment) and the target is always the destination folder into which the file should exist. The program can assume that the files and folders always exist.

From this requirements statement, I can extract, just by looking at the statement, the following implementation requirements:

  1. A configuration class that serializes / deserializes the XML
  2. A class to maintain the property values of each "flitter" record (source, target, delay)
  3. A list to maintain the collection of records
  4. A UI for displaying the configuration
  5. A "service" that monitors the source file changes and copies them when they change
  6. A way of logging success and failure
  7. A UI for displaying the log file

I like this approach very much and use it as a cornerstone for everything that I do. The beauty of it is that I can take even highly abstract requirements and recursively refine them into more and more concrete statements, such that at the end, I have a very concise definition of each requirement in terms of things (nouns) and activities (verbs), the interaction between the user and the things, and the interaction between the things themselves.

The above requirements were easily implemented in the desired timeframe, and next I'll discuss each one.

Configuration Classes

The configuration class began simple enough, but quickly grew into four different classes:

  • The Config class itself, whose methods are accessed through a singleton property
  • The serializable configuration, FileFlitConfig, which just maintains a list of file records
  • The FlitterRecord class, which maintains the property values of a single file record
  • A class implementing IEnumerable, ConfigEnumerator, to iterate over the record list

The only part that I didn't consider in the implementation requirements was the enumeration of the record list, and frankly was a kludgy "for convenience" implementation, one that I probably would never have created if the configuration class maintained anything more than a collection of file records. These classes take care of the implementation requirements #1-3.

FlitterRecord Class

This class maintains the property values of a single "Flitter" record. Because I like readable XML, I decorated the properties with XmlAttribute tags, and you'll notice later on that the serialization uses the formatting option Formatting.Indented. I originally created two constructors, one which doesn't require specifying the delay time to initiate the copy. Later on in some method, I use a default parameter value instead, so I figured, well, I should be consistent, and use a default parameter value for the constructor here. So here we are, faced with the amusing question, are multiple constructors better or are default parameter values better? I got rid of the first constructor, and amusingly, Visual Studio 2010 highlights the code with an error:

but it compiles just fine. So now, I'm stuck with a little red flag on my code, even though there's nothing wrong with it!

And of course, the default constructor is required for deserialization, so the class can be instantiated.

[Serializable]
public class FlitterRecord
{
  [XmlAttribute("SourceFile")]
  public string SourceFile { get; set; }

  [XmlAttribute("DestinationFolder")]
  public string DestinationFolder { get; set; }

  [XmlAttribute("Delay")]
  public int Delay { get; set; }

  public FlitterRecord()
  {
  }

  public FlitterRecord(string sourceFile, 
         string destinationFolder, int msDelay=1000)
  {
    SourceFile = sourceFile;
    DestinationFolder = destinationFolder;
    Delay = msDelay;
  }
}

FileFlitConfig Class

This class maintains the collection of FileFlitter records. I had initially put the List into the Config class (see below) but then started encountering serialization errors when I added the IEnumerable interface. From MSDN:

The XmlSerializer gives special treatment to classes that implement IEnumerable or ICollection. A class that implements IEnumerable must implement a public Add method that takes a single parameter. The Add method's parameter must be of the same type as is returned from the Current property on the value returned from GetEnumerator, or one of that type's bases.

IEnumerable makes serialization more complicated! So, not wanting to learn something new, I decided to separate out the class that is being serialized, that maintains the collection of FlitterRecord instances. I figure, this is probably a better implementation anyways.

[Serializable]
public class FileFlitConfig
{
  public List<FlitterRecord> FlitterRecords { get; set; }

  public FileFlitConfig()
  {
    FlitterRecords = new List<FlitterRecord>();
  }
}

Config Class

This isn't a very elegant class, it combines enumeration and encapsulation of the record collection class, as well as methods to support clearing the list, adding records to the list, and serializing / deserializing the list. There is a static static property "Records" that, if configuration is not initialized, will do so in the property getter. I guess this is a quasi Factory and Singleton pattern. It's a Factory pattern because it handles the instantiation of the record collection, but it's a Singleton pattern because it returns an instance of itself and the constructor is protected. Well, whatever. Patterns are for nerdy discussions by geeks who drape their arms over the walls of their fellow cubicle inmates in an attempt at self-deification. And I wonder why I don't interview well!

Oh, and errors that are encountered in deserialization are reported to the user via a MessageBox displayed in the deserializer method - talk about mixing of concerns! In the event of a deserialization error, the getter returns a Config instance and the FlitterRecord collection is empty. We don't really need to throw an exception, do we? We don't really need to inform the caller that there was an error, right? And whatever option could we possibly offer the user other than to grudgingly click on the "OK" button: "Yeah, there's an error, and you can't do anything about it!!!" Simplicity!

[Serializable]
public class Config : IEnumerable<FlitterRecord>
{
  protected FileFlitConfig fileFlitConfig = new FileFlitConfig();
  protected static Config config;

  /// <summary>
  /// Factory getter.
  /// </summary>
  public static Config Records
  {
  get
    {
      if (config == null)
      {
        Deserialize();
      }

      return config;
    }

    protected set
    {
      config = value;
    }
  }

  /// <summary>
  /// Indexer.
  /// </summary>
  public FlitterRecord this[int i] { get { return fileFlitConfig.FlitterRecords[i]; } }
  
  public int Count { get { return fileFlitConfig.FlitterRecords.Count; } }

  public void Clear()
  {
    fileFlitConfig.FlitterRecords.Clear();
  }

  /// <summary>
  /// Adds a record to the record list.
  /// </summary>
  public void AddRecord(string sourceFile, string targetPath)
  {
    fileFlitConfig.FlitterRecords.Add(new FlitterRecord(sourceFile, targetPath));
  }

  public void AddRecord(string sourceFile, string targetPath, int msDelay)
  {
    fileFlitConfig.FlitterRecords.Add(
           new FlitterRecord(sourceFile, targetPath, msDelay));
  }

  protected Config()
  {
  }

  public void Serialize()
  {
    XmlTextWriter xtw = new XmlTextWriter("config.xml", Encoding.UTF8);
    xtw.Formatting = Formatting.Indented;
    XmlSerializer xs = new XmlSerializer(typeof(FileFlitConfig));
    xs.Serialize(xtw, fileFlitConfig);
    xtw.Close();
  }

  public static void Deserialize()
  {
    if (File.Exists("config.xml"))
    {
      XmlTextReader xtr = null;

      try
      {
        xtr = new XmlTextReader("config.xml");
        XmlSerializer xs = new XmlSerializer(typeof(FileFlitConfig));
        config = new Config();
        config.fileFlitConfig = (FileFlitConfig)xs.Deserialize(xtr);
      }
      catch (Exception e)
      {
        MessageBox.Show(e.Message, "Error Loading Configuration", 
                        MessageBoxButtons.OK, MessageBoxIcon.Error);
        config = new Config();
      }
      finally
      {
        xtr.Close();
      }
    }
    else
    {
      config = new Config();
    }
  }

  public IEnumerator<FlitterRecord> GetEnumerator()
  {
    return new ConfigEnumerator(fileFlitConfig.FlitterRecords);
  }

  IEnumerator IEnumerable.GetEnumerator()
  {
    return new ConfigEnumerator(fileFlitConfig.FlitterRecords);
  }
}

ConfigEnumerator

This class supports the IEnumerable interface from which the Config class is derived, iterating through the "flitter" records. Nothing new here. Since I don't usually even write IEnumerator implementations, I have no idea how to write one, so I basically just copied an example from MSDN. Like I said, I interview really poorly! I think interviews should allow the interviewee to use Google, the way students can now use calculators for tests. After all, doesn't that show something much more valuable, that I actually know how to find answers rather than have my brain cluttered with essentially useless information? But I digress...

public class ConfigEnumerator : IEnumerator, IEnumerator<FlitterRecord>
{
  protected List<FlitterRecord> records;
  protected int index;

  public ConfigEnumerator(List<FlitterRecord> records)
  {
    this.records = records;
    index = -1;
  }

  public object Current
  {
    get { return records[index]; }
  }

  public bool MoveNext()
  {
    ++index;

    return index < records.Count;
  }

  public void Reset()
  {
    index = -1;
  }

  FlitterRecord IEnumerator<FlitterRecord>.Current
  {
    get { return records[index]; }
  }

  public void Dispose()
  {
  }
}

The User Interface

The user interface was slapped together initially with the idea of having a grid for the file records and a textbox for the log, plus three buttons to add and delete records, and a save button to save the configuration. I quickly realized that I needed a couple buttons for selecting the source file and the target folder, and it would be nice to have a clear button for the log. In the process of writing the file selector, I also realized that being able to select multiple files was a definite must, something that did not make its way into the requirements statement or the implementation requirements. That's a point worth remembering, that consideration of how to handle the interface to the rest of the world ought to be formally specified--obvious in hindsight.

The Form1 Class

I didn't even bother renaming this class!

Some things that are noteworthy (not because of their elegance, in some cases, quite the opposite!):

  • The file record list is converted to and from a DataTable, which is then handed off to the DataView, which becomes the data source for a BindingSource. A BindingSource is used so the Position property can be updated when rows are added, which then allows for updating cell values for the current row of the grid.
  • When a single source file is selected, it updates the currently selected row of the grid. When multiple source files are selected from the file browser, each source file is added as a new record.
  • The static method Log provides a mechanism for logging activity to the form's logger TextBox control without requiring an instance of the form.

The implementation is simple enough that I really didn't feel like it warranted a separate controller. So, what you're seeing here is basically just a View-View-View implementation, with the control concerns built directly into the View. We have a very narcissistic piece of code here, folks. It's all about the form, me, me, me! The form handles the implementation requirements above, #4, #6, and #7.

Oh, and yeah, I think I read somewhere that there's a way to bind a grid to a List, but I'll be damned if I can remember how to make it work so I actually get three columns (I was only getting the Delay field when I tried assigning the List to the DataSource), and I was too lazy to figure it out. And besides, when I implement the grid with a DevExpress XtraGrid control (I find it annoying that I have to write articles using dumbed-down .NET controls, don't you??? - and please don't tell me to use WPF!), who knows what fun things will be possible?

public partial class Form1 : Form
{
  protected static Form1 form;
  protected DataTable dt;
  protected DataView dv;
  protected BindingSource bs;

  public Form1()
  {
    form = this;
    InitializeComponent();
    Setup();
    Populate();
    Assign();
    AddInitialRowIfNoData();
  }

  public static void Log(bool success, FlitterRecord fr, 
                         string message="")
  {
    StringBuilder sb = new StringBuilder();
    DateTime date=DateTime.Now;
    sb.Append(date.ToString("MM/dd/yy"));
    sb.Append(" ");
    sb.Append(date.ToString("HH:mm:ss"));
    sb.Append(" ");

    if (success)
    {
      sb.Append("Copied " + fr.SourceFile + 
                " to " + fr.DestinationFolder);
    }
    else
    {
      sb.Append("Error copying " + fr.SourceFile);
      sb.Append("\r\n");
      sb.Append(message);
    }

    sb.Append("\r\n");

    form.tbLog.Text += sb.ToString();
  }

  protected void Setup()
  {
    dt = new DataTable();
    dt.Columns.Add("SourceFile", typeof(string));
    dt.Columns.Add("TargetFolder", typeof(string));
    dt.Columns.Add("Delay", typeof(int));
    dv = new DataView(dt);
    bs = new BindingSource();
    bs.DataSource = dv;
  }

  protected void Populate()
  {
    foreach (FlitterRecord fr in Config.Records)
    {
      DataRow row = dt.NewRow();
      row["SourceFile"] = fr.SourceFile;
      row["TargetFolder"] = fr.DestinationFolder;
      row["Delay"] = fr.Delay;
      dt.Rows.Add(row);
    }

    dt.AcceptChanges();
  }

  protected void UpdateConfig()
  {
    Config.Records.Clear();

    foreach (DataRow row in dt.Rows)
    {
      Config.Records.AddRecord(row["SourceFile"].ToString(), 
            row["TargetFolder"].ToString(), 
            Convert.ToInt32(row["Delay"]));
    }
  }

  protected void Assign()
  {
    dgFlitter.DataSource = bs;
  }

  private void OnSave(object sender, EventArgs e)
  {
    UpdateConfig();
    Config.Records.Serialize();
  }

  private void OnBrowseSource(object sender, EventArgs e)
  {
    AddInitialRowIfNoData();
    OpenFileDialog fd = new OpenFileDialog();
    fd.RestoreDirectory = true;
    fd.Multiselect = true;
    DialogResult res = fd.ShowDialog();

    if (res == DialogResult.OK)
    {
      foreach (string fn in fd.FileNames)
      {
        // When multiple files are selected, always add new rows.
        if (fd.FileNames.Length > 1)
        {
          AddRow();
          bs.Position = dt.Rows.Count - 1;
        }

        dgFlitter.CurrentRow.Cells["SourceFile"].Value = fn;
      }
    }
  }

  private void OnBrowseTarget(object sender, EventArgs e)
  {
    AddInitialRowIfNoData();
    FolderBrowserDialog fbd = new FolderBrowserDialog();
    DialogResult res = fbd.ShowDialog();

    if (res == DialogResult.OK)
    {
      dgFlitter.CurrentRow.Cells["TargetFolder"].Value = fbd.SelectedPath;
    }
  }

  private void OnRemove(object sender, EventArgs e)
  {
    if (bs.Position != -1)
    {
      bs.RemoveCurrent();
      dt.AcceptChanges();
    }
  }

  private void OnAdd(object sender, EventArgs e)
  {
    AddRow();
  }

  protected void AddRow()
  {
    DataRow row = dt.NewRow();
    row["SourceFile"] = "[source file]";
    row["TargetFolder"] = "[target folder]";
    row["Delay"] = 1000;
    dt.Rows.Add(row);
  }

  protected void AddInitialRowIfNoData()
  {
    if (dt.Rows.Count == 0)
    {
      AddRow();
    }
  }

  private void OnMonitor(object sender, EventArgs e)
  {
    Update();
    Monitor.Go(Config.Records.ToList());
  }

  private void OnUpdateNow(object sender, EventArgs e)
  {
    Update();
    Monitor.UpdateNow(Config.Records.ToList());
  }

  private void OnClear(object sender, EventArgs e)
  {
    tbLog.Text = String.Empty;
  }
}

The Monitor Class

The Monitor class provides two static methods, one for immediately copying source files to target folders, and the other for initiating monitoring of source files. The class instance provides a method for initializing the FileSystemWatcher instances (actually, a derived implementation, so that the FlitterRecord can be associated with the file watcher) and the file changed event handler. OK, maybe Form1 isn't as narcissistic as I originally made it out to be, why gosh, here's sort of a controller!

public class Monitor
{
  protected List<FileSystemWatcher> watchList;
  protected static Monitor monitor;

  public static void Go(List<FlitterRecord> fileList)
  {
    if (monitor == null)
    {
      monitor = new Monitor();
    }

    monitor.WatchFiles(fileList);
  }

  public static void UpdateNow(List<FlitterRecord> fileList)
  {
    foreach (FlitterRecord fr in fileList)
    {
      try
      {
        File.Copy(fr.SourceFile, Path.Combine(fr.DestinationFolder, 
                  Path.GetFileName(fr.SourceFile)), true);
        Form1.Log(true, fr);
      }
      catch (Exception e)
      {
        Form1.Log(false, fr, e.Message);
      }
    }
  }

  public Monitor()
  {
    watchList = new List<FileSystemWatcher>();
  }

  protected void WatchFiles(List<FlitterRecord> fileList)
  {
    foreach (FileSystemWatcher fsw in watchList)
    {
      fsw.Changed -= OnChanged;
    }

    List<FileSystemWatcher> newWatchList = 
                  new List<FileSystemWatcher>();

    foreach (FlitterRecord fr in fileList)
    {
      FlitterFileSystemWatcher fsw = new FlitterFileSystemWatcher(
        Path.GetDirectoryName(fr.SourceFile), 
        Path.GetFileName(fr.SourceFile)) { FlitterRecord = fr };
      fsw.NotifyFilter = NotifyFilters.LastWrite;
      fsw.Changed += new FileSystemEventHandler(OnChanged);
      fsw.EnableRaisingEvents = true;
      newWatchList.Add(fsw);
    }

    watchList = newWatchList;
  }

  protected void OnChanged(object sender, FileSystemEventArgs e)
  {
    FlitterRecord fr = ((FlitterFileSystemWatcher)sender).FlitterRecord;
    bool success=false;
    int retries = 3;

    while (!success)
    {
      try
      {
        Thread.Sleep(fr.Delay);
        File.Copy(fr.SourceFile, Path.Combine(fr.DestinationFolder, 
                  Path.GetFileName(fr.SourceFile)), true);
        success = true;
      }
      catch(Exception ex)
      {
        if (--retries == 0)
        {
          Form1.Log(false, fr, ex.Message);
          break;
        }
      }

      if (success)
      {
        Form1.Log(true, fr);
      }
    }
  }
}

Conclusion

I hope you've enjoyed this little adventure. And hopefully, if you actually use this utility, you will remember to click on the "Monitor!" button to get the thing going in an automated mode, unlike me, who keeps forgetting and then wondering why nothing got transferred to my distribution folders after compiling the project! So an indicator would be nice, and gee, maybe a way to stop the monitoring of files? Ya' think?

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Marc Clifton

United States United States
Marc is the creator of two open source projets, MyXaml, a declarative (XML) instantiation engine and the Advanced Unit Testing framework, and Interacx, a commercial n-tier RAD application suite.  Visit his website, www.marcclifton.com, where you will find many of his articles and his blog.
 
Marc lives in Philmont, NY.

Comments and Discussions

 
GeneralMy vote of 5 PinprofessionalMaimonides20-Jul-13 20:14 
GeneralMy vote of 5 Pinmembermanoj kumar choubey2-Mar-12 22:57 
GeneralMy vote of 5 PinmemberArchKaine17-Jan-11 12:41 
GeneralSnazzy Frameworks... PinmemberAndrew Rissing17-Jan-11 6:35 
GeneralRe: Snazzy Frameworks... PinprotectorMarc Clifton17-Jan-11 8:11 
GeneralRe: Snazzy Frameworks... PinmemberAndrew Rissing18-Jan-11 4:25 
GeneralRe: Snazzy Frameworks... PinprotectorMarc Clifton18-Jan-11 4:57 
GeneralExcellent PinmemberPranay Rana16-Jan-11 18:52 
GeneralRe: Excellent PinprotectorMarc Clifton17-Jan-11 8:13 
GeneralMy vote of 5 PinmemberRaviRanjankr16-Jan-11 2:57 
GeneralRe: My vote of 5 PinprotectorMarc Clifton17-Jan-11 8:13 
GeneralMy vote of 5 Pinmemberlinuxjr16-Jan-11 2:36 
GeneralRe: My vote of 5 PinprotectorMarc Clifton17-Jan-11 8:13 
GeneralMy Vote of 5 PinmemberScruffyDuck16-Jan-11 1:52 
GeneralRe: My Vote of 5 PinprotectorMarc Clifton17-Jan-11 8:13 
GeneralA very useful app there Marc PinmvpSacha Barber15-Jan-11 21:27 
GeneralRe: A very useful app there Marc PinprotectorMarc Clifton17-Jan-11 8:12 
QuestionWho cares? PinmvpPete O'Hanlon15-Jan-11 21:20 
AnswerRe: Who cares? PinprotectorMarc Clifton17-Jan-11 8:12 
GeneralMy vote of 5 Pinmvpthatraja15-Jan-11 18:47 
GeneralRe: My vote of 5 PinprotectorMarc Clifton17-Jan-11 8:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140827.1 | Last Updated 15 Jan 2011
Article Copyright 2011 by Marc Clifton
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid