Caught this article on the Morning Brew today, on making web service calls in Silverlight synchronous (which is fine as long as they're not running on the UI thread). Had a quick look through the code, including the CodeProject article which provided the base, and thought it seemed a little complicated.
I've recently discovered that you can use the Reactive Framework extensions to wrap the event-based async pattern and make it synchronous; so far, I've just used it for unit testing, but I thought I'd try to apply it to this situation.
The basic pattern goes like this:
- Use Observable.FromEvent to get an IObservable from the XyzCompleted event.
- Use the ToEnumerable extension method to convert the asynchronous IObservable to a blocking IEnumerable.
- Call the XyzAsync method.
- Get the First element off the IEnumerable.
You see, when you create an IEnumerable from an IObservable, and then start to iterate it, it creates an enumerator which blocks on the current thread until it gets an OnNext from the Observable. (Disclaimer: I don't know for sure how it actually does that, I just know that's how it works on the outside. If I was implementing it, I'd probably be using some sort of queuing and polling algorithm.)
WebClient.DownloadString(Uri uri)
To start off with, I added an extension method to WebClient which implemented the pattern around DownloadStringAsync/DownloadStringCompleted:
public static string DownloadString(this WebClient webClient, Uri address)
{
// Convert event to blocking IEnumerable
var blocker = Observable.FromEvent<DownloadStringCompletedEventArgs>
(webClient, "DownloadStringCompleted")
.SubscribeOnDispatcher()
.ToEnumerable();
// Call the Async method
webClient.DownloadStringAsync(address);
// Get the first element off the blocking Enumerable
var eventArgs = blocker.First().EventArgs;
// Throw exception?
if (eventArgs.Error != null)
{
throw eventArgs.Error;
}
return eventArgs.Result;
}
That's pretty straightforward.
Genericising the pattern
In fact, looked at through functional goggles, there are only four variables:
- the object with the Async method on it;
- the name of the Completed event;
- the Async method being invoked;
- the extraction of the actual result from the EventArgs.
So we should be able to wrap this pattern up in a helper method with four parameters. And indeed we can:
public static TResult Execute<TEventArgs, TResult>(object obj,
string eventName,
Action<object> asyncMethodInvoker,
Func<TEventArgs, TResult> resultSelector)
where TEventArgs : AsyncCompletedEventArgs
{
// Convert event to blocking IEnumerable
var blocker = Observable.FromEvent<TEventArgs>(obj, eventName)
.SubscribeOnDispatcher()
.ToEnumerable();
// Use Guid to identify our call
var guid = Guid.NewGuid();
// Call the Async method
asyncMethodInvoker(guid);
// Get our element off the blocking Enumerable
var eventArgs = blocker
.First(e => e.EventArgs.UserState.Equals(guid))
.EventArgs;
// Throw exception?
if (eventArgs.Error != null)
{
throw eventArgs.Error;
}
// Return result from EventArgs
return resultSelector(eventArgs);
}
Woo. OK, we've got two generic parameters, one for the type of EventArgs involved, and one for the final return type. We don't need to know the type of the object which holds the Async method, because we're only using it for the Rx FromEvent call, and that just takes object.
We've wrapped up the actual invocation of the Async method in an Action delegate which we can just call; the consumer of this method can include any parameters, such as Uri, in the Action body as closures. But we've added a parameter to the delegate, just an object, for which we're going to supply a Guid to identify the Completed event.
By adding the generic constraint to say we only deal with AsyncCompletedEventArgs and its derivatives, we can handle the exception cases because we know we've got the Error property available to us.
And we can call the supplied Func delegate which takes whatever the actual completed EventArgs is and returns the Result bit of it.
So, you can call this helper method from anywhere to dynamically make any Async Event Pattern synchronous, but it's a bit messy, so I think the better approach is to use it inside an extension method like the WebClient.DownloadString method above. Here's that method reimplemented using the helper method:
public static string DownloadString(this WebClient webClient, Uri address)
{
return Sync.Execute
<DownloadStringCompletedEventArgs, string>
(webClient, "DownloadStringCompleted",
(o) => webClient.DownloadStringAsync(address, o),
(e) => e.Result);
}
Caveat lector: I've only hacked this together quickly and run a couple of tests, so if you try it and find problems, please let me know.