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,ZoomOutAsyncCenterAtAsync(4 overloads)PanByOffsetAsync,PanByDegreesAndScreenDistanceAsync,PanByDirectionAndScreenDistanceAsyncToggleMapExtentsAsync,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.Overlaysis 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.RedrawIfNotDrawnequivalent. - The map-level
OverlayDrawnevent does not fire. Subscribe to the overlay's ownDrawnevent 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. |