Search code examples
c#.netwinformsfilesystemwatcherfile-copying

How to copy a file to a new folder after folder creation only once?


private void OnCreated(object sender, FileSystemEventArgs e)
        {
            this.Invoke(new Action(
delegate ()
{
    RichTextBoxExtensions.AppendText(richTextBox1, $"Created: {e.FullPath}", Color.GreenYellow);
    RichTextBoxExtensions.AppendText(richTextBox1, $" On: {DateTime.Now}", Color.Yellow);
    richTextBox1.AppendText(Environment.NewLine);

    string folderName = savedGamesPath + "\\Save Game " + DateTime.Now.ToString("dddd, dd MMMM yyyy");
    if (!Directory.Exists(folderName))
    {
        Directory.CreateDirectory(folderName);
    }
    string destFile = Path.Combine(folderName, e.Name);


    File.Copy(e.FullPath, destFile);
}));
        }

I'm using filesystemwatcher and the problem is that the first time it's doing File.Copy it's fine but then it's doing it again and in the second time it's giving exception that it can't find the directory.

First, why it's doing it twice in a row ? and how to make that it will copy the file only once ?


Solution

  • The test code that I made to replicate your issue suggests that the second OnCreate event occurs when the Save Game directory is created. AFAIK the simplest way to detect that is to try it as a DirectoryInfo first:

    private void onCreated(object sender, FileSystemEventArgs e)
    {
        if (Directory.Exists(e.FullPath))
        {
            var fiCopy = new FileInfo(e.FullPath);
            richTextBox1.AppendText($"Created Directory: {e.FullPath}", Color.LightCoral, newLine: false);
            richTextBox1.AppendText($" On: {fiCopy.CreationTime}", Color.Yellow);
            return; // this is a directory, not a file.
        }
        Debug.Assert(string.Equals(Path.GetDirectoryName(e.FullPath), savedGamesPath), "Expecting none other");
    
        var fiSrce = new FileInfo(e.FullPath);
    
        string folderName = Path.Combine(
            savedGamesPath,
            $"Save Game {fiSrce.CreationTime.ToString("dddd, dd MMMM yyyy")}");
        // Harmless if already exists
        Directory.CreateDirectory(folderName);
    
        string destFile = Path.Combine(folderName, e.Name);
    
        File.Copy(e.FullPath, destFile);
        Debug.Assert(
            fiSrce.CreationTime.Equals(fiSrce.LastWriteTime),
            "Expecting matching CreationTime"
        );
    
        var fiDest = new FileInfo(destFile);
        Debug.Assert(
            !fiSrce.CreationTime.Equals(fiDest.CreationTime),
            "Expecting different CreationTime"
        );
        fiDest.CreationTime = fiSrce.CreationTime;
        fiDest.LastWriteTime = fiSrce.LastWriteTime;
        Debug.Assert(
            fiSrce.CreationTime.Equals(fiDest.CreationTime),
            "Expecting matching CreationTime"
        );
    
        richTextBox1.AppendText($"Created: {e.FullPath}", Color.GreenYellow, newLine: false);
        richTextBox1.AppendText($" On: {fiSrce.CreationTime}", Color.Yellow);
    }
    

    If building in NET Core 6 the Invoke won't be needed as long as the SynchronizingObject property of the FSW is set to this. In legacy NET Core 3.1 it may be necessary to Invoke or preferably BeginInvoke regardless.

    The CreationTime properties were mentioned so I went ahead and made the original and the copy have matched times. It doesn't appear that these times were at play in the behavior you're describing, however.


    MOCK

    The way you have it now, your Save Game directory is based on calendar day only and it will be created just once even of new games are created like this test run with Games 1, 2 and 3.

    screenshot

    In this sample, the FileSystemWatcher is initialized in the MainForm CTor. In Net Core 6, if the SynchronizingObject property is set to this it shouldn't be necessary to use Invoke when calling RichTextBox in the event handlers. This approach didn't seem to work for legacy .NET Core 3.1 in VS 2019. Judicious use of Invoke, or preferably BeginInvoke, may still be required.

    public MainForm()
    {
        InitializeComponent();
        savedGamesPath = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "StackOverflow",
            "filewatcher_with_copy"
        );
        // Start with clean slate for test
        Directory.Delete(savedGamesPath, recursive: true);
        // ===============================
    
        Directory.CreateDirectory(savedGamesPath);
        _fileSystemWatcher = new FileSystemWatcher()
        {
            Path = savedGamesPath,
            IncludeSubdirectories = false,
            SynchronizingObject = this, // But it seems to complain if not Invoked "anyway"
        };
        _fileSystemWatcher.Created += onCreated;
        _fileSystemWatcher.Changed += onChanged;
        _fileSystemWatcher.Deleted += onDeleted;
        _fileSystemWatcher.EnableRaisingEvents = true;
    }
    private FileSystemWatcher _fileSystemWatcher;
    private readonly string savedGamesPath;
    private int _gameCount = 0;
    private void buttonNewGame_Click(object sender, EventArgs e)
    {
        _gameCount++;
        var gamePrimary = Path.Combine(savedGamesPath, $"Game{_gameCount}.game");
        File.WriteAllText(gamePrimary, String.Empty);
    }
    

    AppendText Extension

    static class Extensions
    {
        public static void AppendText(this RichTextBox richTextBox, string text, Color color, bool newLine = true)
        {
            var colorB4 = richTextBox.SelectionColor;
            richTextBox.SelectionColor = color;
            richTextBox.AppendText(text);
            richTextBox.SelectionColor = colorB4;
            if (newLine) richTextBox.AppendText(Environment.NewLine);
        }
    }
    

    Clone for NET Core 6

    Clone for NET Core 3.1