3

I have a legacy class which I would like to consume in a more convenient way.

class Camera 
{
     void RequestCapture();
     public delegate void ReadResultEventHandler(ControllerProxy sender, ReadResult readResult);
     public event ReadResultEventHandler OnReadResult;
}

The class manages a camera that takes pictures. It works by:

  1. Invoking the RequestCapture method
  2. Awaiting for the OnReadResult to be raised (obviously, you need to subscribe to this event in order to get the captured data)

The operation is asynchronous. The RequestCapture is a fire-and-forget operation (fast!) operation. After some seconds, the event will raise.

I would like to consume it either like as regular Task or as an IObservable<ReadResult> but I'm quite lost, since it doesn't adapt to the Async Pattern, and I have never used a class like this.

SuperJMN
  • 13,110
  • 16
  • 86
  • 185
  • 1
    Maybe have a look into this article: https://devblogs.microsoft.com/pfxteam/tasks-and-the-event-based-asynchronous-pattern/ and also this https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/interop-with-other-asynchronous-patterns-and-types#tasks-and-the-event-based-asynchronous-pattern-eap – Fildor Mar 31 '21 at 11:22

2 Answers2

3

Following the example in this article my rudimentary approach (untested!!) would be:

class AsyncCamera
{
    private readonly Camera camera;
    
    public AsyncCamera(Camera camera)
    {
        this.camera = camera ?? throw new ArgumentNullException(nameof(camera));
    } 

    public Task<ReadResult> CaptureAsync()
    {
         TaskCompletionSource<ReadResult> tcs = new TaskCompletionSource<ReadResult>();
         camera.OnReadResult += (sender, result) => {
             tcs.TrySetResult(result);
         };
         camera.RequestCapture();
         return tcs.Task;
    }
}

Possible usage:

public async Task DoSomethingAsync()
{
    var asyncCam = new AsyncCamera(new Camera());
    var readResult = await asyncCam.CaptureAsync();
    // use readResult
}

EDIT taking Stephen's comment into account (T H A N K you!)

So, this is my naive take on unsubscribing the event handler. I didn't have the time to make any tests, so suggestions for improvement are welcome.

class AsyncCamera
{
    private readonly Camera camera;
    
    public AsyncCamera(Camera camera)
    {
        this.camera = camera ?? throw new ArgumentNullException(nameof(camera));
    } 

    public Task<ReadResult> CaptureAsync()
    {
         ReadResultEventHandler handler = null;
         TaskCompletionSource<ReadResult> tcs = new TaskCompletionSource<ReadResult>();

         handler = (sender, result) => {
             camera.OnReadResult -= handler;
             tcs.TrySetResult(result);
         };

         camera.OnReadResult += handler;
         camera.RequestCapture();
         return tcs.Task;
    }
}

In case comments get "cleaned up": Stephen said

Unsubscription isn't handled in the MS examples, but it really should be. You'd have to declare a variable of type ReadResultDelegate (or whatever it's called), set it to null, and then set it to the lambda expression which can then unsubscribe that variable from the event. I don't have an example of this on my blog, but there's a general-purpose one here. Which, now that I look at it, does not seem to handle cancellation appropriately. – Stephen Cleary

Emphasis by me.

Seems to work: https://dotnetfiddle.net/9XsaUB

Fildor
  • 14,510
  • 4
  • 35
  • 67
  • Nice approach! It makes sense. The main concern I have about it is, when to unsubscribe from the OnReadResult event? – SuperJMN Mar 31 '21 at 11:46
  • 1
    Yep, good question. Haven't found an example or passage concerning exactly that, yet. Will update as soon as I do. Maybe if one of the "async-Gurus" sees this, they can help out ... – Fildor Mar 31 '21 at 11:50
  • I've found this question. Maybe this is one of the scenarios in which you oughtn't https://stackoverflow.com/questions/4172809/should-i-unsubscribe-from-events – SuperJMN Mar 31 '21 at 11:52
  • 1
    Hmm, I am not 100% convinced. But even Stephen Cleary doesn't mention an unsubscribe here: https://blog.stephencleary.com/2012/02/creating-tasks.html – Fildor Mar 31 '21 at 12:02
  • 1
    If Stephen doesn't mention it, then it's not needed :) In any case, I would like to know the exact reason. Event subscriptions and memory leaks have caused me troubles in the past. – SuperJMN Mar 31 '21 at 12:07
  • 1
    Reading the last answer from that question you linked, I am tempted to infer, that it's because it's a lambda that's registered. It only references the captured TaskCompletionSource, which should go out of scope, when the await finishes. But that's really only a suspicion. – Fildor Mar 31 '21 at 12:10
  • 3
    Unsubscription isn't handled in the MS examples, but it really should be. You'd have to declare a variable of type `ReadResultDelegate` (or whatever it's called), set it to `null`, and then set it to the lambda expression which can then unsubscribe that variable from the event. I don't have an example of this on my blog, but there's a general-purpose one [here](https://github.com/StephenCleary/AsyncEx/blob/8a73d0467d40ca41f9f9cf827c7a35702243abb8/src/Nito.AsyncEx.Tasks/Interop/EventAsyncFactory.cs#L25). Which, now that I look at it, does not seem to handle cancellation appropriately. – Stephen Cleary Mar 31 '21 at 22:14
  • 1
    @SuperJMN Updated answer. – Fildor Apr 01 '21 at 07:09
2

You could convert the Camera.OnReadResult event to an observable sequence using the FromEvent operator:

var camera = new Camera();

IObservable<ReadResult> observable = Observable
    .FromEvent<Camera.ReadResultEventHandler, ReadResult>(
        h => (sender, readResult) => h(readResult),
        h => camera.OnReadResult += h,
        h => camera.OnReadResult -= h);

Alternatively you could use the Create operator. It's more verbose, but also probably easier to comprehend.

IObservable<ReadResult> observable = Observable.Create<ReadResult>(o =>
{
    camera.OnReadResult += Camera_OnReadResult;
    void Camera_OnReadResult(ControllerProxy sender, ReadResult readResult)
    {
        o.OnNext(readResult);
    }
    return Disposable.Create(() => camera.OnReadResult -= Camera_OnReadResult);
});
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Thanks for this approach, but I'm afraid the legacy camera won't capture anything without a call to the RequestCapture method. This is maybe a problem that doesn't fit well with observables, because the camera is essentually pull based operation. – SuperJMN Mar 31 '21 at 18:39
  • 1
    @SuperJMN my answer is focused on how to consume the event. I assumed that the producing part was already solved. :-) – Theodor Zoulias Mar 31 '21 at 22:22