Reputation: 445
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); };
}
}
Upvotes: 3
Views: 2324
Reputation: 8004
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
Win32Native.MoveFile
c++ function which works great.Win32.MoveFile
as well.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...
Upvotes: 2