Posted June 14, 201212 yr FPCH Admin In the blog post Diving Deep with Await and WinRT, we discussed the new async and await keywords in C# and Visual Basic and how you can use them to consume Windows Runtime (WinRT) asynchronous operations. With some assistance from the .NET Base Class Libraries (BCL), you can also use these keywords to develop asynchronous operations that are then exposed via WinRT for other components built in other languages to consume. In this post, we’ll explore how to do so. (For overall details on implementing WinRT components with C# or Visual Basic, see Creating Windows Runtime Components in C# and Visual Basic.) Let’s start by reviewing the shape of asynchronous APIs in WinRT. WinRT async interfaces WinRT has several interfaces related to asynchronous operations. The first is IAsyncInfo, which every valid WinRT async operation implements. It surfaces the common capabilities for an asynchronous operation, including the operation’s current status, the operation’s error if it failed, an ID for the operation, and the operation’s ability to have cancellation requested: public interface IAsyncInfo { AsyncStatus Status { get } Exception ErrorCode { get } uint Id { get } void Cancel() void Close() } In addition to implementing IAsyncInfo, every valid WinRT asynchronous operation implements one of four additional interfaces: IAsyncAction, IAsyncActionWithProgress, IAsyncOperation, or IAsyncOperationWithProgress. These additional interfaces allow the consumer to set a callback that will be invoked when the asynchronous work completes, and optionally allow for the consumer to get a result and/or to receive progress reports: // No results, no progress public interface IAsyncAction : IAsyncInfo { AsyncActionCompletedHandler Completed { get set } void GetResults() } // No results, with progress public interface IAsyncActionWithProgress : IAsyncInfo { AsyncActionWithProgressCompletedHandler Completed { get set } AsyncActionProgressHandler Progress { get set } void GetResults() } // With results, no progress public interface IAsyncOperation : IAsyncInfo { AsyncOperationCompletedHandler Completed { get set } TResult GetResults() } // With results, with progress public interface IAsyncOperationWithProgress : IAsyncInfo { AsyncOperationWithProgressCompletedHandler Completed { get set } AsyncOperationProgressHandler Progress { get set } TResult GetResults() } (IAsyncAction provides a GetResults method even though there are no results to be returned. This provides a method on a faulted async operation to throw its exception, rather than forcing all consumers to access the IAsyncInfo’s ErrorCode property. It is similar to how awaiter types in C# and Visual Basic expose a GetResult method, even if that GetResult method is typed to return void.) When building a WinRT library, all publicly exposed asynchronous operations in that library are strongly typed to return one of these four interfaces. In contrast, new asynchronous operations exposed from .NET libraries follow the Task-based Asynchronous Pattern (TAP), returning Task or Task, the former for operations that don’t return a result, and the latter for operations that do. Task and Task don’t implement these WinRT interfaces, nor does the Common Language Runtime (CLR) implicitly paper over the differences (as it does do for some types, such as the WinRT Windows.Foundation.Uri type and the BCL System.Uri type). Instead, we need to explicitly convert from one world to the other. In the Diving Deep with Await and WinRT post, we saw how the AsTask extension method in the BCL provides an explicit cast-like mechanism to convert from the WinRT async interfaces to .NET Tasks. The BCL also supports the other direction, with methods to convert from .NET Tasks to the WinRT async interfaces. Converting with AsAsyncAction and AsAsyncOperation For the purposes of this blog post, let’s assume we have a .NET asynchronous method DownloadStringAsyncInternal. We pass to it a URL to a web page, and the method asynchronously downloads and returns the contents of that page as a string: internal static Task DownloadStringAsyncInternal(string url) How this method is implemented doesn’t matter. Rather, our goal is to wrap this as a WinRT asynchronous operation, meaning as a method that returns one of the four previously mentioned interfaces. As our operation has a result (a string) and as it doesn’t support progress reporting, our WinRT async operation returns IAsyncOperation: public static IAsyncOperation DownloadStringAsync(string url) To implement this method, we can call the DownloadStringAsyncInternal method to get the resulting Task. Then we need to convert that task to the required IAsyncOperation… but how? public static IAsyncOperation DownloadStringAsync(string url) { Task from = DownloadStringAsyncInternal(url) IAsyncOperation to = ... // TODO: how do we convert 'from'? return to } To address this gap, the System.Runtime.WindowsRuntime.dll assembly in .NET 4.5 includes extension methods for Task and Task that provide the necessary conversions: // in System.Runtime.WindowsRuntime.dll public static class WindowsRuntimeSystemExtensions { public static IAsyncAction AsAsyncAction( this Task source) public static IAsyncOperation AsAsyncOperation( this Task source) ... } These methods return a new IAsyncAction or IAsyncOperation instance that wraps the supplied Task or Task, respectively (because Task derives from Task, both of these methods are available for Task, though it’s relatively rare that you would use AsAsyncAction with a result-returning asynchronous method). Logically, you can think of these operations as explicit, synchronous casts, or from a design pattern perspective, as adapters. They return an instance that represents the underlying task but that exposes the required surface area for WinRT. With such extension methods, we can complete our DownloadStringAsync implementation: public static IAsyncOperation DownloadStringAsync(string url) { Task from = DownloadStringAsyncInternal(url) IAsyncOperation to = from.AsAsyncOperation() return to } We can also write this more succinctly, highlighting how very cast-like the operation is: public static IAsyncOperation DownloadStringAsync(string url) { return DownloadStringAsyncInternal(url).AsAsyncOperation() } DownloadStringAsyncInternal is being invoked before we ever call AsAsyncOperation. This means that we need the synchronous call to DownloadStringAsyncInternal to return quickly to ensure that the DownloadStringAsync wrapper method is responsive. If for some reason you fear the synchronous work you’re doing will take too long, or if you explicitly want to offload the invocation to a thread pool for other reasons, you can do so using Task.Run, then invoking AsAsyncOperation on its returned task: public static IAsyncOperation DownloadStringAsync(string url) { return Task.Run(()=>DownloadStringAsyncInternal(url)).AsAsyncOperation() } More flexibility with AsyncInfo.Run These built-in AsAsyncAction and AsAsyncOperation extension methods are great for simple conversions from Task to IAsyncAction and from Task to IAsyncOperation. But what about more advanced conversions? System.Runtime.WindowsRuntime.dll contains another type that provides more flexibility: AsyncInfo, in the System.Runtime.InteropServices.WindowsRuntime namespace. AsyncInfo exposes four overloads of a static Run method, one for each of the four WinRT async interfaces: // in System.Runtime.WindowsRuntime.dll public static class AsyncInfo { // No results, no progress public static IAsyncAction Run( Func Task> taskProvider) // No results, with progress public static IAsyncActionWithProgress Run( Func IProgress, Task> taskProvider) // With results, no progress public static IAsyncOperation Run( Func Task> taskProvider) // With results, with progress public static IAsyncOperationWithProgress Run( Func IProgress, Task> taskProvider) } The AsAsyncAction and AsAsyncOperation methods we’ve already examined accept a Task as an argument. In contrast, these Run methods accept a function delegate that returns a Task, and this difference between Task and Func is enough to give us the added flexibility we need for more advanced operations. Logically, you can think of AsAsyncAction and AsAsyncOperation as being simple helpers on top of the more advanced AsyncInfo.Run: public static IAsyncAction AsAsyncAction( this Task source) { return AsyncInfo.Run(_ => source) } public static IAsyncOperation AsAsyncOperation( this Task source) { return AsyncInfo.Run(_ => source) } This isn’t exactly how they’re implemented in .NET 4.5, but functionally they behave this way, so it helps to think about them as such to contrast between the basic and advanced support. If you have a simple case, use AsAsyncAction and AsAsyncOperation, but there are several advanced cases where AsyncInfo.Run shines. Cancellation AsyncInfo.Run makes it possible to support cancellation with WinRT async methods. To continue with our downloading example, let’s say we have another DownloadStringAsyncInternal overload that accepts a CancellationToken: internal static Task DownloadStringAsyncInternal( string url, CancellationToken cancellationToken) CancellationToken is .NET Framework type that supports cooperative cancellation in a composable manner. You can pass a single token into any number of method calls, and when that token has cancellation requested (via the CancellationTokenSource that created the token), the cancellation request is then visible to all of those consuming operations. This approach differs slightly from that used by WinRT async, which is to have each individual IAsyncInfo expose its own Cancel method. Given that, how do we arrange for a call to Cancel on the IAsyncOperation to have cancellation requested on a CancellationToken that’s passed into DownloadStringAsyncInternal? AsAsyncOperation won’t work in this case: public static IAsyncOperation DownloadStringAsync(string uri) { return DownloadStringAsyncInternal(uri, … /* what goes here?? */) .AsAsyncOperation() } To know when cancellation is requested of the IAsyncOperation, that instance would need to somehow notify a listener that its Cancel method was called, for example by requesting cancellation of a CancellationToken that we’d pass into DownloadStringAsyncInternal. But in a classic “catch-22,” we don’t get the Task on which to invoke AsAsyncOperation until we’ve already invoked DownloadStringAsyncInternal, at which point we would have already needed to supply the very CancellationToken we’d have wanted AsAsyncOperation to provide. There are multiple ways to solve this conundrum, including the solution employed by AsyncInfo.Run. The Run method is responsible for constructing the IAsyncOperation, and it creates that instance to request cancellation of a CancellationToken that it also creates when the async operation’s Cancel method is called. Then when invoking the user-supplied delegate passed to Run, it passes in this token, avoiding the previously discussed cycle: public static IAsyncOperation DownloadStringAsync(string uri) { return AsyncInfo.Run(cancellationToken => DownloadStringAsyncInternal(uri, cancellationToken)) } Lambdas and Anonymous Methods AsyncInfo.Run simplifies using lambda functions and anonymous methods to implement WinRT async methods. For example, if we didn’t already have the DownloadStringAsyncInternal method, we might implement it and DownloadStringAsync like this: public static IAsyncOperation DownloadStringAsync(string uri) { return AsyncInfo.Run(delegate(CancellationToken cancellationToken) { return DownloadStringAsyncInternal(uri, cancellationToken)) }) } private static async Task DownloadStringAsyncInternal( string uri, CancellationToken cancellationToken) { var response = await new HttpClient().GetAsync( uri, cancellationToken) response.EnsureSuccessStatusCode() return await response.Content.ReadAsStringAsync() } By taking advantage of the C# and Visual Basic support for writing asynchronous anonymous methods, we can simplify our implementation by combining these two methods into one: public static IAsyncOperation DownloadStringAsync(string uri) { return AsyncInfo.Run(async delegate(CancellationToken cancellationToken) { var response = await new HttpClient().GetAsync( uri, cancellationToken) response.EnsureSuccessStatusCode() return await response.Content.ReadAsStringAsync() }) } Progress AsyncInfo.Run also provides support for progress reporting through WinRT async methods. Instead of our DownloadStringAsync method returning an IAsyncOperation, imagine if we instead wanted it to return an IAsyncOperationWithProgress: public static IAsyncOperationWithProgress DownloadStringAsync(string uri) DownloadStringAsync now can provide progress updates containing integral data, such that consumers can set a delegate as the interface’s Progress property to receive notifications of progress change. AsyncInfo.Run provides an overload that accepts a Func,Task>. Just as AsyncInfo.Run passes into the delegate a CancellationToken that will have cancellation requested when the Cancel method is called, it can also pass in an IProgress instance whose Report method triggers invocations of the consumer’s Progress delegate. For example, if we wanted to modify our previous example to report 0% progress at the beginning, 50% progress after getting the response back, and 100% progress after parsing the response into a string, that might look like this: public static IAsyncOperationWithProgress DownloadStringAsync(string uri) { return AsyncInfo.Run(async delegate( CancellationToken cancellationToken, IProgress progress) { progress.Report(0) try { var response = await new HttpClient().GetAsync(uri, cancellationToken) progress.Report(50) response.EnsureSuccessStatusCode() return await response.Content.ReadAsStringAsync() } finally { progress.Report(100) } }) } A look under the cover To get a good mental model for how all of this works, let’s explore an implementation of AsAsyncOperation and AsyncInfo.Run. These are not the same implementations that exist in .NET 4.5, and they aren’t as robust. Rather, they are approximations that provide a good sense of how things work under the cover, and are not intended to be used in production. AsAsyncOperation The AsAsyncOperation method takes a Task and returns an IAsyncOperation: public static IAsyncOperation AsAsyncOperation( this Task source) To implement this method, we need to create a type that implements IAsyncOperation and that wraps the supplied task. public static IAsyncOperation AsAsyncOperation( this Task source) { return new TaskAsAsyncOperationAdapter(source) } internal class TaskAsAsyncOperationAdapter : IAsyncOperation { private readonly Task m_task public TaskAsAsyncOperationAdapter(Task task) { m_task = task } ... } Each of the interface method implementations on this type will delegate to functionality on the wrapped task. Let’s start with the easiest members. First, the IAsyncInfo.Close method is supposed to aggressively clean up any resources that the completed asynchronous operation used. As we have no such resources (our object just wraps a task), our implementation is empty: public void Close() { /* NOP */ } With the IAsyncInfo.Cancel method a consumer of the async operation can request its cancellation. It is purely a request, and doesn’t in any way force the operation to exit. We simply use a CancellationTokenSource to store that a request occurred: private readonly CancellationTokenSource m_canceler = new CancellationTokenSource() public void Cancel() { m_canceler.Cancel() } The IAsyncInfo.Status property returns an AsyncStatus to represent the current status of the operation with regards to its asynchronous lifecycle. This can be one of four values: Started, Completed, Error, or Canceled. For the most part, we can simply delegate to the underlying Task’s Status property and map from its returned TaskStatus to the needed AsyncStatus: From TaskStatus To AsyncStatus RanToCompletion Completed Faulted Error Canceled Canceled All other values & cancellation was requested Canceled All other values & cancellation was not requested Started If the Task is not yet completed and cancellation has been requested, we need to return Canceled instead of Started. This means that although TaskStatus.Canceled is only a terminal state, AsyncStatus.Canceled can be either a terminal state or a non-terminal state, in that it’s possible for an IAsyncInfo to end in the Canceled state, or for an IAsyncInfo in the Canceled state to transition to either AsyncStatus.Completed or AsyncStatus.Error. public AsyncStatus Status { get { switch (m_task.Status) { case TaskStatus.RanToCompletion: return AsyncStatus.Completed case TaskStatus.Faulted: return AsyncStatus.Error case TaskStatus.Canceled: return AsyncStatus.Canceled default: return m_canceler.IsCancellationRequested ? AsyncStatus.Canceled : AsyncStatus.Started } } } The IAsyncInfo.Id property returns a UInt32 identifier for the operation. As Task itself already exposes such an identifier (as an Int32), we can implement this property simply by delegating through to the underlying Task property: public uint Id { get { return (uint)m_task.Id } } WinRT defines the IAsyncInfo.ErrorCode property to return an HResult. But the CLR internally maps the WinRT HResult to a .NET Exception, surfacing it that way to us through the managed projection. Task itself exposes an Exception property, so we can just delegate through to it: if the Task ended in the Faulted state, we return its first exception (a Task could potentially fault due to multiple exceptions, such as with a Task returned from Task.WhenAll), returning null otherwise: public Exception ErrorCode { get { return m_task.IsFaulted ? m_task.Exception.InnerException : null } } That’s it for implementing IAsyncInfo. Now we need to implement the two additional members supplied by IAsyncOperation: GetResults and Completed. Consumers call GetResults after the operation completes successfully or after an exception occurs. In the former case, it returns the computed result, and in the latter case, it throws the relevant exception. If the operation ended as Canceled, or if it hasn’t ended yet, it’s illegal to invoke GetResults. As such, we can implement GetResults as follows, relying on the task’s awaiter’s GetResult method to return the result if successful or to propagate the right exception if the task ended as Faulted. public TResult GetResults() { switch (m_task.Status) { case TaskStatus.RanToCompletion: case TaskStatus.Faulted: return m_task.GetAwaiter().GetResult() default: throw new InvalidOperationException("Invalid GetResults call.") } } Finally, we have the Completed property. Completed represents a delegate that should be invoked when the operation completes. If the operation has already finished when Completed is set, the supplied delegate must be invoked or scheduled immediately. Additionally, a consumer can set the property only once (attempts to set multiple times result in exceptions). And after the operation has been completed, implementations must drop the reference to the delegate to avoid memory leaks. We can rely on Task’s ContinueWith method to implement much of this behavior, but because ContinueWith can be used multiple times on the same Task instance, we need to manually implement the more restrictive “set once” behavior: private AsyncOperationCompletedHandler m_handler private int m_handlerSet public AsyncOperationCompletedHandler Completed { get { return m_handler } set { if (value == null) throw new ArgumentNullException("value") if (Interlocked.CompareExchange(ref m_handlerSet, 1, 0) != 0) throw new InvalidOperationException("Handler already set.") m_handler = value var sc = SynchronizationContext.Current m_task.ContinueWith(delegate { var handler = m_handler m_handler = null if (sc == null) handler(this, this.Status) else sc.Post(delegate { handler(this, this.Status) }, null) }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default) } } With that, our implementation of AsAsyncOperation is complete, and we can use any Task-returning method as an IAsyncOperation method. AsyncInfo.Run Now, what about the more advanced AsyncInfo.Run? public static IAsyncOperation Run( Func taskProvider) To support this Run overload, we can use the same TaskAsAsyncOperationAdapter type we just created, with all of our existing implementation intact. In fact, we just need to augment it with the ability to work in terms of Func instead of only in terms of Task. When such a delegate is provided, we can simply invoke it synchronously, passing in the m_canceler CancellationTokenSource we defined earlier and storing the returned task: internal class TaskAsAsyncOperationAdapter : IAsyncOperation { private readonly Task m_task public TaskAsAsyncOperationAdapter(Task task) { m_task = task } public TaskAsAsyncOperationAdapter( Func func) { m_task = func(m_canceler.Token) } ... } public static class AsyncInfo { public static IAsyncOperation Run( Func taskProvider) { return new TaskAsAsyncOperationAdapter(taskProvider) } … } This implementation highlights that there’s no magic happening here. AsAsyncAction, AsAsyncOperation, and AsyncInfo.Run are all just helper implementations that save you from having to write all of this boilerplate yourself. Conclusion At this point, I hope you have a good understanding of what AsAsyncAction, AsAsyncOperation, and AsyncInfo.Run do for you: they make it easy to take a Task or a Task, and expose it as an IAsyncAction, an IAsyncOperation, an IAsyncActionWithProgress, or an IAsyncOperationWithProgress. Combined with the async and await keywords in C# and Visual Basic, this makes it very easy to implement new WinRT asynchronous operations in managed code. As long as the functionality you’re exposing is available as a Task or Task, you should rely on these built-in capabilities to do the conversions for you instead of implementing the WInRT async interfaces manually. And if the functionality you’re trying to expose is not available yet as a Task or Task, try to expose it as a Task or Task first, and then rely on the built-in conversions. It can be quite difficult to get all of the semantics around a WinRT asynchronous interface implementation correct, which is why these conversions exist to do it for you. Similar support also exists if you’re implementing WinRT asynchronous operations in C++, whether through the base AsyncBase class in the Windows Runtime Library, or through the create_async function in the Parallel Pattern Library. Off Topic Forum - Unlike the Rest
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.