FileSystemWatcher Changed event is raised twice
Asked Answered
L

44

394

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.

Lundt answered 19/11, 2009 at 16:55 Comment(4)
It is a workaround, but it should be judged by the quality of the workaround. Keeping track of the changes works perfectly, and it's simple. OP is asking for a way to suppress duplicate events, and that's what the responses below give. msdn.microsoft.com/en-us/library/… Explains that the multiple events could be caused by anti-virus, or other "complicated file system stuff" (which just sounds like an excuse).Flop
I recently opended this issue github.com/Microsoft/dotnet/issues/347Thrive
I have created a class that helps you get only one event. You can get the code from github.com/melenaos/FileSystemSafeWatcherThreatt
The solution by Menelaos Vergis above, works 100%Serica
B
311

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.

Bourgogne answered 19/11, 2009 at 17:31 Comment(4)
Raymond Chen has just blogged about this: Why does saving a file in Notepad fire multiple FindFirstChangeNotification events?Kalamazoo
A decent solution: FileSystemWatcher is a Bit Broken and FileSystemWatcherMemoryCache examples by Ben Hall.Cystitis
Just tested, notepad editing results in one event, notepad++ editing in two events raised.Friesian
Cody Gray link is dead, it has moved here: Why does saving a file in Notepad fire multiple FindFirstChangeNotification events?Subcritical
D
163

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;
   }
}
Desired answered 6/5, 2010 at 11:53 Comment(10)
I tried that and it worked if I modified one file at a time but if I modified two files at a time ( like copy 1.txt and 2.txt to copy of 1.txt and copy of 2.txt ) it would only raise one event not two as expected.Captor
@ChristopherPainter Davids approach works for me if I just switch 'EnableRaisingEvents' on and off before doing my slightly time consuming read operations. You might try this as well.Schuler
It's been a couple months but I think what I ended up doing is having the event call a method that puts business logic inside of a lock statement. That way if I get extra events they queue up until it's their turn and there's nothing for them to do since the previous iteration took care of everything.Captor
This appears to fix the issue, but it does not. If another process is making changes you might lose them, the reason it appears to work is because the IO of the other process is async, and you disable monitoring till you are done your processing, thus creating a race condition with other events that might be of interest. That is why @ChristopherPainter observed his issue.Barbiebarbieri
-1: What if another change you'd be interested in happens while disabled?Maidy
@G.Stoynev You wont catch it. Depending on your application needs this might or might not be a problem. In my case, it wasn't.Desired
If /* do my stuff once */ takes more than 1 second of time, then another files are created, the watcher will lose the changed inforamtionLitigation
@cYounes : unless you do your stuff asynchronously.Desired
NOT RECOMMENDED. This is a dirty workaround. Use with caution.Ribble
It works perfectly if you watch a single file. For folders/multiple files, you should implement additional logic.Advocate
G
118

Any duplicated OnChanged events from the FileSystemWatcher can be detected and discarded by checking the File.GetLastWriteTime timestamp on the file in question. Like so:

DateTime lastRead = DateTime.MinValue;

void OnChanged(object source, FileSystemEventArgs a)
{
    DateTime lastWriteTime = File.GetLastWriteTime(uri);
    if (lastWriteTime != lastRead)
    {
        doStuff();
        lastRead = lastWriteTime;
    }
    // else discard the (duplicated) OnChanged event
}
Grandmother answered 15/6, 2010 at 6:24 Comment(10)
I like that solution, but I've used Rx to do the "right" thing(change "Rename" to the name of the event you're interested in): Observable.FromEventPattern<FileSystemEventArgs>(fileSystemWatcher, "Renamed") .Select(e => e.EventArgs) .Distinct(e => e.FullPath) .Subscribe(onNext);Ihab
Am i missing something? I don't understand how this will work. From what Ive seen the events fire simultaneously so if they both enter the above event at the same time they will both start running before lastRead is set.Vorster
As DateTime only has millisecond resolution, this method works even if you replace File.GetLastWriteTime with DateTime.Now. Depending on your situation, you may also use the a.FullName in a global variable to detect duplicate events.Ultraviolet
@PeterJamsmenson The events do not exactly fire simultaneously. For example, Notepad may generate several events when saving modifications to disk, but these events are sequentially fired, one after the other, during the several steps that Notepad needs to perform for a save. Babu's method works great.Ultraviolet
They may fire in order but they do not run synchronously. One can begin before the other is finished.Vorster
Doesn't work as the events fired are ticks apart: Last Write Time: 636076274162565607 Last Write Time: 636076274162655722Epochmaking
It's a cleaner approach imo, as it's fitting better into the logic of handling the changed event and it does not hack the watcher behavior.Hydrolysis
@Ihab In Rx there is a function called Throttle that is very usefull in this kind of scenarios.Drucilladrucy
I'd suggest using UTC where possible. lastWriteTime = File.GetLastWriteTimeUtcPeper
Doesn't work as Asheh explained. This will work: if (lastWriteTime.Ticks - lastRead.Ticks > 100000)Hitoshi
P
27

Here is my solution which helped me to stop the event being raised twice:

watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size;

Here I have set the NotifyFilter property with only Filename and size.
watcher is my object of FileSystemWatcher. Hope this will help.

Porker answered 21/11, 2011 at 8:11 Comment(5)
Also, in Notepad, I created a file with four characters: abcd in it. I then opened a new instance of Notepad and entered the same four characters. I chose File | Save As and chose the same file. The file is identical and the size and filename do not change, since the file has the same four letters, so this doesn't fire.Standing
It's possible that a genuine change could be made which doesn't alter the size of the file, therefore this technique would fail in that situation.Dory
I would guess it is a fairly common case where you know that any meaningful change will modify the file size (for example, my case was appending to a log file). While anyone who uses this solution should be aware of (and document) that assumption, this was exactly what I needed.Hileman
@GrandOpener: This is not always true. In my case I'm watching files where its content consists of just one character which is either 0 or 1.Zebrass
This will fail against small files and text files. But working great for my binary files (images, where even if one pixel changes, whole file size changes).Lilla
C
11

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 =\

Colombia answered 23/1, 2011 at 2:16 Comment(4)
Your solution works great for me. Only, you forgot to add the file to the _changedFiles List. The first part of the code should look like this: lock (_changedFiles) { if (_changedFiles.Contains(e.FullPath)) { return; } _changedFiles.Add(e.FullPath); // add this! } // do your stuffDemeanor
Your solution is not thread-safe. The _changedFiles is accessed from multiple threads. One way to fix it is to use a ConcurrentDictionary instead of List. Another way is to assign the current Form to the Timer.SynchronizingObject property, as well as to the FileSystemWatcher.SynchronizingObject property.Deprave
@TheodorZoulias did you get it working with ConcurrentDicitonary? Because for me it works fine with List but gives the issue with ConcurrentDicitonary.Avouch
@PriyankPanchal using a ConcurrentDicitonary is a bit involved because it requires to use the specialized concurrent API of this class. It might be easier to use davidthegrey's suggestion from a comment above, and just add lock (_changedFiles) before accessing the List<string>. Otherwise, if you just rely on your good luck of avoiding concurrent mutations of the non-thread-safe List<T> class, I am giving you my best wishes for eternal luck too. 🍀 😃Deprave
T
11

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;
Threatt answered 25/4, 2014 at 8:25 Comment(5)
This seems to fail when an event is raised on a directory. I got it to work by wrapping a directory check before opening the fileAmoeboid
Despite the typo in the example, this seems to be a viable solution for me. However, in my case there can be a dozen of updates within one second, so I had to lower _consolidationInterval drastically to not miss any changes. While 10 ms seems to be fine I still loose about 50% of the updates if I set _consolidationInterval to 50 ms. I still have to run some tests to find the value that fits best.Zebrass
_consolidationInterval seems to work good for me. I'd like someone to fork this and make it a NuGet package.Owens
Thanks :) It solved my issue.. Hope the created and copied events will work properly with a single watcher to solve this problem well. #55015632Interlard
Worked for my app. Thanks a bunchDallon
T
9

My scenario is that I have a virtual machine with a Linux server in it. I am developing files on the Windows host. When I change something in a folder on the host I want all the changes to be uploaded, synced onto the virtual server via Ftp. This is how I do eliminate the duplicate change event when I write to a file ( which flags the folder containing the file to be modified as well ) :

private Hashtable fileWriteTime = new Hashtable();

private void fsw_sync_Changed(object source, FileSystemEventArgs e)
{
    string path = e.FullPath.ToString();
    string currentLastWriteTime = File.GetLastWriteTime( e.FullPath ).ToString();

    // if there is no path info stored yet
    // or stored path has different time of write then the one now is inspected
    if ( !fileWriteTime.ContainsKey(path) ||
         fileWriteTime[path].ToString() != currentLastWriteTime
    )
    {
        //then we do the main thing
        log( "A CHANGE has occured with " + path );

        //lastly we update the last write time in the hashtable
        fileWriteTime[path] = currentLastWriteTime;
    }
}

Mainly I create a hashtable to store file write time information. Then if the hashtable has the filepath that is modified and it's time value is the same as the currently notified file's change then I know it is the duplicate of the event and ignore it.

Toothache answered 13/8, 2010 at 7:34 Comment(4)
I assume you empty the hashtable periodically.Yevetteyew
This would be accurate to the second but if the period between the two changes is long enough to pass a second it will fail. Moreover if you want more accuracy you could use ToString("o") but be prepared for more failures.Moffatt
Don't compare strings, use DateTime.Equals()Virtuosity
No, don't. They're not equal. In the case of my current project, they're about a millisecond apart. I use (newtime-oldtime).TotalMilliseconds < (arbitrary threshold, usually 5ms).Tenaille
J
7

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;
    }
}
Joint answered 9/6, 2016 at 12:59 Comment(1)
What semaphore? I see just a boolean variable here. Furthermore, the main issue is not solved: FileSystemEventHandler is still firing multiple events. And what effectiveness has this code? if (let==false) { ... } else { let = false; } ? Incredible how this got upvotes, this must be the a matter of StackOverflow badges only.Abcoulomb
T
5

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
                }
            }
Tolson answered 18/3, 2016 at 13:45 Comment(2)
This is a solid solution, but its missing a line of code. in the your code here section, you should add or update the dateTimeDictionary. dateTimeDictionary[e.FullPath] = System.IO.File.GetLastWriteTime(e.FullPath);Weirick
Did not work for me. My change handler is called twice and the file has a different timestamp the second time. Could be because it is a large file and the write was in progress the first time. I found a timer to collapse duplicate events worked better.Immure
I
4

One possible 'hack' would be to throttle the events using Reactive Extensions for example:

var watcher = new FileSystemWatcher("./");

Observable.FromEventPattern<FileSystemEventArgs>(watcher, "Changed")
            .Throttle(new TimeSpan(500000))
            .Subscribe(HandleChangeEvent);

watcher.EnableRaisingEvents = true;

In this case I'm throttling to 50ms, on my system that was enough, but higher values should be safer. (And like I said, it's still a 'hack').

Inguinal answered 10/2, 2014 at 6:54 Comment(2)
I've used .Distinct(e => e.FullPath) which I find way more intuitive to deal with. And you've got the behaviour restored that would be expected from the API.Ihab
The Reactive library seems like a pretty robust way to handle this problem! Hardly a hack. Here's some sample code that handles writes & renames for a directory of files. It uses GroupBy to split the Change stream into sub-streams (one per file) which are in turn Throttle'd. It seems to work very well, even against slow network transfers!Sirius
G
4

I spent some significant amount of time using the FileSystemWatcher, and some of the approaches here will not work. I really liked the disabling events approach, but unfortunately, it doesn't work if there is >1 file being dropped, second file will be missed most if not all times. So I use the following approach:

private void EventCallback(object sender, FileSystemEventArgs e)
{
    var fileName = e.FullPath;

    if (!File.Exists(fileName))
    {
        // We've dealt with the file, this is just supressing further events.
        return;
    }

    // File exists, so move it to a working directory. 
    File.Move(fileName, [working directory]);

    // Kick-off whatever processing is required.
}
Grigson answered 8/10, 2015 at 21:24 Comment(1)
Thanks, This topic has been discussed numerous times, and many workarounds have been proposed. I think yours is the correct and concise approach. Thanks, Anthony PeirisDaveen
T
3

I have a very quick and simple workaround here, it does work for me, and no matter the event would be triggered once or twice or more times occasionally, check it out:

private int fireCount = 0;
private void inputFileWatcher_Changed(object sender, FileSystemEventArgs e)
    {
       fireCount++;
       if (fireCount == 1)
        {
            MessageBox.Show("Fired only once!!");
            dowork();
        }
        else
        {
            fireCount = 0;
        }
    }
}
Thorianite answered 8/3, 2016 at 13:54 Comment(3)
At first I thought this will work for me, but it does not. I have a situation, where the files content is sometimes just overwritten and other times the file is deleted and recreated. While your solution seems to work in case the file is overwritten, it doesn't always work in case the file is recreated. In the latter case events sometimes get lost.Zebrass
Try to sort out different types of events and deal with them separately, i just offer a possible workaround. good luck.Thorianite
though not test it, i am not quite sure this doesn't work for creation and deletion. it should be theoretically applicable as well.Since the fireCount++ and if() statement are both atomic and will not be put to wait. even with two triggered events competiting with each other. i guess there must be something else cause your trouble. (by lost? what do you mean?)Thorianite
H
3

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 );
    }
Hotblooded answered 22/9, 2016 at 21:20 Comment(2)
You don't need to invoke the delegate type's constructor. this.fileSystemWatcher1.Changed -= this.fileSystemWatcher1_Changed; should do the right thing.Vitelline
@Vitelline Thanks for that. I'm not sure why I invoked the entire constructor. Honestly its most likely a newbie mistake. Regardless though it seems like my hack of a fix worked fairly well.Hotblooded
A
3

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();
    }
Apportion answered 26/6, 2019 at 20:50 Comment(1)
Very clever solution. Thank you for sharing this piece of code.Broadwater
L
2

The main reason was first event's last access time was current time(file write or changed time). then second event was file's original last access time. I solve under code.

        var lastRead = DateTime.MinValue;

        Watcher = new FileSystemWatcher(...)
        {
            NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite,
            Filter = "*.dll",
            IncludeSubdirectories = false,
        };
        Watcher.Changed += (senderObject, ea) =>
        {
            var now = DateTime.Now;
            var lastWriteTime = File.GetLastWriteTime(ea.FullPath);

            if (now == lastWriteTime)
            {
                return;
            }

            if (lastWriteTime != lastRead)
            {
                // do something...
                lastRead = lastWriteTime;
            }
        };

        Watcher.EnableRaisingEvents = true;
Loper answered 31/5, 2015 at 12:39 Comment(1)
same as this answerSyst
D
2

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);
    }
Dunne answered 14/9, 2016 at 11:59 Comment(0)
D
2

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)
Drucilladrucy answered 5/9, 2017 at 21:13 Comment(0)
I
2

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;
        }
    }
Inositol answered 7/3, 2019 at 6:18 Comment(1)
I think unsubscribing and subscribing again to Changed event is not required here. Just set EnableRaisingEvents to false on first trigger, do your processing, then set it back to true. Try catch finally is not required neither, this may fail only if Watcher is null.Broadwater
D
2

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.

Deprave answered 24/9, 2019 at 11:31 Comment(1)
I did a similar concept here https://mcmap.net/q/86662/-filesystemwatcher-changed-event-is-raised-twice. It also has a version that handles the abnormal case, although it could be normal for log files.Haystack
W
2

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
            });
Worrywart answered 5/11, 2019 at 12:17 Comment(0)
D
1

You could try to open it for write, and if successful then you could assume the other application is done with the file.

private void OnChanged(object source, FileSystemEventArgs e)
{
    try
    {
        using (var fs = File.OpenWrite(e.FullPath))
        {
        }
        //do your stuff
    }
    catch (Exception)
    {
        //no write access, other app not done
    }
}

Just opening it for write appears not to raise the changed event. So it should be safe.

Disinfectant answered 14/12, 2009 at 20:37 Comment(0)
K
1
FileReadTime = DateTime.Now;

private void File_Changed(object sender, FileSystemEventArgs e)
{            
    var lastWriteTime = File.GetLastWriteTime(e.FullPath);
    if (lastWriteTime.Subtract(FileReadTime).Ticks > 0)
    {
        // code
        FileReadTime = DateTime.Now;
    }
}
Kay answered 19/11, 2014 at 6:55 Comment(1)
While this may be the best solution to the question asked, it's always nice to add some comments as to why you chose this approach and why you think it works. :)Peluso
E
1

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" );
        }
    }
Eason answered 19/9, 2016 at 15:37 Comment(1)
Doesn't work as the events fired are ticks apart: Last Write Time: 636076274162565607 Last Write Time: 636076274162655722Menfolk
E
1

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
Eclecticism answered 16/3, 2018 at 8:40 Comment(0)
S
1

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;
}
Spaceship answered 31/1, 2019 at 17:3 Comment(0)
F
1

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}");
}
Forint answered 20/10, 2021 at 15:17 Comment(0)
H
1

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();
    }
}
Haystack answered 8/3, 2023 at 21:53 Comment(4)
Isn't it a problem that the FileSystemEventArgs argument is not propagated?Deprave
@TheodorZoulias depends on the usage. If doing so it would either be only the args of the last change seeen or a list of all the ones received.Haystack
@TheodorZoulias btw, in my use case I only cared that one or more of the files were changing and to be able to react after the changes were in place.Haystack
You could consider mentioning this limitation inside the answer, so that people know if it's suitable for their case.Deprave
C
0

I have changed the way I monitor files in directories. Instead of using the FileSystemWatcher I poll locations on another thread and then look at the LastWriteTime of the file.

DateTime lastWriteTime = File.GetLastWriteTime(someFilePath);

Using this information and keeping an index of a file path and it's latest write time I can determine files that have changed or that have been created in a particular location. This removes me from the oddities of the FileSystemWatcher. The main downside is that you need a data structure to store the LastWriteTime and the reference to the file, but it is reliable and easy to implement.

Corespondent answered 19/11, 2009 at 17:43 Comment(1)
as well as you have to burn background cycles instead of being notified by a system event.Chios
F
0

I was able to do this by added a function that checks for duplicates in an buffer array.

Then perform the action after the array has not been modified for X time using a timer: - Reset timer every time something is written to the buffer - Perform action on tick

This also catches another duplication type. If you modify a file inside a folder, the folder also throws a Change event.

Function is_duplicate(str1 As String) As Boolean
    If lb_actions_list.Items.Count = 0 Then
        Return False
    Else
        Dim compStr As String = lb_actions_list.Items(lb_actions_list.Items.Count - 1).ToString
        compStr = compStr.Substring(compStr.IndexOf("-") + 1).Trim

        If compStr <> str1 AndAlso compStr.parentDir <> str1 & "\" Then
            Return False
        Else
            Return True
        End If
    End If
End Function

Public Module extentions
<Extension()>
Public Function parentDir(ByVal aString As String) As String
    Return aString.Substring(0, CInt(InStrRev(aString, "\", aString.Length - 1)))
End Function
End Module
Fallacious answered 23/10, 2014 at 18:31 Comment(0)
C
0

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
Coign answered 25/1, 2017 at 16:57 Comment(1)
It uses same concept like @Jamie Krcmar but for VB.NETCoign
B
0

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

}
Bullen answered 12/4, 2017 at 3:40 Comment(0)
M
0

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
    }
}
Malti answered 29/12, 2017 at 6:36 Comment(0)
N
0

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
Newlywed answered 3/4, 2018 at 11:40 Comment(0)
I
0

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);
    }
Incorruption answered 18/9, 2018 at 12:58 Comment(0)
P
0

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
             }
       }
    }
Personage answered 11/3, 2019 at 11:14 Comment(0)
M
0

I used n way simpler approach.

  1. Boolean - if something is being done, true. When it ends, false.
  2. Before processing , add it to a HashSet. This way i won't be repeating elements.
  3. Every 30 minutes the elapsed event of an timer runs, and if there's no job being executed it will clear the list (just hashset = new hashset).
Mead answered 17/12, 2019 at 21:36 Comment(0)
U
0

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 :-)

Ultraviolet answered 8/2, 2021 at 17:5 Comment(5)
Doesn't your code produce a warning in the Visual Studio, about an async method that lacks the await keyword? Also I think you should mention that your solution is intended for watching a single file.Deprave
@TheodorZoulias Now that you mention it, it does. Feel free to improve my answer, but this algorithm seems to me the most pure to solve the 'changed event is raised twice' problem, it worked for me at first shot, and I don't feel that lacking the await is a problem other than hurting the feelings of VS. Please save your downvotes for answers that 'are not useful', see the mouse popup on the down arrowUltraviolet
@TheodorZoulias I did mention the single-file use case.The original question does too.Ultraviolet
Roland IMHO the mention about the single-file usage is not clear/prominent enough. As for improving other peoples answers, life is too short for that. My downvote is fair IMHO, because even if your answer shows an idea of how to solve a real-life problem, it also promotes bad coding practices. Well behaved asynchronous methods are not supposed to block the caller. They are supposed to return an incomplete Task, that the caller can wait asynchronously. You could probably fix this flaw by replacing the EventWaitHandle with a SemaphoreSlim. This has a WaitAsync method that can be awaited.Deprave
@TheodorZoulias The mention is your opinion. The first sentence discusses use cases, which other answers avoid completely. I appreciate your advice on the semaphore, I will read up on that. But even without that, I found this solution so useful and original that I like to share it. I did not experience that the caller is blocked. To make this answer more useful for others, I removed the implementation detail of Task, which is not relevant to the algorithm. Please reconsider your downvoteUltraviolet
J
0

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}");
            
        }
Jimenez answered 27/5, 2022 at 12:52 Comment(0)
E
0

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

  1. To me it works so that you can verify on another application
  2. Someone smarter, better experienced, could improve it and help me understand where it isn't robust enough
    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);
        }
    }
Eliathan answered 6/9, 2022 at 9:20 Comment(0)
F
-1

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...
}
Fleece answered 27/4, 2018 at 14:6 Comment(0)
L
-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
            }
        }  
Lusitania answered 20/11, 2019 at 6:50 Comment(0)
R
-2

if you register to the OnChanged event, then by deleting the monitored file before changing it might work, as long as you only need to monitor the OnChange event..

Raucous answered 20/6, 2010 at 13:16 Comment(1)
If I understand you correctly, you're suggesting to delete an existing file before changing it, and that should only raise 1 Changed event. Unfortunately that won't work.Hierodule
G
-6

Well, here is my solution how to raise an event only once:

FileSystemWatcheк watcher = new FileSystemWatcher();

//'path' - path to the file that has been modified.
watcher.Changed += (s, e) => FileChanged(path);

here is implementation of FileChanged

//count is our counter to triger when we can raise and when not.
private int count = 0;
private void FileChanged(string path)
{
   if (count % 2 == 0)
     {
       //code here
     }

     count ++;
}
Glynis answered 20/7, 2011 at 13:20 Comment(6)
This implementation assumes you'll always get exactly 2 notifications. That's not guaranteed any more than the original assumption of "one user action"=="one notification"Unstrung
it might be raised 3 times, in which case you'll handle it twice. If it was always raised exactly twice, they'd have fixed it so that it only raised the event once.Unstrung
Exactly, it's because Changed event raised two times by the framework, and I don't need to handle it two times each time when it is raised. And with this approach it is actually guaranty that "one user action"=="one notification" .Glynis
Hi Damien :) In my case it is raised two times. The case is, that user opens the file from our app in notepad++ and edit it. When user finished she/he save the file and than Changed event raise two times. I found in network that it is known issue, that's why I become with that solution, of course if they fix that issue I have to skip this code. But for now it is ok. What do u think ?Glynis
it's okay for the version of Notepad++ you've tested against, and the specific user scenario. The general issue is that the event will be generated an unknown number of times (variable based on the other applications involved, versions of said applications, the actions taken, etc) - you cannot, in general assume the event will be raised a fixed number of times.Unstrung
As Damien points out, this solution might work in Julian's narrow scenario, but it won't be reliable in general. For example, it will break if 2 different files are changed at the same time. (Can happen on network shares or if copying multiple files into a watched folder)Hierodule
A
-6

Make it simple define one global variable var1 = true.

Private Sub FileWatchman_Changed(ByVal sender As System.Object, ByVal e As System.IO.FileSystemEventArgs) Handles FileWatchman.Changed
   If var1 = true 
       your logic goes here
       var1 = false
   Else
       var1 = true 
   End If
End Sub
Amhara answered 22/3, 2012 at 11:28 Comment(1)
Essentially what this does is that it ignores Changed events while processing a Changed event, and only then. So it doesn't really solve the problem if your processing is fast enough and completes before the second event is raised, both events will be processed. And besides, this implementation will cause problems if 2 files are changed at the same time. (Can happen on network shares or if multiple files are copied into a watched folder)Hierodule

© 2022 - 2024 — McMap. All rights reserved.