Skip to content

Commit

Permalink
- Fix bug where cancelling a request before the download has started …
Browse files Browse the repository at this point in the history
…was ignored.

- Made DownloadDataTaskAsync() unregister the event handler when done (to prevent memory leaks), in case these extension methods are used outside this library where the WebClient instance may be reused.
- Removed Subversion reference from solution file.
  • Loading branch information
Allon-Guralnek committed May 30, 2012
1 parent 1aeb5dd commit 8a3ce39
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 49 deletions.
64 changes: 40 additions & 24 deletions GoogleMapsApi.Test/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,7 @@ public void Geocoding_InvalidClientCredentials_Throws()
{
var request = new GeocodingRequest { Address = "285 Bedford Ave, Brooklyn, NY 11211, USA", ClientID = "gme-ThisIsAUnitTest", SigningKey = "AAECAwQFBgcICQoLDA0ODxAREhM=" };

try
{
GoogleMaps.Geocode.Query(request);
}
catch (AggregateException ex)
{
throw ex.InnerException;
}
Utils.ThrowInnerException(() => GoogleMaps.Geocode.Query(request));
}

[Test]
Expand All @@ -86,14 +79,7 @@ public void Geocoding_TimeoutTooShort_Throws()
{
var request = new GeocodingRequest { Address = "285 Bedford Ave, Brooklyn, NY 11211, USA" };

try
{
GoogleMaps.Geocode.Query(request, TimeSpan.FromMilliseconds(1));
}
catch (AggregateException ex)
{
throw ex.InnerException;
}
Utils.ThrowInnerException(() => GoogleMaps.Geocode.Query(request, TimeSpan.FromMilliseconds(1)));
}

[Test]
Expand All @@ -105,16 +91,19 @@ public void GeocodingAsync_Cancel_Throws()
var tokeSource = new CancellationTokenSource();
var task = GoogleMaps.Geocode.QueryAsync(request, tokeSource.Token);
tokeSource.Cancel();
task.ThrowInnerException();
}

try
{
task.Wait();
}
catch (AggregateException ex)
{
throw ex.InnerException;
}
[Test]
[ExpectedException(typeof(TaskCanceledException))]
public void GeocodingAsync_WithPreCanceledToken_Cancels()
{
var request = new GeocodingRequest { Address = "285 Bedford Ave, Brooklyn, NY 11211, USA" };
var cts = new CancellationTokenSource();
cts.Cancel();

var task = GoogleMaps.Geocode.QueryAsync(request, cts.Token);
task.ThrowInnerException();
}

[Test]
Expand Down Expand Up @@ -195,4 +184,31 @@ public void ElevationAsync_ReturnsCorrectElevation()
Assert.AreEqual(14.782454490661619, result.Results.First().Elevation);
}
}

static class Utils
{
public static void ThrowInnerException(this Task task)
{
try
{
task.Wait();
}
catch (AggregateException ex)
{
throw ex.Flatten().InnerException;
}
}

public static void ThrowInnerException(Action action)
{
try
{
action();
}
catch (AggregateException ex)
{
throw ex.InnerException;
}
}
}
}
4 changes: 0 additions & 4 deletions GoogleMapsApi.sln
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoogleMapsApi.Test", "GoogleMapsApi.Test\GoogleMapsApi.Test.csproj", "{AD9AD8E9-0E2C-470E-9004-373679F9D954}"
EndProject
Global
GlobalSection(SubversionScc) = preSolution
Svn-Managed = True
Manager = AnkhSVN - Subversion Support for Visual Studio
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|Mixed Platforms = Debug|Mixed Platforms
Expand Down
68 changes: 47 additions & 21 deletions GoogleMapsApi/WebClientExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,24 @@
namespace GoogleMapsApi
{
/// <summary>
/// Provides asynchronous methods based on the new Async Task pattern. Emulates the new methods added to WebClient in .NET 4.5 (and has additions).
/// Provides asynchronous methods based on the Task-based Asynchronous Pattern for the WebClient class.
/// </summary>
/// <remarks>
/// The code below uses the guidelines outlined in the MSDN article "Simplify Asynchronous Programming with Tasks"
/// at http://msdn.microsoft.com/en-us/magazine/ff959203.aspx under the "Converting an Event-Based Pattern" section.
/// at http://msdn.microsoft.com/en-us/magazine/ff959203.aspx under the "Converting an Event-Based Pattern" section,
/// and follows the general TAP guidelines found at http://www.microsoft.com/en-us/download/details.aspx?id=19957.
/// </remarks>
public static class WebClientExtensionMethods
{
private static readonly Task<byte[]> PreCancelledTask;

static WebClientExtensionMethods()
{
var tcs = new TaskCompletionSource<byte[]>();
tcs.SetCanceled();
PreCancelledTask = tcs.Task;
}

/// <summary>
/// Constant. Specified an infinite timeout duration. This is a TimeSpan of negative one (-1) milliseconds.
/// </summary>
Expand Down Expand Up @@ -46,7 +56,7 @@ public static Task<byte[]> DownloadDataTaskAsync(this WebClient client, Uri addr
/// <exception cref="ArgumentNullException">Thrown when a null value is passed to the client or address parameters.</exception>
public static Task<byte[]> DownloadDataTaskAsync(this WebClient client, Uri address, CancellationToken token)
{
return client.DownloadDataTaskAsync(address, TimeSpan.FromSeconds(Timeout.Infinite), token);
return client.DownloadDataTaskAsync(address, InfiniteTimeout, token);
}

/// <summary>
Expand All @@ -66,38 +76,54 @@ public static Task<byte[]> DownloadDataTaskAsync(this WebClient client, Uri addr
if (client == null) throw new ArgumentNullException("client");
if (address == null) throw new ArgumentNullException("address");
if (timeout.TotalMilliseconds < 0 && timeout != InfiniteTimeout)
throw new ArgumentOutOfRangeException("address", timeout, "The timeout value must be a positive value or have a TotalMilliseconds value of Timeout.Infinite.");
throw new ArgumentOutOfRangeException("address", timeout, "The timeout value must be a positive or equal to InfiniteTimeout.");

if (token.IsCancellationRequested)
return PreCancelledTask;

var tcs = new TaskCompletionSource<byte[]>();
var delayTokenSource = new CancellationTokenSource();

token.Register(() =>
{
delayTokenSource.Cancel();
client.CancelAsync();
});

if (timeout != InfiniteTimeout)
{
TaskEx.Delay(timeout, delayTokenSource.Token).ContinueWith(t =>
{
tcs.TrySetException(new TimeoutException(string.Format("The request has exceeded the timeout limit of {0} and has been aborted. The requested URI was: {1}", timeout, address)));
tcs.TrySetException(new TimeoutException(string.Format("The request has exceeded the timeout limit of {0} and has been aborted.", timeout)));
client.CancelAsync();
}, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.NotOnCanceled);
}

client.DownloadDataCompleted += (sender, args) =>

DownloadDataCompletedEventHandler completedHandler = null;
completedHandler = (sender, args) =>
{
client.DownloadDataCompleted -= completedHandler;
delayTokenSource.Cancel();

if (args.Cancelled)
tcs.TrySetCanceled();
else if (args.Error != null)
tcs.TrySetException(args.Error);
else tcs.TrySetResult(args.Result);
};

client.DownloadDataCompleted += completedHandler;

try
{
client.DownloadDataAsync(address);
}
catch
{
client.DownloadDataCompleted -= completedHandler;
throw;
}

token.Register(() =>
{
delayTokenSource.Cancel();
client.CancelAsync();
});

if (args.Cancelled)
tcs.TrySetCanceled();
else if (args.Error != null)
tcs.TrySetException(args.Error);
else tcs.TrySetResult(args.Result);
};

client.DownloadDataAsync(address);
return tcs.Task;
}
}
Expand Down

0 comments on commit 8a3ce39

Please sign in to comment.