2

I want to use the .NET-FTP Libary (http://netftp.codeplex.com). The libary offers BeginOpenRead(string,AsyncCallback,object) to download the contents using the Asynchronous Programming Model. My implementation of the Callback is basically the same as the example:

static void BeginOpenReadCallback(IAsyncResult ar) {
        FtpClient conn = ar.AsyncState as FtpClient;

        try {
            if (conn == null)
                throw new InvalidOperationException("The FtpControlConnection object is null!");

            using (Stream istream = conn.EndOpenRead(ar)) {
                byte[] buf = new byte[8192];

                try {
                    DateTime start = DateTime.Now;

                    while (istream.Read(buf, 0, buf.Length) > 0) {
                        double perc = 0;

                        if (istream.Length > 0)
                            perc = (double)istream.Position / (double)istream.Length;

                        Console.Write("\rTransferring: {0}/{1} {2}/s {3:p}         ",
                                      istream.Position.FormatBytes(),
                                      istream.Length.FormatBytes(),
                                      (istream.Position / DateTime.Now.Subtract(start).TotalSeconds).FormatBytes(),
                                      perc);
                    }
                }
                finally {
                    Console.WriteLine();
                    istream.Close();
                }
            }
        }
        catch (Exception ex) {
            Console.WriteLine(ex.ToString());
        }
        finally {
            m_reset.Set();
        }
    }

After the work of the async Method is completed, it would be great if a Completed event is fired (by the thread that started the asynchronous method in order to get no problems with the UI) to pass the results to the Main-Thread. Just like BackgroundWorker does (using RunWorkerCompleted).

How can I realize this?

0xDEADBEEF
  • 3,401
  • 8
  • 37
  • 66
  • 2
    What version of .NET are you working with? Your options vary between versions 3.5, 4, and 4.5. – Scott Chamberlain Jan 29 '14 at 21:14
  • You'll need to know what thread to invoke to, right now you have no idea and there's no place in the posted code where you could find out. BackgroundWorker does it by copying SynchronizationContext.Current in its RunWorkerAsync() method and, later, use its Post() method to invoke back. You'd have to find a similar place in this library where you can make the copy. Or just leave it up to the UI to invoke, it *never* has a problem guessing how to do it correctly. – Hans Passant Jan 29 '14 at 21:31
  • 1
    @0xDEADBEEF, if you work in VS2012+, you still can target .NET 4.0 with [Microsoft.Bcl.Async](http://www.nuget.org/packages/microsoft.bcl.async) and use the modern TPL features. – noseratio Jan 29 '14 at 22:41

2 Answers2

2

Try converting the APM pattern into the TAP pattern (more info):

static public Task<Stream> OpenReadAsync(FtpClient ftpClient, string url)
{
    return Task.Factory.FromAsync(
         (asyncCallback, state) =>
             ftpClient.BeginOpenRead(url, asyncCallback, state),
         (asyncResult) =>
             ftpClient.EndOpenRead((asyncResult));
}

Then you can use async/await and don't have to worry about synchronization context:

Stream istream = await OpenReadAsync(ftpClient, url); 

Further, you can use Stream.ReadAsync:

while (await istream.ReadAsync(buf, 0, buf.Length) > 0) 
{
    // ...
}

BackgroundWorker is superseded by Task-based API, so it may be a win-win situation (more info: Task.Run vs BackgroundWorker and here).

[UPDATE] If you work in VS2012+, you can target .NET 4.0 with Microsoft.Bcl.Async and still use the modern language and TPL features like async/await. I've been through that and I'd highly recommend it as it makes the future porting to .NET 4.5 a breeze.

Otherwise, you can use Task.ContinueWith(callback, TaskScheduler.FromCurrentSynchronizationContext()) to continue on the UI thread. Here's a related example.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
1

The simplest way is pass the SynchronizationContext in to the BeginOpenRead and use that in the callback.

private class StateHolder
{
    public StateHolder(FtpClient client, SynchronizationContext context)
    {
        Client = client;
        Context = context;

        //SynchronizationContext.Current can return null, this creates a new context that posts to the Thread Pool if called.
        if(Context == null)
            Context = new SynchronizationContext();
    }

    public FtpClient Client {get; private set;}
    public SynchronizationContext Context {get; private set;}
}

//...

ftpClient.BeginOpenRead(someString,BeginOpenReadCallback, new StateHolder(ftpClient, SynchronizationContext.Current));

Then in your callback use that state object you passed in.

void BeginOpenReadCallback(IAsyncResult ar) 
{
    StateHolder state = ar.AsyncState as StateHolder;
    FtpClient conn = state.client;

    //... Everything else the same in the function.

    //state.Context can't be null because we set it in the constructor.
    state.Context.Post(OnCompleted, conn);

}

protected virtual void OnCompleted(object state) //I use object instead of FtpClient to make the "state.Context.Post(OnCompleted, conn);" call simpler.
{
    var conn = state as FtpClient;
    var tmp = Completed; //This is the event people subscribed to.
    (tmp != null)
    {
        tmp(this, new CompletedArgs(conn)); //Assumes you followed the standard Event pattern and created the necessary classes.
    }
}
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431