3

I am trying to move files from one directory to another while seeing a progress bar in my WPF application.

The move operation is insanely slow, and I cant find a solution to make it go any faster (testing the speed it was 2:30 minutes to move 38 Mb) But I have no idea how to move it efficiently. The way I am moving now works, but is horribly inefficient.

public delegate void ProgressChangeDelegate(double percentage);
    public delegate void CompleteDelegate();

    class FileMover
    { 
        public string SourceFilePath { get; set; }
        public string DestFilePath { get; set; }

        public event ProgressChangeDelegate OnProgressChanged;
        public event CompleteDelegate OnComplete;

        public FileMover(string Source, string Dest)
        {
            SourceFilePath = Source;
            DestFilePath = Dest;

            OnProgressChanged += delegate { };
            OnComplete += delegate { };
        }

        public void Copy()
        {
            byte[] buffer = new byte[1024 * 1024]; // 1MB buffer
            using (FileStream source = new FileStream(SourceFilePath, FileMode.Open, FileAccess.Read))
            {
                long fileLength = source.Length;
                using (FileStream dest = new FileStream(DestFilePath, FileMode.CreateNew, FileAccess.Write))
                {
                    long totalBytes = 0;
                    int currentBlockSize = 0;

                    while ((currentBlockSize = source.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        totalBytes += currentBlockSize;
                        double percentage = (double) totalBytes * 100.0 / fileLength;

                        dest.Write(buffer, 0, currentBlockSize);
                        OnProgressChanged(percentage);
                    }
                }
            }
            OnComplete();
        }
    }
        private async void MoveFile(string source, string outDir)
        {
            if (!string.IsNullOrEmpty(outDir) && !string.IsNullOrEmpty(source))
            {
                //InputButtonText.Text = "Please be patient while we move your file.";
                //Task.Run(() => { new FileInfo(source).MoveTo(Path.Combine(outDir, Path.GetFileName(source))); }).GetAwaiter().OnCompleted(
                //    () =>
                //    {
                //        OutputScanned.ItemsSource = null;
                //        InputButtonText.Text = "Click to select a file";
                //    });

                var mover = new FileMover(source, Path.Combine(outDir, Path.GetFileName(source)));
                await Task.Run(() => { mover.Copy(); });

                mover.OnProgressChanged += percentage =>
                {
                    MoveProgress.Value = percentage;
                    InputButtonText.Text = percentage.ToString();
                };

                mover.OnComplete += () => { File.Delete(source); };
            }
        }
  • You have a few issues it seems, what specifically are you asking here for help on? – Trevor Oct 02 '19 at 17:42
  • You need to break your problem down into separate questions. Focus on one at a time, provide good [mcve] for each so that people can help. That said, you should not be moving files by explicitly copying. Use the `System.IO.File.Move()` method instead. As long as the file is being moved within the same volume, this operation is _very_ fast and independent of the file size. If you try to move across volumes, the file will be copied instead of moved (you'll have to delete the source yourself if that's what you want), but the operation will still be done efficiently. – Peter Duniho Oct 02 '19 at 17:44
  • @PeterDuniho what do you recommend to the OP that wants to show progress of this move if they choose to use `System.IO.File.Move()`? – Trevor Oct 02 '19 at 17:46
  • @Çöđěxěŕ: for moves within the same volume, progress bar is entirely unnecessary. If they care about the cross-volume case and still want a progress bar, there are other options, including implementing the copy themselves, but first they need to settle on what problem it is they want help with first. – Peter Duniho Oct 02 '19 at 17:47
  • The exact issue I would like to get out of the way first, is the copying being rather slow while keeping a progress tracker of some kind. My existing code shows what I have tried. But anything can be changed if needed to get a better result. I will edit my post so I only ask one question. – Torben Van Assche Oct 02 '19 at 17:51
  • _"the copying being rather slow while keeping a progress tracker of some kind"_ -- if that's a single issue, then that means the copy isn't slow if there is no progress bar. But that doesn't make any sense: your 38MB file should only have (about) 38 iterations of updating the progress bar, which isn't enough to slow anything down. – Peter Duniho Oct 02 '19 at 17:54
  • The tracker is an event listener so that shouldn't impact anything. Which is making no sense to me. I'll remove it and see if that does anything. My nooby idea would be to increase the buffer size so I copy more at once but I'm not sure if that's how that works. I forgot to remove the last part as well. The tracker is a separate issue for after this one is resolved. – Torben Van Assche Oct 02 '19 at 18:13
  • There's a lot involved (from a system perspective) when moving a file. If you're using a traditional spinner drive, you may see better performance by first reading the file into a buffer, then save the buffer contents to the new file location, and finally delete the old file. – Jim Fell Oct 02 '19 at 18:49

1 Answers1

2

The move operation is insanely slow, and I cant find a solution to make it go any faster

There can be many reason's why it's taking so long to move a file. A few for example: Anti-Malware applications - may scan file, network load (if moving to another volume/drive), file size itself, and well, possible code smells.

My guess is I think you went the way you did with your code so you can handle how much has been moved so far, this is fine, but there are alternatives that can move these file's just fine and much quicker.

A few options

  1. System.IO.File.Move() method - this works well, but you don't have control on the progress either. Under the hood it actually calls out to the: Win32Native.MoveFile c++ function which works great.
  2. FileInfo.MoveTo - this just ends up delegating it's work to the Win32.MoveFile as well.
  3. Your way - Using some functions from the Kernel32.dll - this allows for complete control, well progress etc...

I will come back to these above in just a minute as I wanted to touch basis on what you originally posted about how the progress wasn't updating earlier.

This call here await Task.Run(() => { mover.Copy(); }); will be awaited until it's complete, but you register the event after this, for example: mover.OnProgressChanged += percentage => is after the Copy() call, so no, you will not get any changes.

Even if you received changes you would have an exception anyways because you are not on the UI thread, but another thread. For example:

 mover.OnProgressChanged += percentage =>
 {
    MoveProgress.Value = percentage;
    InputButtonText.Text = percentage.ToString();
 };

You are trying to update the UI (progressbar.value) from another thread, you simply can't do this. To get around that you would need to invoke from the Dispatcher. For example:

 Application.Current.Dispatcher.Invoke(() =>
 {
    pbProgress.Value = percentage;
 });

Back to the file operations

In all honesty you can still do what you want the way you are, just move a few things around and you should be good. Otherwise, below I have written a class in which you can use that will move a file, report progress etc. please see below.

Note: I tested with a 500MB file and it moved in 2.78 seconds and a 850MB file in 3.37 seconds from a local drive to a different volume.

 using System;
 using System.IO;
 using System.Runtime.InteropServices;
 using System.Threading.Tasks;
 using System.Transactions; // must add reference to System.Transactions     

public class FileHelper
    {
        #region | Public Events |

        /// <summary>
        /// Occurs when any progress changes occur with file.
        /// </summary>
        public event ProgressChangeDelegate OnProgressChanged;

        /// <summary>
        /// Occurs when file process has been completed.
        /// </summary>
        public event OnCompleteDelegate OnComplete;

        #endregion

        #region | Enums |

        [Flags]
        enum MoveFileFlags : uint
        {
        MOVE_FILE_REPLACE_EXISTSING = 0x00000001,
        MOVE_FILE_COPY_ALLOWED = 0x00000002,
        MOVE_FILE_DELAY_UNTIL_REBOOT = 0x00000004,
        MOVE_FILE_WRITE_THROUGH = 0x00000008,
        MOVE_FILE_CREATE_HARDLINK = 0x00000010,
        MOVE_FILE_FAIL_IF_NOT_TRACKABLE = 0x00000020
        }

        enum CopyProgressResult : uint
        {
        PROGRESS_CONTINUE = 0,
        PROGRESS_CANCEL = 1,
        PROGRESS_STOP = 2,
        PROGRESS_QUIET = 3,
        }

        enum CopyProgressCallbackReason : uint
        {
        CALLBACK_CHUNK_FINISHED = 0x00000000,
        CALLBACK_STREAM_SWITCH = 0x00000001
        }

        #endregion

        #region | Delegates |

        private delegate CopyProgressResult CopyProgressRoutine(
        long TotalFileSize,
        long TotalBytesTransferred,
        long StreamSize,
        long StreamBytesTransferred,
        uint dwStreamNumber,
        CopyProgressCallbackReason dwCallbackReason,
        IntPtr hSourceFile,
        IntPtr hDestinationFile,
        IntPtr lpData);

        public delegate void ProgressChangeDelegate(double percentage);

        public delegate void OnCompleteDelegate(bool completed);

        #endregion

        #region | Imports |

        [DllImport("Kernel32.dll")]
        private static extern bool CloseHandle(IntPtr handle);

        [DllImport("Kernel32.dll")]
        private static extern bool MoveFileTransactedW([MarshalAs(UnmanagedType.LPWStr)]string existingfile, [MarshalAs(UnmanagedType.LPWStr)]string newfile,
            IntPtr progress, IntPtr lpData, IntPtr flags, IntPtr transaction);

        [DllImport("Kernel32.dll")]
        private static extern bool MoveFileWithProgressA(string existingfile, string newfile,
            CopyProgressRoutine progressRoutine, IntPtr lpData, MoveFileFlags flags);

        [ComImport]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        [Guid("79427A2B-F895-40e0-BE79-B57DC82ED231")]
        private interface IKernelTransaction
        {
            void GetHandle([Out] out IntPtr handle);
        }

        #endregion

        #region | Public Routines |

        /// <summary>
        /// Will attempt to move a file using a transaction, if successful then the source file will be deleted.
        /// </summary>
        /// <param name="existingFile"></param>
        /// <param name="newFile"></param>
        /// <returns></returns>
        public static bool MoveFileTransacted(string existingFile, string newFile)
        {
            bool success = true;
            using (TransactionScope tx = new TransactionScope())
            {
                if (Transaction.Current != null)
                {
                    IKernelTransaction kt = (IKernelTransaction)TransactionInterop.GetDtcTransaction(Transaction.Current);
                    IntPtr txh;
                    kt.GetHandle(out txh);

                    if (txh == IntPtr.Zero) { success = false; return success; }

                    success = MoveFileTransactedW(existingFile, newFile, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, txh);

                    if (success)
                    {
                        tx.Complete();
                    }
                    CloseHandle(txh);
                }
                else
                {
                    try
                    {
                        File.Move(existingFile, newFile);
                        return success;
                    }
                    catch (Exception ex) { success = false; }
                }

                return success;
            }
        }

        /// <summary>
        /// Attempts to move a file from one destination to another. If it succeeds, then the source
        /// file is deleted after successful move.
        /// </summary>
        /// <param name="fileToMove"></param>
        /// <param name="newFilePath"></param>
        /// <returns></returns>
        public async Task<bool> MoveFileAsyncWithProgress(string fileToMove, string newFilePath)
        {
            bool success = false;

            try
            {
                await Task.Run(() =>
                {
                    success = MoveFileWithProgressA(fileToMove, newFilePath, new CopyProgressRoutine(CopyProgressHandler), IntPtr.Zero, MoveFileFlags .MOVE_FILE_REPLACE_EXISTSING|MoveFileFlags.MOVE_FILE_WRITE_THROUGH|MoveFileFlags.MOVE_FILE_COPY_ALLOWED);
                });
            }
            catch (Exception ex)
            {
                success = false;
            }
            finally
            {
                OnComplete(success);
            }

            return success;
        }

        private CopyProgressResult CopyProgressHandler(long total, long transferred, long streamSize, long StreamByteTrans, uint dwStreamNumber,CopyProgressCallbackReason reason, IntPtr hSourceFile, IntPtr hDestinationFile, IntPtr lpData)
        {
            double percentage = transferred * 100.0 / total;
            OnProgressChanged(percentage);

            return CopyProgressResult.PROGRESS_CONTINUE;
        }

        #endregion
    }

How to Use

One example -

 // Just a public property to hold an instance we need
 public FileHelper FileHelper { get; set; }

On load register the events...

 FileHelper = new FileHelper();
 FileHelper.OnProgressChanged += FileHelper_OnProgressChanged;
 FileHelper.OnComplete += FileHelper_OnComplete;

Here's the logic...

 private async void Button_Click(object sender, RoutedEventArgs e)
 {
    bool success = await FileHelper.MoveFileAsyncWithProgress("FILETOMOVE", "DestinationFilePath");
 }

 // This is called when progress changes, if file is small, it
 // may not even hit this.
 private void FileHelper_OnProgressChanged(double percentage)
 {
        Application.Current.Dispatcher.Invoke(() =>
        {
            pbProgress.Value = percentage;
        });
 }

 // This is called after a move, whether it succeeds or not
 private void FileHelper_OnComplete(bool completed)
 {
        Application.Current.Dispatcher.Invoke(() =>
        {
            MessageBox.Show("File process succeded: " + completed.ToString());
        });
 }

*Note: there's another function in that helper class, MoveFileTransacted, you really don't need this, It's another helper function that allows you to move a file using a transaction; if an exception occurs the file doesn't move etc...

Trevor
  • 7,777
  • 6
  • 31
  • 50
  • 1
    That is one hell of an explanation, I think I understood most of it. But damn, that's way more complex (-looking) that what I expected it to be. Thank you so much for this! I hand typed it over and made sure I understood what it said. – Torben Van Assche Oct 02 '19 at 22:44
  • 1
    Though I have to admit the DLL imports have got me seriously confused. – Torben Van Assche Oct 02 '19 at 22:50
  • I was moving a file of about 2 GB and it took roughly 25 seconds. I am very surprised as to how fast that went! Next step will be to watch for changes in the filesystem and automate the process. Will this code make an issue if there are multiple files being moved? I am expecting a significant slowdown. Bit late on the reply, went to bed since I am at work now haha – Torben Van Assche Oct 03 '19 at 06:22
  • 1
    @TorbenVanAssche I don't believe it should cause issue's if multiple files are being moved, but implementing and testing that should be minimal. All of the main work is done on a new thread, of course if you have thousands going you could potentially see some slowdowns, but more than likely wont be your case. – Trevor Oct 03 '19 at 12:47
  • 1
    Cool. Time to start working on my filesystemwatcher than. – Torben Van Assche Oct 03 '19 at 12:57