Microsoft

Blog Categories

Subscribe to RSS feed

Archives

Cancelable .NET File Copy with Progress

If your application needs to move a file, one way to do this is to use the .NET Framework System.IO.File.Move method, which moves the specified file from one file system location to another. Unfortunately, this method does not permit you to cancel the operation, nor does it provide progress information. So using this method to copy a 4GB VHD, for example, would appear to freeze your application.

If you would like to provide move file functionality with the ability to cancel the operation and get progress information, you can use the Windows API MoveFileWithProgress. This function lives in Kernel32.dll and requires you use .NET Framework P/Invoke to access it. The basic idea is that you call the API specifying the source and target paths as well as a pointer to a callback function. The callback function is called by Windows every time a chunk of the file is copied, with information about the total file size, total bytes copied so far, and a cancel option.

I won’t include the full text of the class inline, but a C# source file is attached which wraps the MoveFileWithProgress Windows API within a .NET class. The class exposes a Progress event using the standard .NET Framework event pattern, so the clients of the class don’t have to worry about the Windows-specific stuff. The client simply needs to instantiate the class, register a delegate pointing to the desired progress callback function, and call the Move method.

The key part of the implementation is the MoveFileWithProgress declaration:

/// <summary>
/// The MoveFileWithProgress function moves a file or directory. MoveFileWithProgress is equivalent to the MoveFileEx function,
/// except that MoveFileWithProgress allows you to provide a callback function that receives progress notifications.
/// </summary>
/// <param name="lpExistingFileName">The existing file or directory on the local computer.</param>
/// <param name="lpNewFileName">The new name of the file or directory on the local computer. When moving a file, lpNewFileName can
/// be on a different file system or volume. If lpNewFileName is on another drive, you must set the MOVEFILE_COPY_ALLOWED flag in dwFlags.
/// When moving a directory, lpExistingFileName and lpNewFileName must be on the same drive. </param>
/// <param name="lpProgressRoutine">Pointer to a CopyProgressRoutine callback function that is called each time another portion of the file has been moved.
/// The callback function can be useful if you provide a user interface that displays the progress of the operation. This parameter can be Null.</param>
/// <param name="lpData">Argument to be passed to the CopyProgressRoutine callback function. This parameter can be Null.</param>
/// <param name="dwFlags">Move options.</param>
/// <returns></returns>
[DllImport("kernel32.dll", SetLastError=true)] private static extern bool MoveFileWithProgress(
string lpExistingFileName,
string lpNewFileName,
CopyProgressRoutine lpProgressRoutine,
IntPtr lpData,
uint dwFlags );

The progress callback function has the following signature and examines the callback reason and takes action on the CALLBACK_CHUNK_FINISHED value:

/// <summary>
/// The CopyProgressRoutine delegate is an application-defined callback function used with the CopyFileEx and MoveFileWithProgress functions.
/// It is called when a portion of a copy or move operation is completed.
/// </summary>
/// <param name="TotalFileSize">Total size of the file, in bytes.</param>
/// <param name="TotalBytesTransferred">Total number of bytes transferred from the source file to the destination file since the copy operation began.</param>
/// <param name="StreamSize">Total size of the current file stream, in bytes.</param>
/// <param name="StreamBytesTransferred">Total number of bytes in the current stream that have been transferred from the source file to the destination file since the copy operation began. </param>
/// <param name="dwStreamNumber">Handle to the current stream. The first time CopyProgressRoutine is called, the stream number is 1.</param>
/// <param name="dwCallbackReason">Reason that CopyProgressRoutine was called.</param>
/// <param name="hSourceFile">Handle to the source file.</param>
/// <param name="hDestinationFile">Handle to the destination file.</param>
/// <param name="lpData">Argument passed to CopyProgressRoutine by the CopyFileEx or MoveFileWithProgress function.</param>
/// <returns>A value indicating how to proceed with the copy operation.</returns>
protected uint CopyProgressCallback(
long TotalFileSize,
long TotalBytesTransferred,
long StreamSize,
long StreamBytesTransferred,
uint dwStreamNumber,
uint dwCallbackReason,
IntPtr hSourceFile,
IntPtr hDestinationFile,
IntPtr lpData )
{
switch ( dwCallbackReason )
{
case CALLBACK_CHUNK_FINISHED:
// Another part of the file was copied.
CopyProgressEventArgs e = new CopyProgressEventArgs( TotalFileSize, TotalBytesTransferred );
InvokeCopyProgress( e );
return e.Cancel ? PROGRESS_CANCEL : PROGRESS_CONTINUE;

case CALLBACK_STREAM_SWITCH:
// A new stream was created. We don’t care about this one – just continue the move operation.
return PROGRESS_CONTINUE;

default:
return PROGRESS_CONTINUE;
}
}

The public Move method simply delegates to the Windows API function and throws a .NET exception if the API returns a failure:

/// <summary>
/// Move a file or directory from source to destination with copy progress reports.
/// </summary>
/// <param name="sourceFile">The file or directory to be moved.</param>
/// <param name="destinationFile">The new name of the file or directory.</param>
public void Move( string sourceFile, string destinationFile )
{
bool success = MoveFileWithProgress( sourceFile, destinationFile, new CopyProgressRoutine( this.CopyProgressCallback ), IntPtr.Zero, MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH );

// Throw an exception if the Move failed.
if ( ! success )
{
int error = Marshal.GetLastWin32Error( );
throw new Win32Exception( error );
}
}

As I mentioned above, the attached class has a full implementation including definition of all the symbols required.

Leave a Reply