Reputation: 4029
I have an application where I am looking for a text file and if there are any changes made to the file I am using the OnChanged
eventhandler to handle the event. I am using the NotifyFilters.LastWriteTime
but still the event is getting fired twice. Here is the code.
public void Initialize()
{
FileSystemWatcher _fileWatcher = new FileSystemWatcher();
_fileWatcher.Path = "C:\\Folder";
_fileWatcher.NotifyFilter = NotifyFilters.LastWrite;
_fileWatcher.Filter = "Version.txt";
_fileWatcher.Changed += new FileSystemEventHandler(OnChanged);
_fileWatcher.EnableRaisingEvents = true;
}
private void OnChanged(object source, FileSystemEventArgs e)
{
.......
}
In my case the OnChanged
is called twice, when I change the text file version.txt
and save it.
Upvotes: 402
Views: 176648
Reputation: 38376
I am afraid that this is a well-known bug/feature of the FileSystemWatcher
class. This is from the documentation of the class:
You may notice in certain situations that a single creation event generates multiple Created events that are handled by your component. For example, if you use a FileSystemWatcher component to monitor the creation of new files in a directory, and then test it by using Notepad to create a file, you may see two Created events generated even though only a single file was created. This is because Notepad performs multiple file system actions during the writing process. Notepad writes to the disk in batches that create the content of the file and then the file attributes. Other applications may perform in the same manner. Because FileSystemWatcher monitors the operating system activities, all events that these applications fire will be picked up.
Now this bit of text is about the Created
event, but the same thing applies to other file events as well. In some applications you might be able to get around this by using the NotifyFilter
property, but my experience says that sometimes you have to do some manual duplicate filtering (hacks) as well.
A while ago I bookedmarked a page with a few FileSystemWatcher tips(Archived). You might want to check it out.
Upvotes: 312
Reputation: 36035
Skipping the duplicates is too risky, as the code could see an incomplete version of the data.
Instead, we can wait until there are no changes for a specified merge milliseconds.
Important: the examples below are only really suited when you want a single notification when one or multiple files change. One of the reasons is it hardcodes the NotifyFilter, which could be modified to allow it to be changed. The other reason is it does not tell you which files changed or the type of change (FileSystemEventArgs), which could also be modified to provide a list of all the changes detected.
A big downside is it does not fire if the files are updated more often than the merge milliseconds.
private sealed class SingleFireFilesWatcher : IDisposable
{
public event EventHandler? Changed;
private readonly FileSystemWatcher watcher;
private bool disposed;
public SingleFireFilesWatcher(int mergeMilliseconds, params string[] filters)
{
var timer = new Timer(_ => Changed?.Invoke(this, EventArgs.Empty));
watcher = new FileSystemWatcher();
foreach (var filter in filters)
watcher.Filters.Add(filter);
watcher.NotifyFilter = NotifyFilters.LastWrite;
watcher.Changed += (s, e) => timer.Change(mergeMilliseconds, Timeout.Infinite);
watcher.EnableRaisingEvents = true;
}
public void Dispose()
{
if (disposed) return;
disposed = true;
watcher.Dispose();
}
}
A solution to the above limitation is to always trigger after merge milliseconds of the first change. Code is only slightly more complex.
One downside to this one is that if mergeMilliseconds is set very low, you may get plenty of extra firings.
private sealed class SingleFireFilesWatcher2 : IDisposable
{
public event EventHandler? Changed;
private readonly FileSystemWatcher watcher;
private bool disposed;
public SingleFireFilesWatcher2(int mergeMilliseconds, params string[] filters)
{
var restartTimerOnNextChange = true;
var timer = new Timer(_ =>
{
restartTimerOnNextChange = true;
Changed?.Invoke(this, EventArgs.Empty);
});
watcher = new FileSystemWatcher();
foreach (var filter in filters)
watcher.Filters.Add(filter);
watcher.NotifyFilter = NotifyFilters.LastWrite;
watcher.Changed += (s, e) =>
{
if (restartTimerOnNextChange)
timer.Change(mergeMilliseconds, Timeout.Infinite);
restartTimerOnNextChange = false;
};
watcher.EnableRaisingEvents = true;
}
public void Dispose()
{
if (disposed) return;
disposed = true;
watcher.Dispose();
}
}
Upvotes: 1
Reputation: 43951
Here is another approach. Instead of propagating the first event of a quick succession of events and suppressing all that follow, here all events are suppressed except from the last one. I think that the scenarios that can benefit from this approach are more common. The technical term for this strategy is debouncing.
To make this happen we must use a sliding delay. Every incoming event cancels the timer that would fire the previous event, and starts a new timer. This opens the possibility that a never-ending series of events will delay the propagation forever. To keep things simple, there is no provision for this abnormal case in the extension methods below.
public static class FileSystemWatcherExtensions
{
/// <summary>
/// Subscribes to the events specified by the 'changeTypes' argument.
/// The handler is not invoked, until an amount of time specified by the
/// 'dueTime' argument has elapsed after the last recorded event associated
/// with a specific file or directory (debounce behavior). The handler
/// is invoked with the 'FileSystemEventArgs' of the last recorded event.
/// </summary>
public static IDisposable OnAnyEvent(this FileSystemWatcher source,
WatcherChangeTypes changeTypes, FileSystemEventHandler handler,
TimeSpan dueTime)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(handler);
if (dueTime < TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(dueTime));
Dictionary<string, CancellationTokenSource> dictionary = new(
StringComparer.OrdinalIgnoreCase);
if (changeTypes.HasFlag(WatcherChangeTypes.Created))
source.Created += FileSystemWatcher_Event;
if (changeTypes.HasFlag(WatcherChangeTypes.Deleted))
source.Deleted += FileSystemWatcher_Event;
if (changeTypes.HasFlag(WatcherChangeTypes.Changed))
source.Changed += FileSystemWatcher_Event;
if (changeTypes.HasFlag(WatcherChangeTypes.Renamed))
source.Renamed += FileSystemWatcher_Event;
return new Disposable(() =>
{
source.Created -= FileSystemWatcher_Event;
source.Deleted -= FileSystemWatcher_Event;
source.Changed -= FileSystemWatcher_Event;
source.Renamed -= FileSystemWatcher_Event;
lock (dictionary)
{
foreach (CancellationTokenSource cts in dictionary.Values)
cts.Cancel();
dictionary.Clear();
}
});
async void FileSystemWatcher_Event(object sender, FileSystemEventArgs e)
{
string key = e.FullPath;
using (CancellationTokenSource cts = new())
{
lock (dictionary)
{
CancellationTokenSource existingCts;
dictionary.TryGetValue(key, out existingCts);
dictionary[key] = cts;
existingCts?.Cancel(); // Cancel the previous event
}
Task delayTask = Task.Delay(dueTime, cts.Token);
await Task.WhenAny(delayTask).ConfigureAwait(false); // No throw
if (delayTask.IsCanceled) return; // Preempted
lock (dictionary)
{
CancellationTokenSource existingCts;
dictionary.TryGetValue(key, out existingCts);
if (!ReferenceEquals(existingCts, cts)) return; // Preempted
dictionary.Remove(key); // Clean up before invoking the handler
}
}
// Invoke the handler the same way it's done in the .NET source code
ISynchronizeInvoke syncObj = source.SynchronizingObject;
if (syncObj != null && syncObj.InvokeRequired)
syncObj.BeginInvoke(handler, new object[] { sender, e });
else
handler(sender, e);
}
}
public static IDisposable OnAllEvents(this FileSystemWatcher source,
FileSystemEventHandler handler, TimeSpan delay)
=> OnAnyEvent(source, WatcherChangeTypes.All, handler, delay);
public static IDisposable OnCreated(this FileSystemWatcher source,
FileSystemEventHandler handler, TimeSpan delay)
=> OnAnyEvent(source, WatcherChangeTypes.Created, handler, delay);
public static IDisposable OnDeleted(this FileSystemWatcher source,
FileSystemEventHandler handler, TimeSpan delay)
=> OnAnyEvent(source, WatcherChangeTypes.Deleted, handler, delay);
public static IDisposable OnChanged(this FileSystemWatcher source,
FileSystemEventHandler handler, TimeSpan delay)
=> OnAnyEvent(source, WatcherChangeTypes.Changed, handler, delay);
public static IDisposable OnRenamed(this FileSystemWatcher source,
FileSystemEventHandler handler, TimeSpan delay)
=> OnAnyEvent(source, WatcherChangeTypes.Renamed, handler, delay);
private class Disposable : IDisposable
{
private Action _action;
public Disposable(Action action) => _action = action;
public void Dispose()
{
try { _action?.Invoke(); } finally { _action = null; }
}
}
}
Usage example:
IDisposable subscription = myWatcher.OnAnyEvent(
WatcherChangeTypes.Created | WatcherChangeTypes.Changed,
MyFileSystemWatcher_Event, TimeSpan.FromMilliseconds(100));
This line combines the subscription to two events, the Created
and the Changed
. So it is roughly equivalent to these:
myWatcher.Created += MyFileSystemWatcher_Event;
myWatcher.Changed += MyFileSystemWatcher_Event;
The difference is that the two events are regarded as a single type of event, and in case of a quick succession of these events only the last one will be propagated. For example if a Created
event is followed by two Changed
events, and there is no time gap larger than 100 msec between these three events, only the second Changed
event will be propagated by invoking the MyFileSystemWatcher_Event
handler, and the previous events will be discarded.
The IDisposable
return value can be used to unsubscribe from the events. Calling subscription.Dispose()
cancels and discards all recorded events, but it doesn't stop or wait for any handlers that are in the midst of their execution.
Specifically for the Renamed
event, the FileSystemEventArgs
argument can be cast to RenamedEventArgs
in order to access the extra information of this event. For example:
void MyFileSystemWatcher_Event(object s, FileSystemEventArgs e)
{
if (e is RenamedEventArgs re) Console.WriteLine(re.OldFullPath);
The debounced events are invoked on the FileSystemWatcher.SynchronizingObject
, if it has been configured, otherwise on the ThreadPool
. The invocation logic has been copy-pasted from the .NET 7 source code.
Upvotes: 2
Reputation: 59
This is quite late, but I faced it recently, then I'd like to post my little contribution.
First, many proposed solutions look to work for a single updated file, while I needed to be notified about 2-3 changed files in a short time (tens of milliseconds), over a relatively long repetition time (tens of seconds to minutes).
One of the most interesting early suggested links is FileSystemWatcher is a Bit Broken. However, the proposed solution is only partially working, as pointed out by the same author in Erratic Behaviour from .NET MemoryCache Expiration Demystified, which gives a notification even after 20 seconds.
What I've done then is craft a silly alternative solution, based on a similar principle, without MemoryCache
.
Basically, it creates a List<>
of items, with a Key
which is the full path to the file and an expiration timer. If another event triggers again the change, the element is found in the list and the timer is updated with a new expiration.
The expiration is empirically long enough to gather multiple events in a single OnStableChange
notification, and not too long to feel unresponsive.
When you instantiate Whatever
you also Link
it to a directory and a very basic external callback.
Nothing is really optimized, I just looked for a solution in a few lines.
I'm publishing it here as
internal class Whatever
{
private FileSystemWatcher? watcher = null;
public delegate void DelegateFileChange(string path);
public DelegateFileChange? onChange;
private const int CacheTimeMilliseconds = 200;
private class ChangeItem
{
public delegate void DelegateChangeItem(string key);
public string Key { get; set; } = "";
public System.Timers.Timer Expiration = new();
public DelegateChangeItem? SignalChanged = null;
}
private class ChangeCache
{
private readonly List<ChangeItem> _changes = new();
public void Set(string key, int milliSecs, ChangeItem.DelegateChangeItem? signal = null)
{
lock (_changes)
{
ChangeItem? existing = _changes.Find(item => item.Key == key);
if (existing != null)
{
existing.Expiration.Interval = milliSecs;
existing.SignalChanged = signal;
}
else
{
ChangeItem change = new()
{
Key = key,
SignalChanged = signal
};
change.Expiration.Interval = milliSecs;
change.Expiration.AutoReset = false;
change.Expiration.Elapsed += delegate { Change_Elapsed(key); };
change.Expiration.Enabled = true;
_changes.Add(change);
}
}
}
private void Change_Elapsed(string key)
{
lock (_changes)
{
ChangeItem? existing = _changes.Find(item => item.Key == key);
existing?.SignalChanged?.Invoke(key);
_changes.RemoveAll(item => item.Key == key);
}
}
}
private ChangeCache changeCache = new();
public bool Link(string directory, DelegateFileChange? fileChange = null)
{
bool result = false;
try
{
if (Directory.Exists(directory))
{
watcher = new FileSystemWatcher(directory);
watcher.NotifyFilter = NotifyFilters.LastWrite;
watcher.Changed += Watcher_Changed;
onChange = fileChange;
watcher.Filter = "*.*";
watcher.IncludeSubdirectories = true;
watcher.EnableRaisingEvents = true;
result = true;
}
}
catch (Exception)
{
}
return result;
}
private void OnStableChange(string path)
{
if (File.Exists(path))
{
onChange?.Invoke(path);
}
}
public void Watcher_Changed(object sender, FileSystemEventArgs e)
{
changeCache.Set(e.FullPath, CacheTimeMilliseconds, OnStableChange);
}
}
Upvotes: 0
Reputation: 41
I hereby bequeath this code to future generations:
static DateTimeOffset lastChanged = DateTimeOffset.UtcNow;
static string lastChangedFile = null;
...
private static void OnChanged(object sender, FileSystemEventArgs e)
{
if (e.ChangeType != WatcherChangeTypes.Changed ||
(lastChanged.AddMilliseconds(500) > DateTimeOffset.UtcNow && lastChangedFile == e.FullPath)
)
{
return;
}
lastChanged = DateTimeOffset.UtcNow;
lastChangedFile = e.FullPath;
Console.WriteLine($"Changed: {e.FullPath}");
}
Upvotes: 0
Reputation: 119
I used n way simpler approach.
Upvotes: 0
Reputation: 11
We can make it simple like this. It works for me.
private static void OnChanged(object sender, FileSystemEventArgs e)
{
if (File.GetAttributes(e.FullPath) == FileAttributes.Directory)
return;
double timeSpan = DateTime.Now.Subtract(File.GetLastWriteTime(e.FullPath)).TotalSeconds;
if (timeSpan > 1)
return;
Console.WriteLine($"Changed: {e.FullPath}");
}
Upvotes: 1
Reputation: 5232
The solution really depends on the use case. Are you watching out for new files that don't change, or one file that changes every once in a while, of changes very often? In my case, it changes not too often, and I don't want to miss any of these changes.
But I also do not want the change event where the writing process is not yet done writing.
In my case, I noticed 6 (six!!) onchange events on writing a 125 char txt file.
My solution is a mix of polling, that is often looked negatively at, and the change-event. Normal polling is slow, say every 10 seconds, just in case the FileSystemWatcher (FSW) "misses" an event. The polling responds immediately to a FSW change event.
The trick is that at a FSW.Change event, the polling goes faster, say every 100 ms, and waits until the file is stable. So we have "two phased polling": phase 1 is slow, but responds immediately on a FSW file change event. Phase 2 is fast, waiting for a stable file.
If the FSW detects multiple file changes, each of those events speeds up the polling loop, and will effectively start a new, short, waiting cycle. Only after the polling loop detects no further change in file last write time, it assumes that the file is stable, and your code may handle the changed file.
I chose timeouts of 10 seconds and 100 ms, but your use case may need different timeout values.
Here is the polling, where AppConfig.fiIO
is the FileInfo
to watch for:
private readonly EventWaitHandle ewhTimeout = new AutoResetEvent(false);
private void TwoPhasedPolling()
{
bool WaitForChange = true; //false: wait until stable
DateTime LastWriteTime = DateTime.MinValue;
while (true)
{
// wait for next poll (timeout), or FSW event
bool GotOne = ewhTimeout.WaitOne(WaitForChange ? 10 * 1000 : 100);
if (GotOne)
{
// WaitOne interrupted: end of Phase1: FSW detected file change
WaitForChange = false;
}
else
{
// WaitOne timed out: Phase2: check file write time for change
if (AppConfig.fiIO.LastWriteTime > LastWriteTime)
{
LastWriteTime = AppConfig.fiIO.LastWriteTime;
}
else
{
// End of Phase2: file has changed and is stable
WaitForChange = true;
// action on changed file
... your code here ...
}}}}
private void fileSystemWatcher1_Changed(object sender, FileSystemEventArgs e)
{
ewhTimeout.Set();
}
NB: yes, I don't like }}}}
either, but it makes the listing shorter so that you don't have to scroll :-)
Upvotes: 0
Reputation: 1
Been searching for answer but I came up with a dirty solution. Since my event fires twice, second action does nothing.
$count = 1
$action = {
if($count -eq 1){
#DO SOMETHING
$count = 2
}else{
$count = 1
}
}
Upvotes: -1
Reputation: 21
I think the best solution to solve the issue is to use reactive extensions When you transform event into observable, then you can just add Throttling(..) (originally called Debounce(..))
Sample code here
var templatesWatcher = new FileSystemWatcher(settingsSnapshot.Value.TemplatesDirectory)
{
NotifyFilter = NotifyFilters.LastWrite,
IncludeSubdirectories = true
};
templatesWatcher.EnableRaisingEvents = true;
Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
addHandler => templatesWatcher.Changed += addHandler,
removeHandler => templatesWatcher.Changed -= removeHandler)
.Throttle(TimeSpan.FromSeconds(5))
.Subscribe(args =>
{
_logger.LogInformation($"Template file {args.EventArgs.Name} has changed");
//TODO do something
});
Upvotes: 2
Reputation: 3020
I wanted to react only on the last event, just in case, also on a linux file change it seemed that the file was empty on the first call and then filled again on the next and did not mind loosing some time just in case the OS decided to do some file/attribute change.
I am using .NET async here to help me do the threading.
private static int _fileSystemWatcherCounts;
private async void OnChanged(object sender, FileSystemEventArgs e)
{
// Filter several calls in short period of time
Interlocked.Increment(ref _fileSystemWatcherCounts);
await Task.Delay(100);
if (Interlocked.Decrement(ref _fileSystemWatcherCounts) == 0)
DoYourWork();
}
Upvotes: 5
Reputation: 734
Code with customizable disabling of the time interval of blocking the second watcher raising and without blocking over watchers if they exist:
namespace Watcher
{
class Static
{
public static DateTime lastDomain { get; set; }
public static string lastDomainStr { get; set; }
}
public partial class Form1 : Form
{
int minMs = 20;//time for blocking in ms
public Form1()
{
InitializeComponent();
Static.lastDomain = new DateTime(1970, 1, 1, 0, 0, 0);
Static.lastDomainStr = "";
Start();
}
private void Start()//Start watcher
{
//...
domain.Changed += new FileSystemEventHandler(Domain);
domain.EnableRaisingEvents = true;
//...you second unblocked watchers
second.Changed += new FileSystemEventHandler(Second);
second.EnableRaisingEvents = true;
}
private void Domain(object source, FileSystemEventArgs e)
{
if (now.Subtract(Static.lastDomain).TotalMilliseconds < minMs && Static.lastDomainStr == e.FullPath)return;
//...you code here
/* if you need form access
this.Invoke(new MethodInvoker(() =>{ textBox1.Text = "...";}));
*/
Static.lastDomain = DateTime.Now;
Static.lastDomainStr = e.FullPath;
}
private void Second(object source, FileSystemEventArgs e)
{
//...Second rised
}
}
}
Upvotes: 0
Reputation: 49
Try this, It's working fine
private static readonly FileSystemWatcher Watcher = new FileSystemWatcher();
static void Main(string[] args)
{
Console.WriteLine("Watching....");
Watcher.Path = @"D:\Temp\Watcher";
Watcher.Changed += OnChanged;
Watcher.EnableRaisingEvents = true;
Console.ReadKey();
}
static void OnChanged(object sender, FileSystemEventArgs e)
{
try
{
Watcher.Changed -= OnChanged;
Watcher.EnableRaisingEvents = false;
Console.WriteLine($"File Changed. Name: {e.Name}");
}
catch (Exception exception)
{
Console.WriteLine(exception);
}
finally
{
Watcher.Changed += OnChanged;
Watcher.EnableRaisingEvents = true;
}
}
Upvotes: 2
Reputation: 11
In my case need to get the last line of a text file that is inserted by other application, as soon as insertion is done. Here is my solution. When the first event is raised, i disable the watcher from raising others, then i call the timer TimeElapsedEvent because when my handle function OnChanged is called i need the size of the text file, but the size at that time is not the actual size, it is the size of the file imediatelly before the insertion. So i wait for a while to proceed with the right file size.
private FileSystemWatcher watcher = new FileSystemWatcher();
...
watcher.Path = "E:\\data";
watcher.NotifyFilter = NotifyFilters.LastWrite ;
watcher.Filter = "data.txt";
watcher.Changed += new FileSystemEventHandler(OnChanged);
watcher.EnableRaisingEvents = true;
...
private void OnChanged(object source, FileSystemEventArgs e)
{
System.Timers.Timer t = new System.Timers.Timer();
try
{
watcher.Changed -= new FileSystemEventHandler(OnChanged);
watcher.EnableRaisingEvents = false;
t.Interval = 500;
t.Elapsed += (sender, args) => t_Elapsed(sender, e);
t.Start();
}
catch(Exception ex) {
;
}
}
private void t_Elapsed(object sender, FileSystemEventArgs e)
{
((System.Timers.Timer)sender).Stop();
//.. Do you stuff HERE ..
watcher.Changed += new FileSystemEventHandler(OnChanged);
watcher.EnableRaisingEvents = true;
}
Upvotes: 1
Reputation: 1
I simple add a dupe check as follows:
private void OnChanged(object source, FileSystemEventArgs e)
{
string sTabName = Path.GetFileNameWithoutExtension(e.Name);
string sLastLine = ReadLastLine(e.FullPath);
if(sLastLine != _dupeCheck)
{
TabPage tp = tcLogs.TabPages[sTabName];
TextBox tbLog = (TextBox)tp.Controls[0] as TextBox;
tbLog.Invoke(new Action(() => tbLog.AppendText(sLastLine + Environment.NewLine)));
tbLog.Invoke(new Action(() => tbLog.SelectionStart = tbLog.Text.Length));
tbLog.Invoke(new Action(() => tbLog.ScrollToCaret()));
_dupeCheck = sLastLine;
}
}
public static String ReadLastLine(string path)
{
return ReadLastLine(path, Encoding.Default, "\n");
}
public static String ReadLastLine(string path, Encoding encoding, string newline)
{
int charsize = encoding.GetByteCount("\n");
byte[] buffer = encoding.GetBytes(newline);
using (FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
long endpos = stream.Length / charsize;
for (long pos = charsize; pos < endpos; pos += charsize)
{
stream.Seek(-pos, SeekOrigin.End);
stream.Read(buffer, 0, buffer.Length);
if (encoding.GetString(buffer) == newline)
{
buffer = new byte[stream.Length - stream.Position];
stream.Read(buffer, 0, buffer.Length);
return encoding.GetString(buffer);
}
}
}
return null;
}
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int SendMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam);
private const int WM_VSCROLL = 0x115;
private const int SB_BOTTOM = 7;
/// <summary>
/// Scrolls the vertical scroll bar of a multi-line text box to the bottom.
/// </summary>
/// <param name="tb">The text box to scroll</param>
public static void ScrollToBottom(TextBox tb)
{
SendMessage(tb.Handle, WM_VSCROLL, (IntPtr)SB_BOTTOM, IntPtr.Zero);
}
Upvotes: 0
Reputation: 91
Here is a new solution you can try. Works well for me. In the event handler for the changed event programmatically remove the handler from the designer output a message if desired then programmatically add the handler back. example:
public void fileSystemWatcher1_Changed( object sender, System.IO.FileSystemEventArgs e )
{
fileSystemWatcher1.Changed -= new System.IO.FileSystemEventHandler( fileSystemWatcher1_Changed );
MessageBox.Show( "File has been uploaded to destination", "Success!" );
fileSystemWatcher1.Changed += new System.IO.FileSystemEventHandler( fileSystemWatcher1_Changed );
}
Upvotes: 3
Reputation: 418
Alot of these answers are shocking, really. Heres some code from my XanderUI Control library that fixes this.
private void OnChanged(object sender, FilesystemEventArgs e)
{
if (FSWatcher.IncludeSubdirectories == true)
{
if (File.Exists(e.FullPath)) { DO YOUR FILE CHANGE STUFF HERE... }
}
else DO YOUR DIRECTORY CHANGE STUFF HERE...
}
Upvotes: -2
Reputation: 768
Here's my approach :
// Consider having a List<String> named _changedFiles
private void OnChanged(object source, FileSystemEventArgs e)
{
lock (_changedFiles)
{
if (_changedFiles.Contains(e.FullPath))
{
return;
}
_changedFiles.Add(e.FullPath);
}
// do your stuff
System.Timers.Timer timer = new Timer(1000) { AutoReset = false };
timer.Elapsed += (timerElapsedSender, timerElapsedArgs) =>
{
lock (_changedFiles)
{
_changedFiles.Remove(e.FullPath);
}
};
timer.Start();
}
This is the solution I used to solve this issue on a project where I was sending the file as attachment in a mail. It will easily avoid the twice fired event even with a smaller timer interval but in my case 1000 was alright since I was happier with missing few changes than with flooding the mailbox with > 1 message per second. At least it works just fine in case several files are changed at the exact same time.
Another solution I've thought of would be to replace the list with a dictionary mapping files to their respective MD5, so you wouldn't have to choose an arbitrary interval since you wouldn't have to delete the entry but update its value, and cancel your stuff if it hasn't changed. It has the downside of having a Dictionary growing in memory as files are monitored and eating more and more memory, but I've read somewhere that the amount of files monitored depends on the FSW's internal buffer, so maybe not that critical. Dunno how MD5 computing time would affect your code's performances either, careful =\
Upvotes: 11
Reputation: 117
I approached the double create issue like this, which ignores the first event:
Private WithEvents fsw As New System.IO.FileSystemWatcher
Private complete As New List(Of String)
Private Sub fsw_Created(ByVal sender As Object, _
ByVal e As System.IO.FileSystemEventArgs) Handles fsw.Created
If Not complete.Contains(e.FullPath) Then
complete.Add(e.FullPath)
Else
complete.Remove(e.FullPath)
Dim th As New Threading.Thread(AddressOf hprocess)
th.Start(e)
End If
End Sub
Upvotes: 0
Reputation: 1096
Event if not asked, it is a shame there are no ready solution samples for F#. To fix this here is my recipe, just because I can and F# is a wonderful .NET language.
Duplicated events are filtered out using FSharp.Control.Reactive
package, which is just a F# wrapper for reactive extensions. All that can be targeted to full framework or netstandard2.0
:
let createWatcher path filter () =
new FileSystemWatcher(
Path = path,
Filter = filter,
EnableRaisingEvents = true,
SynchronizingObject = null // not needed for console applications
)
let createSources (fsWatcher: FileSystemWatcher) =
// use here needed events only.
// convert `Error` and `Renamed` events to be merded
[| fsWatcher.Changed :> IObservable<_>
fsWatcher.Deleted :> IObservable<_>
fsWatcher.Created :> IObservable<_>
//fsWatcher.Renamed |> Observable.map renamedToNeeded
//fsWatcher.Error |> Observable.map errorToNeeded
|] |> Observable.mergeArray
let handle (e: FileSystemEventArgs) =
printfn "handle %A event '%s' '%s' " e.ChangeType e.Name e.FullPath
let watch path filter throttleTime =
// disposes watcher if observer subscription is disposed
Observable.using (createWatcher path filter) createSources
// filter out multiple equal events
|> Observable.distinctUntilChanged
// filter out multiple Changed
|> Observable.throttle throttleTime
|> Observable.subscribe handle
[<EntryPoint>]
let main _args =
let path = @"C:\Temp\WatchDir"
let filter = "*.zip"
let throttleTime = TimeSpan.FromSeconds 10.
use _subscription = watch path filter throttleTime
System.Console.ReadKey() |> ignore
0 // return an integer exit code
Upvotes: 1
Reputation: 3955
I have created a Git repo with a class that extends FileSystemWatcher
to trigger the events only when copy is done. It discards all the changed events exept the last and it raise it only when the file become available for read.
Download FileSystemSafeWatcher and add it to your project.
Then use it as a normal FileSystemWatcher
and monitor when the events are triggered.
var fsw = new FileSystemSafeWatcher(file);
fsw.EnableRaisingEvents = true;
// Add event handlers here
fsw.Created += fsw_Created;
Upvotes: 11
Reputation: 1
I had to combine several ideas from the posts above and add file locking check to get it working for me:
FileSystemWatcher fileSystemWatcher;
private void DirectoryWatcher_Start()
{
FileSystemWatcher fileSystemWatcher = new FileSystemWatcher
{
Path = @"c:\mypath",
NotifyFilter = NotifyFilters.LastWrite,
Filter = "*.*",
EnableRaisingEvents = true
};
fileSystemWatcher.Changed += new FileSystemEventHandler(DirectoryWatcher_OnChanged);
}
private static void WaitUntilFileIsUnlocked(String fullPath, Action<String> callback, FileAccess fileAccess = FileAccess.Read, Int32 timeoutMS = 10000)
{
Int32 waitMS = 250;
Int32 currentMS = 0;
FileInfo file = new FileInfo(fullPath);
FileStream stream = null;
do
{
try
{
stream = file.Open(FileMode.Open, fileAccess, FileShare.None);
stream.Close();
callback(fullPath);
return;
}
catch (IOException)
{
}
finally
{
if (stream != null)
stream.Dispose();
}
Thread.Sleep(waitMS);
currentMS += waitMS;
} while (currentMS < timeoutMS);
}
private static Dictionary<String, DateTime> DirectoryWatcher_fileLastWriteTimeCache = new Dictionary<String, DateTime>();
private void DirectoryWatcher_OnChanged(Object source, FileSystemEventArgs ev)
{
try
{
lock (DirectoryWatcher_fileLastWriteTimeCache)
{
DateTime lastWriteTime = File.GetLastWriteTime(ev.FullPath);
if (DirectoryWatcher_fileLastWriteTimeCache.ContainsKey(ev.FullPath))
{
if (DirectoryWatcher_fileLastWriteTimeCache[ev.FullPath].AddMilliseconds(500) >= lastWriteTime)
return; // file was already handled
}
DirectoryWatcher_fileLastWriteTimeCache[ev.FullPath] = lastWriteTime;
}
Task.Run(() => WaitUntilFileIsUnlocked(ev.FullPath, fullPath =>
{
// do the job with fullPath...
}));
}
catch (Exception e)
{
// handle exception
}
}
Upvotes: 0
Reputation: 43599
I've "fixed" that problem using the following strategy in my delegate:
// fsw_ is the FileSystemWatcher instance used by my application.
private void OnDirectoryChanged(...)
{
try
{
fsw_.EnableRaisingEvents = false;
/* do my stuff once asynchronously */
}
finally
{
fsw_.EnableRaisingEvents = true;
}
}
Upvotes: 165
Reputation: 2946
mostly for future me :)
I wrote a wrapper using Rx:
public class WatcherWrapper : IDisposable
{
private readonly FileSystemWatcher _fileWatcher;
private readonly Subject<FileSystemEventArgs> _infoSubject;
private Subject<FileSystemEventArgs> _eventSubject;
public WatcherWrapper(string path, string nameFilter = "*.*", NotifyFilters? notifyFilters = null)
{
_fileWatcher = new FileSystemWatcher(path, nameFilter);
if (notifyFilters != null)
{
_fileWatcher.NotifyFilter = notifyFilters.Value;
}
_infoSubject = new Subject<FileSystemEventArgs>();
_eventSubject = new Subject<FileSystemEventArgs>();
Observable.FromEventPattern<FileSystemEventArgs>(_fileWatcher, "Changed").Select(e => e.EventArgs)
.Subscribe(_infoSubject.OnNext);
Observable.FromEventPattern<FileSystemEventArgs>(_fileWatcher, "Created").Select(e => e.EventArgs)
.Subscribe(_infoSubject.OnNext);
Observable.FromEventPattern<FileSystemEventArgs>(_fileWatcher, "Deleted").Select(e => e.EventArgs)
.Subscribe(_infoSubject.OnNext);
Observable.FromEventPattern<FileSystemEventArgs>(_fileWatcher, "Renamed").Select(e => e.EventArgs)
.Subscribe(_infoSubject.OnNext);
// this takes care of double events and still works with changing the name of the same file after a while
_infoSubject.Buffer(TimeSpan.FromMilliseconds(20))
.Select(x => x.GroupBy(z => z.FullPath).Select(z => z.LastOrDefault()).Subscribe(
infos =>
{
if (infos != null)
foreach (var info in infos)
{
{
_eventSubject.OnNext(info);
}
}
});
_fileWatcher.EnableRaisingEvents = true;
}
public IObservable<FileSystemEventArgs> FileEvents => _eventSubject;
public void Dispose()
{
_fileWatcher?.Dispose();
_eventSubject.Dispose();
_infoSubject.Dispose();
}
}
Usage:
var watcher = new WatcherWrapper(_path, "*.info");
// all more complicated and scenario specific filtering of events can be done here
watcher.FileEvents.Where(x => x.ChangeType != WatcherChangeTypes.Deleted).Subscribe(x => //do stuff)
Upvotes: 2
Reputation: 1
Try this!
string temp="";
public void Initialize()
{
FileSystemWatcher _fileWatcher = new FileSystemWatcher();
_fileWatcher.Path = "C:\\Folder";
_fileWatcher.NotifyFilter = NotifyFilters.LastWrite;
_fileWatcher.Filter = "Version.txt";
_fileWatcher.Changed += new FileSystemEventHandler(OnChanged);
_fileWatcher.EnableRaisingEvents = true;
}
private void OnChanged(object source, FileSystemEventArgs e)
{
.......
if(temp=="")
{
//do thing you want.
temp = e.name //name of text file.
}else if(temp !="" && temp != e.name)
{
//do thing you want.
temp = e.name //name of text file.
}else
{
//second fire ignored.
}
}
Upvotes: 0
Reputation: 447
This solution worked for me on production application:
Environment:
VB.Net Framework 4.5.2
Set manually object properties: NotifyFilter = Size
Then use this code:
Public Class main
Dim CalledOnce = False
Private Sub FileSystemWatcher1_Changed(sender As Object, e As IO.FileSystemEventArgs) Handles FileSystemWatcher1.Changed
If (CalledOnce = False) Then
CalledOnce = True
If (e.ChangeType = 4) Then
' Do task...
CalledOnce = False
End If
End Sub
End Sub
Upvotes: 0
Reputation: 75
Sorry for the grave dig, but I've been battling this issue for a while now and finally came up with a way to handle these multiple fired events. I would like to thank everyone in this thread as I have used it in many references when battling this issue.
Here is my complete code. It uses a dictionary to track the date and time of the last write of the file. It compares that value, and if it is the same, it suppresses the events. It then sets the value after starting the new thread.
using System.Threading; // used for backgroundworker
using System.Diagnostics; // used for file information
private static IDictionary<string, string> fileModifiedTable = new Dictionary<string, string>(); // used to keep track of our changed events
private void fswFileWatch_Changed( object sender, FileSystemEventArgs e )
{
try
{
//check if we already have this value in our dictionary.
if ( fileModifiedTable.TryGetValue( e.FullPath, out sEmpty ) )
{
//compare timestamps
if ( fileModifiedTable[ e.FullPath ] != File.GetLastWriteTime( e.FullPath ).ToString() )
{
//lock the table
lock ( fileModifiedTable )
{
//make sure our file is still valid
if ( File.Exists( e.FullPath ) )
{
// create a new background worker to do our task while the main thread stays awake. Also give it do work and work completed handlers
BackgroundWorker newThreadWork = new BackgroundWorker();
newThreadWork.DoWork += new DoWorkEventHandler( bgwNewThread_DoWork );
newThreadWork.RunWorkerCompleted += new RunWorkerCompletedEventHandler( bgwNewThread_RunWorkerCompleted );
// capture the path
string eventFilePath = e.FullPath;
List<object> arguments = new List<object>();
// add arguments to pass to the background worker
arguments.Add( eventFilePath );
arguments.Add( newEvent.File_Modified );
// start the new thread with the arguments
newThreadWork.RunWorkerAsync( arguments );
fileModifiedTable[ e.FullPath ] = File.GetLastWriteTime( e.FullPath ).ToString(); //update the modified table with the new timestamp of the file.
FILE_MODIFIED_FLAG.WaitOne(); // wait for the modified thread to complete before firing the next thread in the event multiple threads are being worked on.
}
}
}
}
}
catch ( IOException IOExcept )
{
//catch any errors
postError( IOExcept, "fswFileWatch_Changed" );
}
}
Upvotes: 1
Reputation: 3399
This code worked for me.
private void OnChanged(object source, FileSystemEventArgs e)
{
string fullFilePath = e.FullPath.ToString();
string fullURL = buildTheUrlFromStudyXML(fullFilePath);
System.Diagnostics.Process.Start("iexplore", fullURL);
Timer timer = new Timer();
((FileSystemWatcher)source).Changed -= new FileSystemEventHandler(OnChanged);
timer.Interval = 1000;
timer.Elapsed += new ElapsedEventHandler(t_Elapsed);
timer.Start();
}
private void t_Elapsed(object sender, ElapsedEventArgs e)
{
((Timer)sender).Stop();
theWatcher.Changed += new FileSystemEventHandler(OnChanged);
}
Upvotes: 2
Reputation: 1634
I know this is an old issue, but had the same problem and none of the above solution really did the trick for the problem I was facing. I have created a dictionary which maps the file name with the LastWriteTime. So if the file is not in the dictionary will go ahead with the process other wise check to see when was the last modified time and if is different from what it is in the dictionary run the code.
Dictionary<string, DateTime> dateTimeDictionary = new Dictionary<string, DateTime>();
private void OnChanged(object source, FileSystemEventArgs e)
{
if (!dateTimeDictionary.ContainsKey(e.FullPath) || (dateTimeDictionary.ContainsKey(e.FullPath) && System.IO.File.GetLastWriteTime(e.FullPath) != dateTimeDictionary[e.FullPath]))
{
dateTimeDictionary[e.FullPath] = System.IO.File.GetLastWriteTime(e.FullPath);
//your code here
}
}
Upvotes: 5
Reputation: 129
Try with this code:
class WatchPlotDirectory
{
bool let = false;
FileSystemWatcher watcher;
string path = "C:/Users/jamie/OneDrive/Pictures/Screenshots";
public WatchPlotDirectory()
{
watcher = new FileSystemWatcher();
watcher.Path = path;
watcher.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite
| NotifyFilters.FileName | NotifyFilters.DirectoryName;
watcher.Filter = "*.*";
watcher.Changed += new FileSystemEventHandler(OnChanged);
watcher.Renamed += new RenamedEventHandler(OnRenamed);
watcher.EnableRaisingEvents = true;
}
void OnChanged(object sender, FileSystemEventArgs e)
{
if (let==false) {
string mgs = string.Format("File {0} | {1}",
e.FullPath, e.ChangeType);
Console.WriteLine("onchange: " + mgs);
let = true;
}
else
{
let = false;
}
}
void OnRenamed(object sender, RenamedEventArgs e)
{
string log = string.Format("{0} | Renamed from {1}",
e.FullPath, e.OldName);
Console.WriteLine("onrenamed: " + log);
}
public void setPath(string path)
{
this.path = path;
}
}
Upvotes: 7