diff --git a/MapControl/Avalonia/TileImageLoader.Avalonia.cs b/MapControl/Avalonia/TileImageLoader.Avalonia.cs index 8c8fb399..ed3a8fd8 100644 --- a/MapControl/Avalonia/TileImageLoader.Avalonia.cs +++ b/MapControl/Avalonia/TileImageLoader.Avalonia.cs @@ -1,17 +1,21 @@ using Avalonia.Media; using Avalonia.Threading; using System; +using System.Threading; using System.Threading.Tasks; namespace MapControl { public partial class TileImageLoader { - private static async Task LoadTileImage(Tile tile, Func> loadImageFunc) + private static async Task LoadTileImage(Tile tile, Func> loadImageFunc, CancellationToken cancellationToken) { var image = await loadImageFunc().ConfigureAwait(false); - _ = Dispatcher.UIThread.InvokeAsync(() => tile.SetImageSource(image)); // no need to await InvokeAsync + if (!cancellationToken.IsCancellationRequested) + { + _ = Dispatcher.UIThread.InvokeAsync(() => tile.SetImageSource(image)); // no need to await InvokeAsync + } } } } diff --git a/MapControl/Shared/ImageLoader.cs b/MapControl/Shared/ImageLoader.cs index c9b5445b..269c65ed 100644 --- a/MapControl/Shared/ImageLoader.cs +++ b/MapControl/Shared/ImageLoader.cs @@ -103,11 +103,11 @@ namespace MapControl if (progress != null && responseMessage.Content.Headers.ContentLength.HasValue) { - buffer = await ReadAsByteArray(responseMessage.Content, progress).ConfigureAwait(false); + buffer = await responseMessage.Content.ReadAsByteArrayAsync(progress, cancellationToken).ConfigureAwait(false); } else { - buffer = await responseMessage.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + buffer = await responseMessage.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); } response = new HttpResponse(buffer, responseMessage.Headers.CacheControl?.MaxAge); @@ -129,20 +129,25 @@ namespace MapControl return response; } + } - private static async Task ReadAsByteArray(HttpContent content, IProgress progress) + internal static class HttpContentExtensions + { + public static async Task ReadAsByteArrayAsync(this HttpContent content, IProgress progress, CancellationToken cancellationToken) { var length = (int)content.Headers.ContentLength.Value; var buffer = new byte[length]; - using (var stream = await content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var stream = await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)) { int offset = 0; int read; while (offset < length && - (read = await stream.ReadAsync(buffer, offset, length - offset).ConfigureAwait(false)) > 0) + (read = await stream.ReadAsync(buffer, offset, length - offset, cancellationToken).ConfigureAwait(false)) > 0) { + cancellationToken.ThrowIfCancellationRequested(); + offset += read; if (offset < length) // 1.0 reported by caller @@ -154,5 +159,21 @@ namespace MapControl return buffer; } + +#if !NET + public static Task ReadAsByteArrayAsync(this HttpContent content, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + return content.ReadAsByteArrayAsync(); + } + + public static Task ReadAsStreamAsync(this HttpContent content, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + return content.ReadAsStreamAsync(); + } +#endif } } \ No newline at end of file diff --git a/MapControl/Shared/TileImageLoader.cs b/MapControl/Shared/TileImageLoader.cs index 7a639f4e..8d915aa6 100644 --- a/MapControl/Shared/TileImageLoader.cs +++ b/MapControl/Shared/TileImageLoader.cs @@ -7,8 +7,8 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading.Tasks; using System.Threading; +using System.Threading.Tasks; #if WPF using System.Windows.Media; #elif UWP @@ -67,6 +67,13 @@ namespace MapControl /// public static int MaxLoadTasks { get; set; } = 4; + /// + /// Indicates whether HTTP requests are cancelled when the LoadTilesAsync method is cancelled. + /// If the property value is false, cancellation only stops dequeuing entries from the tile queue, + /// but lets currently running requests run to completion. + /// + public static bool RequestCancellationEnabled { get; set; } + private static ILogger logger; private static ILogger Logger => logger ?? (logger = ImageLoader.LoggerFactory?.CreateLogger()); @@ -82,8 +89,6 @@ namespace MapControl if (taskCount > 0) { - progress?.Report(0d); - if (Cache == null || tileSource.UriTemplate == null || !tileSource.UriTemplate.StartsWith("http")) { cacheName = null; // disable tile image caching @@ -95,27 +100,48 @@ namespace MapControl { tile.IsPending = false; - progress.Report((double)(tileCount - pendingTiles.Count) / tileCount); + progress?.Report((double)(tileCount - pendingTiles.Count) / tileCount); try { - await LoadTileImage(tile, tileSource, cacheName, cancellationToken).ConfigureAwait(false); + var requestCancellationToken = RequestCancellationEnabled ? cancellationToken : CancellationToken.None; + + await LoadTileImage(tile, tileSource, cacheName, requestCancellationToken).ConfigureAwait(false); } catch (Exception ex) { Logger?.LogError(ex, "Failed loading tile image {zoom}/{column}/{row}", tile.ZoomLevel, tile.Column, tile.Row); } + + tile.IsPending = cancellationToken.IsCancellationRequested && tile.Image == null; } } - var tasks = new Task[taskCount]; - - for (int i = 0; i < taskCount; i++) + try { - tasks[i] = Task.Run(LoadTilesFromQueueAsync, cancellationToken); + var tasks = new Task[taskCount]; + + for (int i = 0; i < taskCount; i++) + { + tasks[i] = Task.Run(LoadTilesFromQueueAsync, cancellationToken); + } + + if (!cancellationToken.IsCancellationRequested) + { + progress?.Report(0d); + + await Task.WhenAll(tasks); + } + } + catch (OperationCanceledException) + { + // no action } - await Task.WhenAll(tasks); + if (cancellationToken.IsCancellationRequested) + { + Logger?.LogTrace("Cancelled LoadTilesAsync with {count} queued tiles", pendingTiles.Count); + } } } @@ -128,7 +154,7 @@ namespace MapControl { Task LoadImage() => tileSource.LoadImageAsync(tile.Column, tile.Row, tile.ZoomLevel, cancellationToken); - await LoadTileImage(tile, LoadImage).ConfigureAwait(false); + await LoadTileImage(tile, LoadImage, cancellationToken).ConfigureAwait(false); } else { @@ -142,7 +168,7 @@ namespace MapControl { Task LoadImage() => tileSource.LoadImageAsync(buffer); - await LoadTileImage(tile, LoadImage).ConfigureAwait(false); + await LoadTileImage(tile, LoadImage, cancellationToken).ConfigureAwait(false); } } } @@ -165,9 +191,13 @@ namespace MapControl { buffer = await Cache.GetAsync(cacheKey, cancellationToken).ConfigureAwait(false); } + catch (OperationCanceledException) + { + Logger?.LogTrace("Cancelled Cache.GetAsync({cacheKey})", cacheKey); + } catch (Exception ex) { - Logger?.LogError(ex, "Cache.GetAsync: {cacheKey}", cacheKey); + Logger?.LogError(ex, "Cache.GetAsync({cacheKey})", cacheKey); } if (buffer == null) @@ -191,9 +221,13 @@ namespace MapControl await Cache.SetAsync(cacheKey, buffer, options, cancellationToken).ConfigureAwait(false); } + catch (OperationCanceledException) + { + Logger?.LogTrace("Cancelled Cache.SetAsync({cacheKey})", cacheKey); + } catch (Exception ex) { - Logger?.LogError(ex, "Cache.SetAsync: {cacheKey}", cacheKey); + Logger?.LogError(ex, "Cache.SetAsync({cacheKey})", cacheKey); } } } diff --git a/MapControl/UWP/TileImageLoader.UWP.cs b/MapControl/UWP/TileImageLoader.UWP.cs index 0f18249d..4fbe71be 100644 --- a/MapControl/UWP/TileImageLoader.UWP.cs +++ b/MapControl/UWP/TileImageLoader.UWP.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Windows.UI.Core; using Windows.UI.Xaml.Media; @@ -7,7 +8,7 @@ namespace MapControl { public partial class TileImageLoader { - private static async Task LoadTileImage(Tile tile, Func> loadImageFunc) + private static async Task LoadTileImage(Tile tile, Func> loadImageFunc, CancellationToken cancellationToken) { var tcs = new TaskCompletionSource(); @@ -19,7 +20,10 @@ namespace MapControl tcs.TrySetResult(null); // tcs.Task has completed when image is loaded - tile.SetImageSource(image); + if (!cancellationToken.IsCancellationRequested) + { + tile.SetImageSource(image); + } } catch (Exception ex) { diff --git a/MapControl/WPF/TileImageLoader.WPF.cs b/MapControl/WPF/TileImageLoader.WPF.cs index bbbbf856..b1ada9ee 100644 --- a/MapControl/WPF/TileImageLoader.WPF.cs +++ b/MapControl/WPF/TileImageLoader.WPF.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using System.Windows.Media; @@ -6,11 +7,14 @@ namespace MapControl { public partial class TileImageLoader { - private static async Task LoadTileImage(Tile tile, Func> loadImageFunc) + private static async Task LoadTileImage(Tile tile, Func> loadImageFunc, CancellationToken cancellationToken) { var image = await loadImageFunc().ConfigureAwait(false); - _ = tile.Image.Dispatcher.InvokeAsync(() => tile.SetImageSource(image)); // no need to await InvokeAsync + if (!cancellationToken.IsCancellationRequested) + { + _ = tile.Image.Dispatcher.InvokeAsync(() => tile.SetImageSource(image)); // no need to await InvokeAsync + } } } } diff --git a/MapControl/WinUI/TileImageLoader.WinUI.cs b/MapControl/WinUI/TileImageLoader.WinUI.cs index c38527cd..0ffc8158 100644 --- a/MapControl/WinUI/TileImageLoader.WinUI.cs +++ b/MapControl/WinUI/TileImageLoader.WinUI.cs @@ -1,13 +1,14 @@ using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml.Media; using System; +using System.Threading; using System.Threading.Tasks; namespace MapControl { public partial class TileImageLoader { - private static Task LoadTileImage(Tile tile, Func> loadImageFunc) + private static Task LoadTileImage(Tile tile, Func> loadImageFunc, CancellationToken cancellationToken) { var tcs = new TaskCompletionSource(); @@ -19,7 +20,10 @@ namespace MapControl tcs.TrySetResult(); // tcs.Task has completed when image is loaded - tile.SetImageSource(image); + if (!cancellationToken.IsCancellationRequested) + { + tile.SetImageSource(image); + } } catch (Exception ex) {