Skip to content

Refresh and Cancellation on the Desktop MapView

Namespace: ThinkGeo.UI.Wpf / ThinkGeo.UI.WinForms
Applies to: ThinkGeo Desktop (WPF and WinForms)


Overview

Every asynchronous method on MapView draws through one shared pipeline, and that pipeline has a single rule about cancellation. This page states the rule, says what each family of methods does when it is cancelled, and shows how to refresh a map from parts of an application that do not know about each other.

The GPU overlay, Blazor and the mobile maps use a different model and are not described here.


One map, one cancellation source

A MapView owns a single CancellationTokenSource. Every method that draws resets it on the way in, before it looks at its own arguments, and links the token you passed to the fresh one.

The consequence is worth stating plainly:

Starting any draw cancels every draw already in flight on that map, including draws of overlays the new call was never asked to touch.

This is deliberate. While a user pans or zooms, the frame that matters is the newest one, and a half-finished older frame should stop rather than spend the machine on pixels nobody will see.

MapView.CancellationTokenSource is public, so you can also cancel everything in flight yourself by calling Cancel() on it. That is the only way to stop a draw that no API handed you a token for, such as the render behind a double-click zoom.


What a cancelled call tells you

Two families, two different answers. The difference is intentional.

You cancel the token you passed A newer call supersedes this one
RefreshAsync throws OperationCanceledException completes normally, no exception
Navigation: ZoomToAsync, CenterAtAsync, PanBy… throws OperationCanceledException throws OperationCanceledException

A refresh has nowhere it meant to end up, so a superseded refresh has nothing to report; a newer draw is already on its way and will paint the same map. Surfacing an exception there would break every event handler that calls RefreshAsync without a try.

A navigation does have a destination. If it is superseded, the map ends up somewhere the caller did not ask for, and the caller is told so it can decide what to do.

Which methods are in which family

Silent when superseded:

  • RefreshAsync() and all of its overloads

Throwing when superseded:

  • ZoomToAsync (4 overloads)
  • ZoomInAsync, ZoomOutAsync
  • CenterAtAsync (4 overloads)
  • PanByOffsetAsync, PanByDegreesAndScreenDistanceAsync, PanByDirectionAndScreenDistanceAsync
  • ToggleMapExtentsAsync, ZoomToPreviousExtentAsync, ZoomToNextExtentAsync

Deprecated navigation methods scheduled for removal are not listed and should not be relied on for either behaviour.

Fire-and-forget hides the exception

The _ = MapView.ZoomToAsync(...) convention used in WPF handlers discards the task, and with it the exception a superseded navigation throws. The call fails silently and the task is never observed.

If you want to know that a navigation did not arrive, either await it or attach a continuation:

// WPF — synchronous handler, but the cancellation is still observed
MapView.ZoomToAsync(targetPoint, targetScale).ContinueWith(
    t => { /* t.IsCanceled means a newer operation took over */ },
    TaskContinuationOptions.OnlyOnCanceled);

Refreshing a single overlay: two different calls

MapView.RefreshAsync(overlay) and Overlay.RefreshAsync() both redraw the same overlay, but they are not interchangeable.

MapView.RefreshAsync(overlay) Overlay.RefreshAsync()
Resets the map-wide cancellation source yes no
Cancels other overlays that are mid-draw yes no
Opens and initializes the overlay if needed yes no
Re-applies map rotation and overlay z-order yes no
Raises MapView.OverlayDrawn / OverlaysDrawn yes no
Raises Overlay.Drawn yes yes
Honours OverlayRefreshType yes no, always a full redraw
Redraws attributions, records extent history yes no

Choose the map-level call when the refresh belongs to a change in the map as a whole, and you want the newest request to win.

Choose the overlay-level call when one part of your application owns an overlay and must redraw it without disturbing anything else on the map.

Three things to know before using Overlay.RefreshAsync():

  • The overlay must already belong to the map. Adding it to MapView.Overlays is enough. Called on an overlay the map has never initialized, it returns without doing anything and without telling you.
  • It always performs a full redraw. There is no OverlayRefreshType.RedrawIfNotDrawn equivalent.
  • The map-level OverlayDrawn event does not fire. Subscribe to the overlay's own Drawn event instead.

Two independent modules, one map

This is the shape that causes the most trouble: two parts of an application that share no code, both refreshing the same MapView at moments neither one controls. A database sync redraws the layer that just received a row while an interactive tool clears its own marker, and the two calls land microseconds apart.

Refreshed through the map, the second call cancels the first, and the first module's features do not appear until something else redraws the map.

Have each module refresh the overlay it owns:

// In the module that owns the data overlay
dataLayer.InternalFeatures.Add(feature.Id, feature);
await dataOverlay.RefreshAsync();

// In the unrelated module that owns its own overlay
await toolOverlay.RefreshAsync();

Neither module needs to know the other exists, and neither cancels the other.

If both refreshes do meet at a single place in your code, one call is better still, because the overlays are then drawn together within a single map draw:

await MapView.RefreshAsync(new[] { dataOverlay, toolOverlay });

Why a shared lock is not the answer

Routing every refresh through a SemaphoreSlim so that only one runs at a time does stop your own calls from cancelling each other, but it has two problems.

It only covers the call sites you own. Panning, zooming, double-clicking and the map's own internal redraw all reset the same cancellation source and never acquire your lock, so a user touching the map still cancels a refresh waiting behind it.

It also inverts the priority the map is built around. During navigation the newest request should win; behind a lock it waits for the oldest to finish first, which shows up as lag.


Knowing that a refresh actually drew

Overlay.Drawn and MapView.OverlayDrawn are raised only when a draw runs to completion. They are suppressed when the draw is cancelled, so they are a reliable signal that the overlay reached the screen.

There is no signal for the opposite case. A superseded RefreshAsync returns a completed task and raises nothing, so an application cannot currently be notified that its render was dropped. Prefer Overlay.RefreshAsync() over detecting the drop.


Common Pitfalls

1. Assuming an awaited RefreshAsync means the pixels landed

It means the call finished. If a newer draw superseded it, it finished without drawing anything. Use the Drawn events when you need to know a redraw reached the screen.

2. Serializing refreshes with a lock

See above. It is incomplete, because the map's own interactions never acquire your lock, and it costs latency during navigation.

3. Discarding the task of a navigation call

_ = MapView.ZoomToAsync(...) throws away the cancellation the method reports. Await it, or attach a continuation, when you need to know the navigation did not arrive.

4. Calling MapView.RefreshAsync on the TrackOverlay

The track overlay is redrawn by the interactive pipeline and when the map extent transforms. Refreshing it through the map is usually unnecessary, and because its own draw is immediate it is the cheapest possible call that can cancel an expensive one.


Reference Table

Call Superseded by a newer call Notes
MapView.RefreshAsync() completes silently Redraws every overlay.
MapView.RefreshAsync(Overlay) completes silently Map-level; cancels other overlays mid-draw.
MapView.RefreshAsync(IEnumerable<Overlay>) completes silently Draws the given overlays together in one map draw.
Overlay.RefreshAsync() not superseded by other overlays Overlay-level; never touches the map-wide source.
MapView.ZoomToAsync(…) throws OperationCanceledException Also ZoomInAsync and ZoomOutAsync.
MapView.CenterAtAsync(…) throws OperationCanceledException
MapView.PanBy…Async(…) throws OperationCanceledException
MapView.ToggleMapExtentsAsync() throws OperationCanceledException Also the previous/next extent zooms.