Asynchronous Directory Copy with Progress Bar

2.7k views Asked by At

I'm currently creating an application that allows me to select a directory and have it copied to upto 4 different location depending on what you've selected

Image of app

And the moment when it copies the main for locks up so I want to get the directory copies running on different threads to stop this from happening. On top of that I need to link up the progress bars for each directory copy to show progress of the transfer.

I'm a bit rusty when it comes to c# and would love it if someone could point me in the right direction. I tried using await/async but it would still just run each directory copy after each other not at the same time.

     private void button1_Click(object sender, EventArgs e)
    {
        //
        // This event handler was created by double-clicking the window in the designer.
        // It runs on the program's startup routine.
        //
        Path.GetFileName(folderBrowserDialog1.SelectedPath);
        folderBrowserDialog1.RootFolder = Environment.SpecialFolder.MyComputer;
        folderBrowserDialog1.SelectedPath = @"C:\Delivery";
        DialogResult result = folderBrowserDialog1.ShowDialog();
        if (result == DialogResult.OK)
        {
            //
            // The user selected a folder and pressed the OK button.
            // We print the number of files found.
            //
            //  string[] files = Directory.GetFiles(folderBrowserDialog1.SelectedPath);
            //  MessageBox.Show("Files found: " + files.Length.ToString(), "Message");

            if (checkBox1.Checked == true)
            {

                DirectoryCopy(folderBrowserDialog1.SelectedPath, "C:\\temp\\1\\" + Path.GetFileName(folderBrowserDialog1.SelectedPath), true);

            }
            if (checkBox2.Checked == true)
            {

                 DirectoryCopy(folderBrowserDialog1.SelectedPath, "C:\\temp\\2\\" + Path.GetFileName(folderBrowserDialog1.SelectedPath), true);
            }
            if (checkBox3.Checked == true)
            {

                 DirectoryCopy(folderBrowserDialog1.SelectedPath, "C:\\temp\\3\\" + Path.GetFileName(folderBrowserDialog1.SelectedPath), true);
            }
            if (checkBox4.Checked == true)
            {

                 DirectoryCopy(folderBrowserDialog1.SelectedPath, "C:\\temp\\4\\" + Path.GetFileName(folderBrowserDialog1.SelectedPath), true);
            }
            if (checkBox5.Checked == true)
            {

                 DirectoryCopy(folderBrowserDialog1.SelectedPath, "C:\\temp\\5\\"+ Path.GetFileName(folderBrowserDialog1.SelectedPath), true);
            }
            MessageBox.Show("These folders have been successfully transfered");
        }
    }
1

There are 1 answers

1
Harald Coppoolse On BEST ANSWER

Your question has several issues:

  • How to copy the directories using async- await
  • How to show progress in your dialog box
  • Is the chosen method efficient?

How to copy the directories using async-await

Every function that uses async-await has to return Task instead of void and Task<TResult> instead of TResult. There is one exception: the async event handler. The event handler returns void.

MSDN about Asynchronous File I/O comes with the following:

public class FileCopier
{
    public System.IO.FileInfo SourceFile {get; set;}
    public System.IO.DirectoryInfo DestinationFolder {get; set;}

    public async Task CopyAsync()
    {
        // TODO: throw exceptions if SourceFile / DestinationFile null
        // TODO: throw exception if SourceFile does not exist
        string destinationFileName = Path.Combine(DestinationFoler.Name, SourceFile.Name);
        // TODO: decide what to do if destinationFile already exists

         // open source file for reading
         using (Stream sourceStream = File.Open(SourceFile, FileMode.Open))
         {
             // create destination file write
             using (Stream destinationStream = File.Open(DestinationFile, FileMode.CreateNew))
             {
                 await CopyAsync(sourceStream, destinationStream);
             }
        }

        public async Task CopyAsync(Stream Source, Stream Destination) 
        { 
            char[] buffer = new char[0x1000]; 
            int numRead; 
            while ((numRead = await Source.ReadAsync(buffer, 0, buffer.Length)) != 0) 
            {
                await Destination.WriteAsync(buffer, 0, numRead);
            } 
        } 
    }
}

Your directory copier would could be like this:

class FolderCopier
{
    public System.IO.DirectoryInfo SourceFolder {get; set;}
    public System.IO.DirectoryInfo DestinationFolder {get; set;}

    public Task CopyAsync()
    {
        foreach (FileInfo sourceFile in SourceFolder.EnumerateFiles())
        {
            var fileCopier = new FileCopier()
            {
                Sourcefile = sourceFile,
                DestinationFolder = this.DestinationFolder,
            };
            await fileCopier.CopyAsync();
        }
    }
}

Finally your event handler:

private async void OnButtonDeploy_Clicked(object sender, ...)
{
    DirectoryInfo sourceFolder = GetSourceFolder(...);
    IEnumerable<DirectoryInfo> destinationFolders = GetDestinationFolders();
    IEnumerable<DirectoryCopier> folderCopiers = destinationFolders
        .Select(destinationFolder => new FolderCopier()
        {
            SourceFolder = sourceFolder,
            DestinationFolder = destinationFolder,
        });

    // if you want to copy the folders one after another while still keeping your UI responsive:
    foreach (var folderCopier in folderCopiers)
    {
        await folderCopier.CopyAsync();
    }

    // or if you want to start copying all:
    List<Task> folderCopyTasks = new List<Task>();
    foreach (var folderCopier in folderCopiers)
    {
        folderCopyTasks.Add(folderCopier.CopyAsync());
    }
    await Task.WhenAll(folderCopyTasks);
}

Feedback in your UI

If it is enough to update your progress bar for every copied file, consider to let the FolderCopier raise an event whenever a file is copied.

public class FileCopiedEventArgs
{
    public string DestinationFolder {get; set;}
    public int NrOfFilesCopied {get; set;}
    public int NrOfFilesToCopy {get; set;}
}

class FolderCopier
{
    public System.IO.DirectoryInfo SourceFolder {get; set;}
    public System.IO.DirectoryInfo DestinationFolder {get; set;}

    public Task CopyAsync()
    {
        List<FileInfo> filesToCopy = DestinationFolder.EnumerateFiles().ToList();

        for (int i=0; i<filesToCopy.Count; ++i)
        {
            // copy one file as mentioned before
            // notify listeners:
            this.OnFileCopied(i, filesToCopy.Count);
        }

        public event EventHandler<FileCopyiedEventArgs> EventFileCopied;
        public void OnFileCopied(int nrOfCopiedFiles, int filesToCopy)
        {
            var tmpEvent = this.EventFileCopied;
            if (tmpEvent != null)
            {
                tmpEvent.Invoke(this, new FileCopiedEventArgs()
                {
                    DestinationFolder = this.DestinationFolder.Name,
                    NrOfFilesCopied = nrOfCopiedFiles,
                    NrOfFilesToCopy = filesToCopy
                });
            }

            // instead of checking for null you can use the null-coalescent operator:
            this.EventFileCopied?.Invocke(this, new ...);
        }
    }
}

Registering to these events:

private async void OnButtonDeploy_Clicked(object sender, ...)
{
    var folderCopier = new FolderCopier(...);
    folderCopier.EventFileCopied += eventFileCopied;
}

private void EventFileCopied(object sender, ...)
{
    // for you to solve: if one copier has already copied 10%
    // and another copier only 2%, what value should the progress bar have?
}

Efficiency during file copy

You want to copy every source file to several locations. If you do this in separate processes, your source file will be read once per destination file. It seems much more efficient to read the source file once and write them to all destination files.

class MultiFileCopier
{
    public System.IO.FileInfo SourceFile {get; set;}
    public IEnumerable<System.IO.FileInfo> DestinationFiles {get; set;}

    public async Task CopyAsync(object sender, RoutedEventArgs e)
    {
        // open your source file as a streamreader
        using (Stream sourceStream = File.Open(SourceFile, Open))
        {
            // open all destination files for writing:
            IEnumerable<Stream> destinationStreams = this.OpenDestinationsForWriting();
            await CopyFilesAsync(sourceStream, destinationStreams);
            this.DisposeDestinationStreams(destinationStreams);
         }
    }
    public async Task CopyAsync(Stream Source, IEnumerable<Stream> destinations) 
        { 
            char[] buffer = new char[0x1000]; 
            int numRead; 
            while ((numRead = await Source.ReadAsync(buffer, 0, buffer.Length)) != 0) 
            {
                // write one after another:
                foreach (var destination in destinations)
                {
                    await Destination.WriteAsync(buffer, 0, numRead);
                }
                // or write to them all at once:
                List<Task> writeTasks = new List<Task>();
                foreach (var destination in destinations)
                {
                    writeTasks.Add(Destination.WriteAsync(buffer, 0, numRead));
                }
                await Task.WhenAll(writeTasks);
            } 
        } 
    }
}

In all my examples, I use pure async-await without starting a new thread. Only one thread is involved (or to be more precise: only one thread at a time). How can this make your process faster?

In this interview Eric Lippert compared async await with a team of cooks who have to prepare dinner (search somewhere in the middle of the article for async-await).

In Eric Lippert's analogy, if a cook has to wait for the bread to toast, he won't be lazily doing nothing. Instead he looks around to see if he can do something else. After a while when the bread is toasted he continues processing the toasted bread, or in a team of cooks: one of his colleagues processes the toasted bread.

The same is in async-await. Your thread can only do one thing at a time, and as long as he is busy he can't do anything else. During the directory copy there are several times your thread would be waiting, namely during reading the source file and during writing the destination file. So if you tell your thread to do something else instead of waiting, it might speed up the process.

So at first glance it seems that async-await would help you: while waiting for the first file to be written, your thread could start reading your second file. Alas, the device that writes your file will probably be the same as the one that reads your files, and thus your device would be too busy to handle your request to read the second file. So I'm not sure if your process will be much faster if you use async-await.

The same is if you really start several threads. It will only be faster if the writing is to different devices.

If you only want to keep your UI responsive consider using a BackGroundWorker class. Easier to start and stop, and easier to report progress.