C#中如何让FileSystemWatcher批量复制粘贴时仅触发一次变更事件?
你推测的原因完全正确——批量复制/移动文件时,FileSystemWatcher会为每个文件触发一次Created事件,而你的GetFiles方法在执行清理(比如Rows.Clear()、DataSource = null)这类UI操作时,如果被重复调用,很容易因为控件还处于前一次操作的占用状态,引发线程冲突或者操作混乱,尤其是WinForms控件本身不是线程安全的,这就会导致程序报错。
下面给你几个针对性的解决方案,一步步修复这个问题:
1. 给GetFiles加执行锁,防止并发调用
你已经有了getFRunning这个变量,但原来的实现没有在方法开头做判断,而且如果方法中间抛出异常,getFRunning可能不会被重置为false。修改GetFiles方法,确保同一时间只有一个实例在执行:
public void GetFiles() { // 如果当前正在执行,直接返回 if (getFRunning) return; getFRunning = true; try { ds.Clear(); this.dgArchivos.DataSource = null; this.dgArchivos.Rows.Clear(); this.dgArchivos.Refresh(); filesPath = Clases.Utilerias.RutaArchivos; this.lblFilePath.Text = filesPath; DirectoryInfo di = new DirectoryInfo(filesPath); var archivos = di.GetFiles("*.pdf"); DataRow dr; //PASO 1 -- Cargamos el XML string rutaConfig = string.Format(@"{0}\{1}", filesPath, "config.xml"); FileInfo fi = new FileInfo(rutaConfig); if (fi.Exists) { ds.ReadXml(rutaConfig); } //PASO 2 -- Agregamos los archivos que no existan en el xml foreach (var archivo in archivos) { if (!ExisteArchivoEnTabla(archivo.Name)) { dr = ds.Tables[0].NewRow(); dr["FileName"] = archivo.Name; ds.Tables[0].Rows.Add(dr); } } this.dgArchivos.DataSource = ds.Tables[0]; if (dgArchivos.Rows.Count > 0) { int Total = (dgArchivos.Rows[0].Cells.Count - 2); dgArchivos.Columns[Total].DefaultCellStyle.NullValue = "Preview"; } } finally { // 无论是否报错,都重置标记 getFRunning = false; } }
2. 延迟合并事件触发,避免重复刷新
批量操作时,事件会在短时间内密集触发,我们可以用一个定时器来“合并”这些触发:只有当最后一次事件触发后,等待几百毫秒没有新事件,再执行刷新操作。这样即使有10个文件触发事件,最终也只会执行一次GetFiles。
在你的frmProcessFile窗体中添加定时器逻辑:
// 窗体类中添加定时器字段 private System.Windows.Forms.Timer _refreshTimer; // 在窗体构造函数中初始化定时器 public frmProcessFile() { InitializeComponent(); _refreshTimer = new System.Windows.Forms.Timer(); _refreshTimer.Interval = 500; // 设置500毫秒延迟,可根据实际情况调整 _refreshTimer.Tick += (s, e) => { _refreshTimer.Stop(); GetFiles(); // 延迟后执行刷新 }; } // 修改FileWatcher中的事件处理方法,不要直接调用GetFiles,而是重置定时器 private static void OnChanged(object source, FileSystemEventArgs e) { Console.WriteLine("File: " + e.FullPath + " " + e.ChangeType); frm.formPadre.xmlCreateOrUpdate(); // 切换到UI线程操作定时器(FileSystemWatcher事件在后台线程触发) frm.BeginInvoke(new Action(() => { frm._refreshTimer.Stop(); frm._refreshTimer.Start(); })); } // 同样修改OnDeleted和OnRenamed方法,用定时器替代直接调用GetFiles private static void OnDeleted(object source, FileSystemEventArgs e) { Console.WriteLine("File: " + e.FullPath + " " + e.ChangeType); string[] file = e.FullPath.Split('\\'); string fName = file[file.Length - 1]; frm.updateXML(fName); frm.BeginInvoke(new Action(() => { frm._refreshTimer.Stop(); frm._refreshTimer.Start(); })); } private static void OnRenamed(object source, RenamedEventArgs e) { Console.WriteLine("File: {0} renamed to {1}", e.OldFullPath, e.FullPath); frm.formPadre.xmlCreateOrUpdate(); frm.BeginInvoke(new Action(() => { frm._refreshTimer.Stop(); frm._refreshTimer.Start(); })); }
3. 确保UI操作在主线程执行
FileSystemWatcher的事件是在后台线程触发的,直接调用操作UI控件的方法(比如GetFiles)会引发跨线程操作异常。上面的代码已经用BeginInvoke切换到了UI线程,这一点一定要注意,很多时候批量操作的报错也和跨线程有关。
把这几个方案结合起来,就能彻底解决批量文件操作时的报错问题了——既避免了GetFiles的并发调用,又合并了密集的事件触发,同时保证了UI操作的线程安全性。
内容的提问来源于stack exchange,提问作者Jorge Herrera

