65.9K
CodeProject 正在变化。 阅读更多。
Home

文件过滤器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (22投票s)

2011年1月15日

CPOL

10分钟阅读

viewsIcon

53827

downloadIcon

819

监视文件,并在文件发生更改时将其复制到指定文件夹。

Screenshot.png

引言

File Flit 是一个简单的实用程序,它监视现有文件的更改,并在检测到更改时将文件复制到现有的目标文件夹。我编写此实用程序的目的是因为我在互联网上没有找到任何功能完全相同且设置简单的工具。因此,在开始编写代码时,我想到不如从开发这个实用程序的角度来写这篇文章,大概用了四个小时。实际上,写这篇文章花费的时间,如果算上的话,可能比写代码还长!你在这里看不到优雅的代码或 WPF、WCF、Silverlight、MVVM 等炫酷的技术。这些根本不需要。你看到的是一段极其简单的代码,它能做,嗯,就是它应该做的,以及关于代码每一部分的一些故事。

需求说明

无论我是在寻找现有的应用程序、一些代码,还是考虑自己动手实现,我都喜欢从需求说明开始,这让人想起 Grady Booch 和其他早期面向对象设计方法的时代。在教授编程时,我也以需求说明开始——用文字表述程序应该做什么。令人惊讶的是,人们发现这项任务有多么困难!所以这是我的需求说明:

程序应监视一个或多个文件何时被更新(通过其更改日期),然后将文件复制到一个或多个目标位置。源和目标应以轻量级的 XML 格式保存在配置文件中,一个简单的 UI 应允许编辑文件列表。每个被监视的文件都可以指定一个延迟时间,从检测到文件更改到执行复制操作之间,以便程序可以补偿文件更新时间。UI 还应显示活动日志以及尝试复制文件时发生的错误。源始终是文件(目前不允许使用通配符),目标始终是文件应存在的目标文件夹。程序可以假定文件和文件夹始终存在。

从这个需求说明中,我只需查看说明,就可以提取出以下实现需求:

  1. 一个处理 XML 序列化/反序列化的配置类
  2. 一个类,用于维护每个“flitter”记录的属性值(源、目标、延迟)
  3. 一个列表,用于维护记录的集合
  4. 一个用于显示配置的 UI
  5. 一个“服务”,用于监视源文件更改并在更改时进行复制
  6. 一种记录成功和失败的方式
  7. 一个用于显示日志文件的 UI

我非常喜欢这种方法,并且将其作为我所有工作的基石。它的美妙之处在于,我可以将高度抽象的需求递归地细化为越来越具体的要求,最终得到一个非常简洁的要求定义,包括事物(名词)和活动(动词),用户与事物之间的交互,以及事物本身之间的交互。

上述需求在期望的时间范围内得到了轻松实现,接下来我将逐一讨论。

配置类

配置类一开始很简单,但很快就扩展为四个不同的类:

  • Config 类本身,其方法通过单例属性访问
  • 可序列化的配置类 FileFlitConfig,它仅维护一个文件记录列表
  • FlitterRecord 类,它维护单个文件记录的属性值
  • 一个实现 IEnumerable 的类 ConfigEnumerator,用于遍历记录列表

我在实现需求中唯一没有考虑到的部分是记录列表的枚举,坦白说,这是一种“为了方便”而实现的折衷方案,如果配置类维护的不仅仅是文件记录集合,我可能永远都不会创建它。这些类负责实现需求 #1-3。

FlitterRecord 类

此类维护单个“Flitter”记录的属性值。因为我喜欢可读的 XML,所以我用 XmlAttribute 标签修饰了属性,稍后您会注意到序列化使用了 Formatting.Indented 格式化选项。我最初创建了两个构造函数,一个是不需要指定延迟时间即可开始复制。后来在某个方法中,我使用了默认参数值,所以我想,嗯,我应该保持一致,并在构造函数这里也使用默认参数值。所以现在,我们面临着一个有趣的问题:多个构造函数更好还是默认参数值更好?我删除了第一个构造函数,有趣的是,Visual Studio 2010 用错误标记了代码。

但它编译得很好。所以现在,我的代码上有一个小红旗,即使它没有任何问题!

当然,为了反序列化,默认构造函数是必需的,这样类才能被实例化。

[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 类

此类维护 FileFlitter 记录的集合。我最初将 List 放入了 Config 类(见下文),但在添加 IEnumerable 接口后开始遇到序列化错误。来自 MSDN:

XmlSerializer 对实现 IEnumerableICollection 的类有特殊处理。实现 IEnumerable 的类必须实现一个接受单个参数的公共 Add 方法。Add 方法的参数必须与 GetEnumerator 返回的值的 Current 属性返回的类型相同,或者与该类型之一的基础类型相同。

IEnumerable 使序列化更加复杂!所以,不想学习新东西,我决定将正在序列化的类(维护 FlitterRecord 实例的集合)分开。我认为,这可能是一个更好的实现方式。

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

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

Config 类

这不是一个很优雅的类,它结合了记录集合类的枚举和封装,以及支持清空列表、向列表中添加记录以及序列化/反序列化列表的方法。有一个静态的静态属性“Records”,如果配置未初始化,它将在属性 getter 中进行初始化。我想这是一个准工厂模式和单例模式。它是工厂模式,因为它处理记录集合的实例化,但它是单例模式,因为它返回自身的实例且构造函数是受保护的。好吧,不管怎样。模式是给那些像书呆子一样在同事隔间里搂着胳膊,试图自我神化的家伙们聊天的。我真想知道为什么我面试不顺利!

哦,还有,在反序列化过程中遇到的错误会通过在反序列化方法中显示的 MessageBox 报告给用户——真是糟糕的关注点分离!如果发生反序列化错误,getter 将返回一个 Config 实例,并且 FlitterRecord 集合将为空。我们真的需要抛出异常吗?我们真的需要告知调用者有错误发生,对吗?我们还能为用户提供什么选择,除了不情愿地点击“OK”按钮:“是的,有错误,你无能为力!!!” 简单!

[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

此类支持 Config 类派生的 IEnumerable 接口,用于遍历“flitter”记录。这里没有什么新鲜事。由于我通常甚至不写 IEnumerator 实现,所以我不知道如何写,所以我基本上只是从 MSDN 复制了一个示例。正如我所说,我的面试表现很差!我认为面试应该允许被面试者使用 Google,就像现在的学生考试可以使用计算器一样。毕竟,这难道不更能说明一些有价值的东西吗?比如,我实际上知道如何找到答案,而不是让我的大脑充斥着基本无用的信息?但跑题了……

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()
  {
  }
}

用户界面

用户界面最初只是草草搭建的,目的是有一个用于文件记录的网格和一个用于日志的文本框,外加三个按钮用于添加和删除记录,以及一个保存按钮用于保存配置。我很快意识到我需要几个按钮来选择源文件和目标文件夹,并且有一个清除日志的按钮会很好。在编写文件选择器的过程中,我也意识到能够选择多个文件绝对是必须的,这一点没有被纳入需求说明或实现需求中。这一点值得记住,即处理与世界其他部分的接口的方式应该被正式指定——事后看来显而易见。

Form1 类

我甚至懒得重命名这个类!

一些值得注意的事情(在某些情况下,并非因为它们很优雅,反而恰恰相反!)

  • 文件记录列表被转换为 DataTable,然后传递给 DataView,后者成为 BindingSource 的数据源。使用 BindingSource 是为了在添加行时更新 Position 属性,这样就可以更新网格当前行的单元格值。
  • 当选择单个源文件时,它会更新网格中当前选定的行。当从文件浏览器中选择多个源文件时,每个源文件都会添加为一条新记录。
  • 静态方法 Log 提供了一种机制,可以在不要求窗体实例的情况下将活动记录到窗体的日志 TextBox 控件。

实现足够简单,以至于我真的觉得它不需要一个单独的控制器。所以,你在这里看到的是基本上一个 View-View-View 的实现,控制逻辑直接内置在 View 中。各位,我们这里有一段非常自恋的代码。一切都关于窗体,我,我,我!窗体处理上述实现需求 #4、#6 和 #7。

哦,还有,是的,我想我在哪里读到过,有一种方法可以将网格绑定到 List,但我发誓我记不起如何实现三个列(当我尝试将 List 分配给 DataSource 时,我只得到 Delay 字段),而且我太懒了,不愿意弄清楚。而且,当我用 DevExpress XtraGrid 控件实现网格时(我发现写文章使用简化版的 .NET 控件很烦人,你们不这么觉得吗???——而且请不要让我使用 WPF!),谁知道会有什么有趣的功能呢?

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;
  }
}

Monitor 类

Monitor 类提供了两个静态方法,一个用于立即将源文件复制到目标文件夹,另一个用于启动源文件的监视。类实例提供了一个方法来初始化 FileSystemWatcher 实例(实际上是一个派生实现,以便 FlitterRecord 可以与文件监视器关联)以及文件更改事件处理程序。好吧,也许 Form1 没有我最初说的那么自恋,天哪,这里好像有个控制器!

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);
      }
    }
  }
}

结论

希望您喜欢这次小小的冒险。希望您在实际使用此实用程序时,会记得点击“Monitor!”按钮使其进入自动模式,不像我,总是忘记,然后奇怪为什么在编译项目后没有任何东西传输到我的分发文件夹!所以一个指示器会很好,而且,也许有一种方法可以停止监视文件?你觉得呢?

© . All rights reserved.