Search code examples
c#asynchronousasync-awaittaskiprogress

Howto wrap EAP Pattern Method and IProgress with Task


I am using a bunch of synchron functions from my own "old" libary. These are used to backup files, compress them and upload them per example. For further use i would like to change these to async functions. Please forgive me my following long intro, but the problem needs a bit background..

i have found several information on how to convert this:

A great book : Concurrency in C# Cookbook from Stephen Cleary.

Here is example Pattern i am tryping to adept:

Also some posts here:

Main Points:

  • Use Async /Await all the way done
  • Don't wrap synchron Methods in asychron pattern with result or wait. Use await everywhere possible
  • Wrap EAP Pattern Methods to Tasks
  • Avoid Using Task.Run in Libaries
  • Use ConfigureAwait(False) in Libaries
  • Use Task.Run instead in your UI
  • Use IProgress to post progress

My basic class looks like this:

public class CompressItem
{
    public string ArchiveName { get; set; }
    public string Status { get; set; }
    public string StatusDetails { get; set; }
    public string SourcePath{ get; set; }
    public string ErrorText { get; set; }
    public int Percent { get; set; }
    public bool IsFinished { get; set; }
    public bool IsCancelling { get; set; }
    public MyClass()
    {
      FileName = Status = SourcePath = StatusDetails = ErrorText = "";
      Precent = 0;
      IsFinished = false;
      IsCancelling = false;
    }
}

(For the progress i now use IProgress, so i removed the old lines within this class)

This class is used in most high level function of the library an should track all different actions, per example for Compress a Directory with SevenZipSharp:

public bool CompressDirectory(CompressItem actionItem) 
{
    // Do some stuff with MyClass to get sourcePath and archiveFileName
    //
    ...
    SevenZipCompressor compressor = new SevenZipCompressor();

    // Add Event Handler
    compressor.Compressing += new EventHandler<ProgressEventArgs>((sender, args) =>
                    { CompressItem_ProgressChanged(sender, args, actionItem); });
    compressor.CompressionFinished += new EventHandler<EventArgs>((sender, args) =>
                    { CompressItem_FileCompleted(sender, args, actionItem); });
    compressor.FileCompressionStarted += new EventHandler<FileNameEventArgs>((sender, args) =>
                    { CompressItem_FileCompressionStarted(sender, args, actionItem); });
    // Start Compression
    compressor.CompressDirectory(sourcePath, archiveFileName);
   ...
   ...
}

As you can see i use the eventhandler to also send the object of my class, to be able to capture beside the progress also additional infos like action,status or status details. So now for my question:

For an async Task based approach this should be converted to a pattern like this:

    public async Task<bool> CompressDirectoryTaskAsync(CompressItem actionItem,
       IProgress<CompressItem> progress, CancellationToken cancellationToken)

This means i need to wrap the above function to this. The Eventhandler from SevenZipSharp uses EventArgs and is not descended from AsyncCompletedEventArgs. Is there a better approach?

UPDATE 2: I wrapped the Compressing Part into a Task, to be able to cancel it, if needed. The SevenZipCompressor doesn't support canceling. So normally i should avoid here within the libary a task.run, but don't know an alternative. I also did change to BeginCompressDirectoy, because it returns just after starting compressing instead of blocking the thread till done like CompressDirectory. So far the progress works, but canceling NOT. Just a little step to complete left ... hopefully you could help.

!! To test this function you only need to install the nuget package Squid-Box.SevenZipSharp !

So far i have tried to wrap the SevenZipCompressor like this:

public static Task TestCompressDirectoryTaskAsync(SevenZipCompressor compressor, 
    CompressItem actionItem, IProgress<CompressItem> progress, 
    CancellationToken cancellationToken)
     {  
          // little setup:
          // set 7z.dll path x64/x86
           string path = Path.Combine(Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory), Environment.Is64BitProcess ? "x64" : "x86", "7z.dll");
           SevenZipBase.SetLibraryPath(path);
           // for testing use this
           //SevenZipCompressor compressor = new SevenZipCompressor();
           // specifiy 7z format
           compressor.ArchiveFormat = OutArchiveFormat.SevenZip;
           // use lzma2
           compressor.CompressionMethod = CompressionMethod.Lzma2;
           compressor.CompressionMode = CompressionMode.Create;
           compressor.TempFolderPath = System.IO.Path.GetTempPath();                         
           var tcs = new TaskCompletionSource<EventArgs>();
            // Registering a lambda into the cancellationToken
            cancellationToken.Register(() =>
            {
                // We received a cancellation message, cancel the TaskCompletionSource.Task
                tcs.TrySetCanceled();
            });
            EventHandler<EventArgs> handler = null;               
            try
            { 
                var task = Task.Run(() =>
                {
                    compressor.CompressionFinished += handler = (sender, args) => { tcs.TrySetResult(args); };
                    compressor.Compressing += (sender, args) =>
                    {
                        try
                        {
                            //Check if cancellation has been requested
                            if (cancellationToken != null)
                            {
                                if (cancellationToken.IsCancellationRequested)
                                {
                                    tcs.TrySetCanceled();
                                    //throw new Exception("Cancel Requested");
                                    cancellationToken.ThrowIfCancellationRequested();
                                    //tcs.TrySetException(new Exception("Cancel Requested"));
                                }
                            }

                            //Report progress
                            if (progress != null)
                            {
                                actionItem.IsFinished = false;
                                actionItem.Status = "Compressing in Progess .."
                                actionItem.Percent = args.PercentDone;
                                progress.Report(actionItem);
                            }
                        }
                        catch (Exception e)
                        {
                            tcs.TrySetException(e);
                        }
                    };
                    compressor.BeginCompressDirectory(actionItem.SourcePath, actionItem.ArchiveName);
                    return tcs.Task;
                },cancellationToken);

                return task;
            }
                catch (Exception e)
            {
                compressor.CompressionFinished -= handler;
                tcs.TrySetException(e);
                tcs.TrySetCanceled();
                throw;
            }
        }

Solution

  • I ended up with using the follwoing solution from this post: A reusable pattern to convert event into task. The functions is now awaitbale, but not cancelbale..coudn't find a way to achieve this so far

    I use the TaskExt Class like this in my CompressDirectoryTaskAsync:

       public static Task CompressDirectoryTaskAsync(SevenZipCompressor compressor, 
        CompressItem actionItem, IProgress<CompressItem> progress, 
        CancellationToken cancellationToken)
        {
            // Do some stuff with MyClass to get sourcePath and archiveFileName
            //
            ...
    
           // Add Event Handler and Progress
           compressor.Compressing += new EventHandler<ProgressEventArgs>((sender, args) =>
           { CompressItem_ProgressChanged(sender, args, actionItem, progress); });
    
           compressor.CompressionFinished += new EventHandler<EventArgs>((sender, args) =>
           { CompressItem_FileCompleted(sender, args, actionItem, progress); });
    
           compressor.FileCompressionStarted += new EventHandler<FileNameEventArgs>((sender, args) =>
           { CompressItem_FileCompressionStarted(sender, args, actionItem, progress); });
    
    
           // Start Compression
           await TaskExt
            .FromEvent<EventArgs>()
            .WithHandlerConversion(handler => new EventHandler<EventArgs>(handler))
            .Start(
             handler => compressor.CompressionFinished += handler,
             () => compressor.BeginCompressDirectory(actionItem.SourcePath, archiveFileName),
             handler => compressor.CompressionFinished -= handler,
             cancellationToken).ConfigureAwait(false); 
           ...
           ...
        }