ProConcepts Asynchronous Programming in ArcGIS Pro - kataya/arcgis-pro-sdk GitHub Wiki
ArcGIS Pro is an asynchronous application. Developers writing add-ins must be prepared to write asynchronous code. This concept document covers various of the asynchronous programming techniques that can be employed in your add-ins. This concept document compliments the information found in ProConcepts Framework working with multithreading in arcgis pro. Developers should familiarize themselves with the content in "ProConcepts Framework working with multithreading in arcgis pro" first.
This concept document is not, however, a multi-threaded or asynchronous programming "intro" or tutorial. It is focused on the Pro APIs and their use in add-ins. For general information on asynchronous programming and related topics, add-in developers are encouraged to read some of the many tutorials and primers on the subject available online.
Language: C#
Subject: Framework
Contributor: ArcGIS Pro SDK Team <[email protected]>
Organization: Esri, http://www.esri.com
Date: 6/18/2020
ArcGIS Pro: 2.7
Visual Studio: 2017, 2019
In this topic
- Overview
- Threads in ArcGIS Pro
- API Characteristics
- Using Asynchronous Methods
- Synchronous methods
- Developing Custom Methods
- Using BackgroundTask
ArcGIS Pro is an asynchronous application. ArcGIS Pro leverages asynchronicity for many reasons but, for add-in developers, the primary purpose of the asynchronous API is to allow add-ins to do "work" whilst allowing the Pro UI to remain responsive. "Responsive" , in this context, means allowing the UI to draw content and refresh its UI while processing is going on in the background. Contrast this with the Arcmap experience where the UI "freezes" and can become non-responsive during long running operations. The primary purpose of the asynchronous API is not really to provide parallelism or concurrency* to add-in developers. As we will see, use of ArcGIS Pro's primary worker thread, via the API, prevents concurrency by queuing all submitted background operations (whether from add-ins or Pro code) so that they run in sequential order. More on this in the following sections.
*Concurrent background operations can now be supported at 2.6 with BackgroundTask
Generally speaking, add-in developers in Pro only need to contend with two threads: the user interface thread and a specialized worker thread provided by the application accessed via "QueuedTask". At 2.6, however, access to a third background thread (actually a background thread pool) has been introduced for use by add-in developers called BackgroundTask. QueuedTask and BackgroundTask both use Microsoft’s .NET Task Parallel Library TPL and the associated programming pattern known as the Task Asynchronous Pattern TAP. Even though QueuedTask and BackgroundTask are modeled after the System.Threading.Tasks.Task class, they have some important differences described below:
- QueuedTask and BackgroundTask make use of single-threaded apartments which are compatible for use with COM components and components that have thread affinity*.
- QueuedTask and BackgroundTask execute operations on Pro's managed thread pools, not on the Microsoft Task thread pool.
- QueuedTask operations are queued and performed in a FIFO sequence to prevent logical conflicts. This ensures the proper ordering of calls, reduces the risk of conflicts, and allows the maintenance of a consistent application state. As separate operations on the QueuedTask do not execute in parallel, it lowers the difficulty of authoring Tasks as compared to using the Microsoft
System.Threading.Tasks.Task
(and, Pro's BackgroundTask).
*BackgroundTask is not really intended for use with objects that have thread affinity however, thread affinity can be provided using a TaskPriority of type
TaskPriority.single
.
The QueuedTask class is the de-facto primary mechanism for add-in developers to use to run their add-in code asynchronously. QueuedTask runs all its operations on the primary worker thread for the Pro application, sometimes referred to as the Main CIM Thread, or "MCT".
The majority of the Pro application state is maintained by what is called the CIM or Cartographic Information Model. The CIM consists of numerous models which describe the complete state of the opened project including its content, organization, and representation of maps, layouts, layers, styles, symbols, and so forth. Access to the CIM is controlled by the MCT, which is, in turn, accessed through the QueuedTask. The MCT ensures that changes to CIM application state are synchronized across all the threads in the CIM thread pool and that the Pro UI is updated accordingly. Application enabled/disabled state of the UI is also automatically controlled during MCT Task execution and critical phases in the application such as view creation and application shutdown are coordinated with the MCT as well.
In most all cases, add-in developers will be using the QueuedTask exclusively when executing asynchronous operations in their custom add-in code.
Even though most all GIS operations in ArcGIS Pro will be performed on the QueuedTask, there are a small number of cases where operations may be suitable to be performed non-modally on a background thread (compatible with use of COM components). These operations may be long running and the add-in developer would like to avoid tying up the QueuedTask for their duration as well as leaving the Pro UI enabled. For this reason, at 2.6, the BackgroundTask has been added to the public API. In general, BackgroundTask is not for use by operations that access application state. It's usage and restrictions are covered in the Using BackgroundTask section.
Developers are not precluded from using the System.Threading.Tasks.Task within add-ins however developers should be aware that the "System" task is not compatible for use with COM components (which underlay the majority of the Pro APIs). Operations suitable for use with "System" task would be operations that contain only 3rd party code, perhaps to communicate with a proprietary web service or some other 3rd party application end-point that is inherently asynchronous and does not require access to the Pro application state. Operations used with a "System" task should be non-modal in nature and do not require the application busy state disabling the Pro UI (as is the case with QueuedTask). Use of "System" task in add-ins, given the availability of the BackgroundTask, should be rare and their use is outside the scope of this document.
Developers who have already tackled some Pro add-in development will probably have realized that the majority of the Pro API is synchronous and not "a"-synchronous. This may seem counter-intuitive for an asynchronous application to have a predominantly synchronous API, however, application workflows are typically sequential in nature with the component steps executed "in order" (eg a feature selection followed by an attribute change followed by a save). The workflow, itself, is typically what is executed asynchronously - as a single unit of work or "operation". The API is also quite granular and fine grained which is also better suited to a synchronous API as using asynchronous methods can be more challenging. Pro API methods, therefore, predominantly fall into two categories:
-
A small number of asynchronous methods that can be called on any thread. Methods of this type are named using an Async suffix and usually return Tasks ("usually" because they can also return void). In most cases, a synchronous version of an asynchronous method is also provided. Asynchronous methods tend to be coarse-grained in nature and can often be used "stand-alone".
-
A very large number of synchronous methods that should be called on a Pro worker thread only - mostly the MCT via QueuedTask. This is the majority of API methods. Attempting to call synchronous methods from the UI thread that require a QueuedTask will result in a
CalledOnWrongThreadExcetpion
.
There is also a third and fourth category:
- A handful of methods (both asynchronous and synchronous) that must be called on the GUI thread (typically they need to create or refresh Pro UI elements) and synchronous methods that have no thread affinity (eg many Geometry methods and methods that use cached application state or "snapshots" such as
map.GetLayersAsFlattenedList()
). Note: methods with no thread affinity are suitable for use with the BackgroundTask (in addition to the UI and QueuedTask).
Asynchronous methods, in the Pro API, are designed to be called safely from the UI thread* (though they can also be called on the QueuedTask or even BackgroundTask). Asynchronous methods can be identified by the "Async" suffix in their method name and have a return type of Task (e.g. PanToSelectedAsync, RedrawAsync, SetViewingModeAsync) . Asynchronous methods tend to be coarse-grained in nature and typically encapsulate a "canned" workflow or operation. In most cases, an asynchronous method in the API will have a synchronous counterpart though the majority of API methods, as previously mentioned, are synchronous only.
Let's use an example workflow to illustrate usage of Pro API asynchronous methods. In the code below, we are using the API to run a GP Tool "SelectLayerByAttribute" to select a county named "Kent CC" in the "TerritorialStats_3" layer. Next, we execute a zoom to the extent of the selected feature(s) (with a time delay to provide an animation effect on the zoom in). The asynchronous methods are being invoked directly from the UI:
internal class SelectCountyAsyncButton : Button {
//select the county of Kent. Zoom to its extent
protected override void OnClick() {
//Create the GP parameters
var values = new string[] { "TerritorialStats_3", "NEW_SELECTION", "name = 'Kent CC'" };
//Execute the GP Tool
Geoprocessing.ExecuteToolAsync("SelectLayerByAttribute_management", values);
//Zoom to the result
MapView.Active.ZoomToSelectedAsync(new TimeSpan(0, 0, 3));
}
}
When executing this code, we notice inconsistencies in its behavior. Sometimes, even though county always gets selected, the Pro extent does not change as the add-in does not zoom in (feature selection is supposed to be followed by a zoom to the extent of the selected feature). Other times the code works correctly with the feature selected followed by the expected zoom. This inconsistency is especially true the first time the code is executed when the underlying GP environment appears to be initializing.
The issue is that the call to "ExecuteToolAsync" executes asynchronously (as the name suggests) returning control immediately to the caller, sometimes before the selection is initiated. The add-in proceeds with code execution and calls "ZoomToSelectedAsync", the second asynchronous method in the workflow which, depending on the internal execution characteristics of ExecuteToolAsync, may execute its zoom logic before the selection is performed. So even though the expected sequence of the workflow (as written in the code) is for the selection first, and the zoom second, because of the asynchronous nature of these methods, the zoom can be performed first and the selection second (in which case there is no "zoom in").
What we need is a consistent way to invoke asynchronous methods such that they execute in a repeatable, and consistent fashion and in the intended order. In our case, we want to execute the GP "selection" tool first, have it complete, and then execute the zoom to selection. How to invoke asynchronous methods in a consistent fashion is the topic of the next section.
*As mentioned in the summary of API characteristics there are a few asynchronous methods that must be called on the UI for thread affinity reasons - but they are rare.
To recap: To ensure our GP execute tool and zoom to selection methods execute in the correct sequence, we need a way to wait for the first method (actually the returned Task) to complete before proceeding to the second method and so on. Moreover, we would like to wait for the first method to complete without blocking or "freezing" the UI. Luckily .NET provides just such a built-in language feature for this purpose called "async" and "await".
The async modifier is used to mark a method as asynchronous and denotes that the same method will be using the await operator internally. The await operator is "added" to each asynchronous method whenever the caller wants to do a non-blocking wait while allowing the asynchronous method to "complete" (specifically, to allow the returned task to complete). As each "awaited" call is non-blocking, the calling thread - typically the UI thread for add-ins - can remain responsive throughout the asynchronous method's execution.
The await operator, as we will see, is an incredibly powerful and versatile construct for asynchronous programming and it is always paired with as async modifier on the containing method. Our modified example using async and await is shown below:
internal class SelectCountyAsyncButton : Button {
//select the county of Kent. Zoom to its extent
//Note the addition of the 'async'
protected async override void OnClick() {/
//Create the GP parameters
var values = new string[] { "TerritorialStats_3", "NEW_SELECTION", "name = 'Kent CC'" };
//Execute the GP Tool - we await it's execution.
//UI remains responsive
await Geoprocessing.ExecuteToolAsync("SelectLayerByAttribute_management", values);
//Zoom to selection only when ExecuteToolAsync has completed
await MapView.Active.ZoomToSelectedAsync(new TimeSpan(0, 0, 3));
}
}
With the addition of the async and await (i.e. the "non-blocking" wait), the improved code now executes as expected. The selection completes first while the add-in does a non-blocking wait. The Pro UI remains responsive (it is not frozen or "blocked"). The zoom to select executes second and consistently zooms to the extent of the selected feature.
Asynchronous functions return values to the caller using a return type of Task<result> (read as "Task of 'result'" or "Task of type of 'result'") where "result" represents the type of the returned value. The returned value is contained within the task's Task.Result property (note: for an asynchronous function that returns just 'Task' there is no returned result and Task.Result is null).
Task.Result should not normally be accessed directly until the task has completed. Task.Result is a blocking property. Accessing it before the task is finished will block the caller until the task completes and the result value becomes available. Luckily, the keyword await we are using to perform a non-blocking wait on the task has the additional benefit of automatically resolving the returned value without having to extract it from the returned Task.Result property.
This is illustrated in the example below:
//Assume this async function returns an int representing some kind of count
//Note the return type of 'Task<int>'- "Task of 'int'"
public Task<int> GetCountOfFeaturesAsync(FeatureLayer featureLayer) { ... }
//We are calling 'GetCountOfFeaturesAsync' from the UI...perhaps via a button click
//Attempt 1 - add-in consumes the method to "get" the count...
//no await is being used
var count = GetCountOfFeaturesAsync(parcelLayer);
//This compiles but use of "var" masks the fact that our "count" local
//variable is actually set to Task<int> -not- int. The method is also not
//being awaited...most likely, the add-in developer forgot to add the await...
//Attempt 2 - using Task.Result directly
var task = GetCountOfFeaturesAsync(parcelLayer);
var count = task.Result;
//This approach works - but - should generally be avoided. Accessing
//Task.Result before the task has finished _blocks_ the UI
//Attempt 3 - using await
var count = await GetCountOfFeaturesAsync(parcelLayer);
//This is the preferred method. Await does a non-blocking wait _and_ resolves
//the Task.Result automatically. 'count' will contain the returned value when the
//task completes.
Part of the problem faced by the .NET Task infrastructure is how to propagate an unhandled exception back to the calling thread (typically the UI). To propagate an exception, the Task infrastructure wraps it in an AggregateException class. AggregateExceptions can also be nested if an attached child Task throws an exception that is propagated to its parent Task (which wraps that AggregateException in its own AggregateException and so on) but this is a very unlikely scenario for an add-in given that most add-ins will not be using a "System" Task but the Pro "QueuedTask" which runs all its operations on a single thread.
The unhandled exception(s) wrapped in the AggregateException are contained in its AggregateException.InnerExceptions property. AggregateException.InnerExceptions can be enumerated to access the original exception(s) that were thrown. However, unless the caller is awaiting the task that is running (or using Task.Wait which is a blocking wait), the AggregateException from the Task will not be propagated to the caller until the Task is garbage collected. This can lead to some seemingly strange behavior where an operation has since completed (eg from a Button click) and a second or two later, the AggregateException is thrown.
Fortunately, await, once again, comes to the rescue. When a task is being awaited, any unhandled exception gets propagated back to the caller while the operation is executing and is automatically extracted from the AggregateException.InnerExceptions
property by the await and thrown. This means add-in code using async and await can use normal try/catch semantics to handle exceptions thrown from asynchronous methods in the usual way.
The following code example illustrate some different scenarios and related exception behaviors from asynchronous methods in an add-in:
//We are calling 'DoWorkAsync' from the UI thread via a button click
//Scenario 1. The Task is not awaited...the "catch" will never catch an exception.
//Not recommended
try {
DoWorkAsync(...);
}
catch(Exception ex) {
//This code will never be hit
...
}
//Because we are not awaiting the asynchronous method, the add-in code will
//exit the scope of the try/catch the moment after DoWorkAsync() is called. Any
//exception thrown within DoWorkAsync will be propagated _after_ the returned task
//is garbage collected (which could be at a much later point in time depending on
//how long DoWorkAsync runs before failing ...)
...
//Scenario 2. use Task.Wait. Not recommended as it is a blocking wait
try {
DoWorkAsync(...).Wait();//ditto for DoWorkAsync().Result
}
catch(AggregateException ae) {
//access unhandled exception(s) via the InnerExceptions property...
foreach(var ex in ae.InnerExceptions) {
...
//Because we are waiting for the task to complete, the add-in remains
//within the scope of the try/catch. We can catch AggregateException and
//enumerate 'InnerExceptions' to retrieve the thrown exception.
//However, we are blocking the UI
...
//Scenario 3. Await the Task. This is the recommended approach.
try {
await DoWorkAsync(...);//non-blocking wait
}
catch(InvalidOperationException ioe) {
//await "unwraps" the unhandled exception which we can catch
//directly
...
//Because we are awaiting the task, we remain within the scope
//of the try/catch while DoWorkAsync() is executing and can catch
//the relevant exception type(s). Notice we do not need to
//handle 'AggregateException'...
Use of Progressors and _Cancelable_Progressors are covered in detail within ProConcepts-Framework, Progress and Cancellation. This section primarily augments that material with a discussion on the use of CancellationTokenSource and CancellationToken.
Note: The default for showing no progress or no cancellation with an asynchronous method that requires a Progressor parameter is to use the built-in static Progressor.None (or CancelableProgressor.None). Additionally, display of the progressor is suppressed whenever running in the debugger to prevent the progressor dialog interfering with the Visual Studio debugger UI thread and vice versa. The display of the progressor can be controlled with an overload of the ProgressorSource constructor that takes a delayedShow
parameter. Setting "delayedShow=true" delays dialog visibility if the Task completes quickly. Setting "delayedShow=false" (default) means there is no delay and the progressor is shown immediately.
CancellationTokenSource and CancellationToken
The .NET TAP API provides a non-visual, built-in cancellation mechanism that uses a CancellationTokenSource and a CancellationToken. The CancellationTokenSource provides the cancellation "trigger" or initiator whereas the CancellationToken provides the listener to allow cancellable methods to 'observe' if, or when, they have been cancelled. The CancellationTokenSource contains its associated CancellationToken within a CancellationTokenSource.Token
property.
To use a CancellationTokenSource, add-ins either instantiate a CancellationTokenSource directly, usually holding on to it as a class-level instance variable, or instantiate a CancelableProgressorSource which comes with a ready-made CancellationTokenSource property "built-in". The add-in can pass the CancellationTokenSource.Token to any asynchronous method that consumes it* (and that the add-in may wish to cancel). The add-in cancels the source by calling CancellationTokenSource.Cancel()
. This sets the CancellationToken.IsCancellationRequested property true allowing listeners (the executing asynchronous operation(s) in this case) to determine whether or not they have been cancelled.
It is the asynchronous operation's responsibility to periodically check the CancellationToken's cancellation status. Cancelled operations can choose to simply exit or, more commonly, they perform necessary clean-up and throw an OperationCancelledException which add-ins should be prepared to catch and handle.
Add-ins should dispose of the CancellationTokenSource after use, especially if it has been cancelled. Once cancelled, a CancellationTokenSource cannot be "un-cancelled" and re-used. The general pattern looks like this:
//class level variable - perhaps in the module or view model of a dockpane
private CancellationTokenSource _cts = null;
//A cancel command bound to a "Cancel" button on a dialog or dockpane
public ICommand CancelCmd {
get {
if (_cancelCmd == null) {
_cancelCmd = new RelayCommand(() => {
//Request cancellation.
_cts?.Cancel();
}, () => _cts != null, true, false);
}
return _cancelCmd;
...
//Runs the cancellable operation - called directly from a 'Start'
//button or similar...
private async void DoWorkAsync() {
_cts = new CancellationTokenSource();
try {
await RunCancelableProgressAsync(_cts.Token);
}
catch (OperationCanceledException e) {
//we were cancelled - take appropriate action
}
finally {
_cts = null;//Dispose of any old source
}
}
//The Cancellable operation elsewhere in the add-in...
//Consumes a CancellationToken
public Task RunCancelableProgressAsync(CancellationToken token) {
return QueuedTask.Run(()=> {
...
while(stillWorking) {
//Doing work
...
//Check cancellation
if (token.IsCancellationRequested) {
//we are cancelled - so we can either simply exit....
stillWorking = false;//or "break", etc.
...
//...or... we can terminate throwing an
//OperationCanceledException
token.ThrowIfCancellationRequested();
}
}
}
}
*Note: cancelable async functions that consume a CancelableProgressorSource can always grab the CancellationTokenSource from the progressor to obtain the CancellationToken.
The majority of methods in the Pro API are synchronous and most all of the Pro synchronous methods require that they are run on the special Pro worker thread called the MCT. Synchronous methods that require the Pro worker thread are marked, in Intellisense, and the API reference, with "This method must be called on the MCT. Use QueuedTask.Run".
This tells the developer that the method must be executed (within an operation) within the context of a QueuedTask.Run. The QueuedTask.Run (and BackgroundTask.Run) returns a task to the caller when it is invoked that can be awaited while it completes.
The initial mistake add-in developers can make when first working with the Pro API is to attempt to call synchronous methods directly in their add-in logic without wrapping them in a QueuedTask.Run. This will result in a ArcGIS.Core.CalledOnWrongThreadException
exception* when the first API method that requires the MCT is called. This error is easily fixed by wrapping the appropriate methods in a QueuedTask.Run which is the subject of the next section.
* It is not intended that the CalledOnWrongThreadException occur in production code. The exception is provided as a debugging aid to inform developers that they are using the wrong thread to invoke the method. It is assumed that this condition would be fixed in the normal course of development.
The most common syntax for invoking synchronous methods with a QueuedTask.Run is to use what is known as an "anonymous" function or delegate or "lambda". A lambda that does not return a value is defined as an 'Action' whereas a lambda that does return a value is defined as a 'Func'. QueuedTask.Run (and BackgroundTask.Run) accept both types meaning that add-in developers can choose to return a value from their lambdas to the caller, or not, as needed*. Lambdas can also be parameterized to accept arguments though that is not common.
The general syntax for using a lambda with the QueuedTask* is as follows (although not explicitly mentioned, the "general" syntax is pretty much the same for use with a BackgroundTask.Run as well):
Task t = QueuedTask.Run(() => {
//code goes here
...
});
//Though this is the more common syntax - rather than assigning the returned
//task into a local variable we simply await it - same as we did with asynchronous
//methods in the preceding section
await QueuedTask.Run(() => {
//code goes here
...
});
The lambda is defined by the () => { ... }
syntax placed within the scope of the QueuedTask.Run. Parameters, if there were any, would be added to the "()" parentheses of the lambda like so: (x_val, y_val) => { ... }
. Notice that a type specifier is not required for the parameters. It is inferred from their type. As we have seen with asynchronous methods, the returned task can be awaited.
*Add-in developers do not always have to "package" their add-in into a lambda. Custom methods can also be used in-lieu of anonymous delegates. A lambda is simply convenient because it keeps the "worker code" within the same function rather than being 'housed' in a separate method.
We will use the following example workflow to illustrate the use of QueuedTask.Run. We assume an add-in developer has implemented the following code to select all selectable features within the current map extent whenever the containing 'SelectFeaturesButton' is clicked:
///<summary>Select all visible and selectable features within the "middle" of
///the current view extent</summary>
internal class SelectFeaturesButton : Button {
protected override void OnClick() {
Envelope selExtent = null;
if (MapView.Active.ViewingMode == ArcGIS.Core.CIM.MapViewingMode.Map) {
var extent = MapView.Active.Extent;
selExtent = extent.Expand(-0.25, -0.25, true);
MapView.Active.SelectFeatures(selExtent);
}
else if (MapView.Active.ViewingMode == ArcGIS.Core.CIM.MapViewingMode.SceneGlobal ||
MapView.Active.ViewingMode == ArcGIS.Core.CIM.MapViewingMode.SceneLocal) {
//get the extent in pixels
var sz = MapView.Active.GetViewSize();
var builder = new EnvelopeBuilder() {
XMin = 0,
YMin = sz.Height,
XMax = sz.Width,
YMax = 0
};
selExtent = builder.ToGeometry().Expand(-0.25, -0.25, true);
MapView.Active.SelectFeatures(selExtent);
}
}
}
When the add-in developer does an initial debug of the code, the developer immediately gets a CalledOnWrongThreadException exception thrown:
On examination of the code, we notice that the developer forgot to wrap his or her add-in code in a QueuedTask. Therefore the add-in is (accidentally) attempting to execute all of the Pro API methods on the UI thread which causes the exception to be thrown. MapView.Active.SelectFeatures, the EnvelopeBuilder property setters, and EnvelopeBuilder.ToGeometry method all require the use of the MCT and must be invoked within a QueuedTask.Run. Executing any one of them on the UI thread will throw a CalledOnWrongThreadException.
To fix this oversight, the developer add-in developer changes his or her code to use a QueuedTask.Run for all methods that require it, wrapping each respective call in its own lambda:
///<summary>Select all visible and selectable features within the "middle" of
///the current view extent</summary>
internal class SelectFeaturesButton : Button {
//Notice addition of 'async' and 'await'...
protected async override void OnClick() {
Envelope selExtent = null;
if (MapView.Active.ViewingMode == ArcGIS.Core.CIM.MapViewingMode.Map) {
var extent = MapView.Active.Extent;
selExtent = extent.Expand(-0.25, -0.25, true);
//SelectFeatures must be run on the MCT.
await QueuedTask.Run(() => {
MapView.Active.SelectFeatures(selExtent);
});
}
else if (MapView.Active.ViewingMode == ArcGIS.Core.CIM.MapViewingMode.SceneGlobal ||
MapView.Active.ViewingMode == ArcGIS.Core.CIM.MapViewingMode.SceneLocal) {
//get the extent in pixels
var sz = MapView.Active.GetViewSize();
//EnvelopeBuilder must be run on the MCT.
await QueuedTask.Run(()=> {
var builder = new EnvelopeBuilder() {
XMin = 0,
YMin = sz.Height,
XMax = sz.Width,
YMax = 0
};
selExtent = builder.ToGeometry().Expand(-0.25, -0.25, true);
});
//SelectFeatures must be run on the MCT.
await QueuedTask.Run(() => {
MapView.Active.SelectFeatures(selExtent);
});
}
}
}
"Technically", with regards to the above example, the developer has now satisfied the requirements of the API by using a QueuedTask.Run to submit the relevant operations that require it to the MCT. The code executes "as advertised". However, this implementation is not optimal. Even though the API states that these methods do indeed require the use of the MCT, it does not literally mean that each method requires the use of the MCT individually (i.e. within separate lambdas).
Instead, we should consolidate the sequence of API methods into a single lambda (whenever possible) like so:
//Avoid this...
await QueuedTask.Run(() => {
method1(...);
});
await QueuedTask.Run(() => {
method2(...);
});
await QueuedTask.Run(() => {
method3(...);
});
//Prefer this...
await QueuedTask.Run(() => {
method1(...);
method2(...);
method3(...);
});
There is also a small overhead associated with constructing a QueuedTask.Run which, although minimal, can add up when the QueuedTask is being invoked within a loop. This is another reason to favor consolidation of code into a single lambda:
//Avoid this...
foreach(var some_item in ...) {
await QueuedTask.Run(() => { //QTR inside the loop
//do work
});
}
//Prefer this...
await QueuedTask.Run(() => { //QTR outside the loop
foreach(var some_item in ...) {
//do work
}
});
Another modification we can make is to capture the state of the active view before we invoke the lambda:
var mv = MapView.Active;//capture the active view state
//use "mv"...
Though straightforward in appearance, use of MapView.Active
could occasionally result in an exception within the application. Conceivably, previously queued operations may be running on the MCT when our selection logic is invoked, and these need to complete before our operation can start executing.
Even though this is an edge case, it is possible for the application state to change between the time our add-in code is invoked and the QueuedTask.Run actually runs. For example, the active view may get changed to a table or the view pane may be closed before our lambda executes. If so, MapView.Active
could become null resulting in an exception. Although not technically required, it can help to rugged-ize your add-in code by using local variables to capture snapshots of the application state. Application state captured in local variables before the lambda is called won’t change out from under you.
This is the code example, now consolidated into a single lambda and application state captured into a local variable:
///<summary>Select all visible and selectable features within the "middle" of
///the current view extent</summary>
internal class Button1 : Button {
//Notice addition of 'async' and 'await'...
protected async override void OnClick() {
Envelope selExtent = null;
var mv = MapView.Active;//capture the Mapview state...add null check if needed
//Consolidate the code into a single QueuedTask.Run.
await QueuedTask.Run(() => {
if (mv.ViewingMode == ArcGIS.Core.CIM.MapViewingMode.Map) {
var extent = mv.Extent;
selExtent = extent.Expand(-0.25, -0.25, true);
mv.SelectFeatures(selExtent);
}
else if (mv.ViewingMode == ArcGIS.Core.CIM.MapViewingMode.SceneGlobal ||
mv.ViewingMode == ArcGIS.Core.CIM.MapViewingMode.SceneLocal) {
//get the extent in pixels
var sz = mv.GetViewSize();
var builder = new EnvelopeBuilder() {
XMin = 0,
YMin = sz.Height,
XMax = sz.Width,
YMax = 0
};
selExtent = builder.ToGeometry().Expand(-0.25, -0.25, true);
mv.SelectFeatures(selExtent);
}
});//end of the QueuedTask.Run lambda
}
}
Note: as there is no code following the QueuedTask.Run in our example, the 'await' is not strictly necessary. Notice too that the OnClick returns void but can still be marked "async". Asynchronous methods that are marked void are very common when associated with events or methods tied directly to the UI (as it the case here).
Exception handlers within a lambda need no special treatment. Unless an asynchronous method is executing within a lambda that is not being awaited, the normal pattern of try/catch/finally can be used as needed. To catch an exception outside the lambda, the QueuedTask.Run or BackgroundTask.Run should be awaited within a try/catch block same as was previously described for dealing with exceptions from asynchronous methods.
await QueuedTask.Run(()=> {
//"normal" try/catch inside the lambda
try {
...
}
catch(InvalidOperationExcepion ioe) { ... }
});
try {
//QTR is awaited!...
await QueuedTask.Run(()=> ....);
}
catch(ArgumentNullException ane) {
... etc ...
QueuedTask.Run (and BackgroundTask.Run) provides overloads for lambdas with and without return values (i.e. 'Func' and 'Action' delegates respectively). To return a value from the lamda, therefore, add-in developers simply add the requisite 'return' statement(s), within the lamda, where needed. Return values, as previously discussed in the Returned Values from Asynchronous Functions section are made available in the Task.Result property which can be 'unwrapped' via an await statement. To assign the returned value into a variable from the Task.Result, add-in developers should use the await operator:
//no return value. QueuedTask is being awaited
await QueuedTask.Run(() => {
//do work
});
//the value in item_count is returned in the Task.Result.
//It is assigned into the local variable 'result' when
//the task completes
var result = await QueuedTask.Run(() => {
//do work - return a value
return item_count;
});
//Be aware of this, notice - no await
//'result' will be assigned a value of type Task<int> and _not_
//int which may not be what the add-in developer was expecting...
var result = QueuedTask.Run(() => {
//do work - return a value
return item_count;
});
//to get the result the task still needs to be awaited...
var count = await result;
Depending on the characteristics of the given workflow implemented in a lambda, there may be a need to call an asynchronous method within the lambda. The same rules apply as for invoking an asynchronous method from the UI thread. Namely, the asynchronous method should be awaited if the intent is to do a non-blocking wait while the method completes. Using an await requires an 'async' modifier be placed on the QueuedTask.Run lambda*:
//execute a QueuedTask that contains an asynchronous method
//note the 'async' modifier on the lambda
await QueuedTask.Run( async () => {
...
//async method within the lambda is awaited...
var result = await FooAsync(...);
...
});
*Nested QueuedTasks, within a "parent" QueuedTask, are in-lined meaning they act as if they were each an ordinary synchronous function and async and await (on nested QueuedTasks) are not actually needed (recall - all QueuedTasks run on the MCT, one at a time, in order of execution. Nested QueuedTasks are no different). However, unless you are confident that a nested asynchronous method is using a QueuedTask, there is no downside to using async and await repetitively 'recipe-style' except in the slightly more complex syntax that results in the enclosing lambda from their use.
Add-in developers often find it useful to create their own libraries of classes and methods - especially utility and helper functions that wrap re-usable workflows. The suggested pattern for add-in developers is to follow the same conventions as the API - namely: write predominantly synchronous methods and provide asynchronous alternatives as needed.
Assume, for the following discussion, that we have the same selection and zoom workflow from the previous section that we want to 'convert' into a synchronous and asynchronous method.
To be synchronous, an add-in method ideally does not contain any asynchronous code* and cannot return a type of Task.
This is our initial workflow consolidated into a synchronous method. It has three parameters, one of type MapView and the other two are for how the extent will be expanded. The method returns the selection results to the caller:
//Synchronous utility method
public Dictionary<BasicFeatureLayer,List<long>> SelectFeaturesWithViewExtent(
MapView mv, double ratio, bool asRatio) {
Envelope selExtent = null;
//null check
if (mv == null) throw new ArgumentNullException(nameof(mv));
if (mv.ViewingMode == ArcGIS.Core.CIM.MapViewingMode.Map) {
selExtent = mv.Extent.Expand(ratio, ratio, asRatio);
}
else if (mv.ViewingMode == ArcGIS.Core.CIM.MapViewingMode.SceneGlobal ||
mv.ViewingMode == ArcGIS.Core.CIM.MapViewingMode.SceneLocal) {
//get the extent in pixels
var sz = mv.GetViewSize();
var builder = new EnvelopeBuilder() {
XMin = 0,
YMin = sz.Height,
XMax = sz.Width,
YMax = 0
};
selExtent = builder.ToGeometry().Expand(ratio, ratio, asRatio);
}
return mv.SelectFeatures(selExtent);
}
There is nothing particularly unique about this code and it can be incorporated into other workflows, as needed, in the usual way - except - it can only be invoked as part of an operation running within a QueuedTask.Run because of the requirements of the underlying Pro API it is consuming.
*This is more a rule of thumb than an absolute. The key is that a method cannot, under any circumstance, be marked as 'async' to be considered synchronous. Therefore, a synchronous method can execute asynchronous code, but to remain synchronous, it just can't await it.
We may want to add a thread check to our synchronous function, as it is scoped public, if we will not be the only consumer. It is not required for custom methods to perform a thread check as the synchronous API methods will take care of throwing an exception if they are called on the wrong thread anyway - however it can provide a useful self-documenting clue to another add-in developer browsing the code.
The QueuedTask provides two static properties add-ins can use to check the thread the code is currently executing on: QueuedTask.OnGUI and QueuedTask.OnWorker to determine if we are on the UI or MCT respectively. Either will work for our purposes. If we are not on the MCT we will throw a CalledOnWrongThreadException making the thread affinity requirements of our method clear to other developers. Usually, the thread check goes first:
public Dictionary<BasicFeatureLayer,List<long>> SelectFeaturesWithViewExtent(
MapView mv, double ratio, bool asRatio) {
Envelope selExtent = null;
//self-documenting thread check
if (!QueuedTask.OnWorker) {
throw new ArcGIS.Core.CalledOnWrongThreadException();
}
//null check
if (mv == null) throw new ArgumentNullException(nameof(mv));
...
}
In certain cases, custom methods need to update UI element content (such as controls showing progress) on the UI. In WPF, UI element content can only be updated from the UI thread*. THerefore, if our custom method is executing on a background thread (such as QueuedTask or BackgroundTask) we must properly invoke the code that does the update on the UI thread. WPF provides a Dispatcher class specifically for this purpose.
A Dispatcher can be accessed off any WPF UI element but as most of our add-in code will probably be running in a non-visual class like a view model or add-in Module, it will have no ready access to a WPF UI element instance (to access a Dispatcher). However, the Pro application instance is readily available via FrameworkApplication.Current
and its Dispatcher can conveniently be used for this purpose:
//access the Pro application dispatcher to invoke UI updates
var dispatcher = FrameworkApplication.Current.Dispatcher;
...
The WPF Dispatcher object provides two primary method for updating UI elements - Dispatcher.BeginInvoke which is asynchronous and Dispatcher.Invoke which is synchronous (and blocks). BeginInvoke
schedules the given action for execution on the UI and immediately returns control to the add-in. BeginInvoke should not be awaited (as that will "convert" your synchronous method to an asynchronous one). It is, basically, a fire and forget mechanism. If it is important that the UI be exactly in-sync with the add-in code then use Invoke
. Invoke blocks the caller until the UI has been updated. In practice the use of Invoke should probably be rare.
If your add-in code that does the UI update can run both on the UI and/or a Pro background thread you can test whether or not an Invoke is required using Dispatcher.CheckAccess()
. CheckAccess checks whether or not the current thread is the same thread that is associated with the Dispatcher (the UI thread in our case). If CheckAccess returns true then invoke is not needed (as we are on the UI thread). If it return false then an invoke is needed (as we are not on the UI thread).
The completed logic looks like this*:
//Assume this method does the relevant UI update...
private void UpdateProgress(...) {
//whatever "update progress" means - change a counter, text, etc
...
NotifyPropertyChanged("ProgressValue");//Assuming something is bound to this
}
//Our method that needs to update the UI
public void DoWork(...) {
...
//this code is running synchronously but on which thread?
//check UI access...
if (FrameworkApplication.Current.Dispatcher.CheckAccess()) {
//this means we are on the UI thread already
UpdateProgress(...);//no invoke necessary
}
else {
//this means we are on another thread (usually the MCT)
//we must invoke progress via the Dispatcher
FrameworkApplication.Current.Dispatcher.BeginInvoke(
(Action)(() => UpdateProgress(...))); //Fire and forget...
}
...
*For simply communicating "progress" to the user, the progressor classes provide a pure callback mechanism to update progress at nominal frequencies.
Notice this example is using BeginInvoke (asynchronous) and is not awaiting it. The syntax for using BeginInvoke can be somewhat intimidating. The main issue is that, typically, if we want to use the overload that excepts just the anonymous delegate (i.e. our "() => ... " lambda) we have to provide an explicit cast of the lambda to type "Action" otherwise the code will not compile. The cast looks like this: (Action)(...lambda goes here...)
which leads to the final syntax of (Action)(() => UpdateProgress(...))
.
Add-ins can consolidate the UI update logic into a utility method which has the added benefit of simplifying the syntax like so:
//Utility method to consolidate UI update logic
public void RunOnUIThread(Action action) {
if (FrameworkApplication.Current.Dispatcher.CheckAccess())
action();//No invoke needed
else
//We are not on the UI
FrameworkApplication.Current.Dispatcher.BeginInvoke(action);
}
//Assume this method does the relevant UI update...
private void UpdateProgress(...) { ... }
//Our method that needs to update the UI
public void DoWork(...) {
...
//Use the utility method...
Module1.Current.RunOnUIThread(() => UpdateProgess(50, "working..."));
...
*All WPF UI content derives from DispatcherObject. DispatcherObjects can only be accessed from the thread on which they were created - often referred to as the owning thread. In most cases, the owning thread for UI content is the UI thread.
To follow the Pro API pattern, we can choose to provide an asynchronous alternative to our custom "select" method which will allow it to be safely called from the UI thread. In designing our asynchronous method we wrap the synchronous method within a QueuedTask and change the returned type to be a type of "Task". We will add an "Async" suffix to the method name to further differentiate it from its synchronous counterpart though this is not required and is a convention only. Our asynchronous method is implemented as follows:
//recall: synchronous method signature
public Dictionary<BasicFeatureLayer,List<long>> SelectFeaturesWithViewExtent(
MapView mv, double ratio, bool asRatio) { ... }
//Asynchronous alternative that wraps the synchronous method
public Task<Dictionary<BasicFeatureLayer,List<long>>> SelectFeaturesWithViewExtentAsync(
MapView mv, double ratio, bool asRatio)
{
//Wrap the synchronous method in a QueuedTask
return QueuedTask.Run(() => SelectFeaturesWithViewExtent(mv, ratio, asRatio));
}
By writing our custom add-in code as predominantly synchronous methods we can easily provide asynchronous alternatives, as needed, simply by wrapping the synchronous methods within a QueuedTask.Run. SelectFeaturesWithViewExtentAsync can now be safely called from the UI thread.
Most all GIS operations in ArcGIS Pro will be performed on the QueuedTask. However, there are a small number of cases where operations may be suitable to be performed non-modally on a background thread (compatible with use of COM components). These operations may be long running and the add-in developer would like to avoid tying up the QueuedTask for their duration as well as leaving the Pro UI enabled. BackgroundTask executes operations on the Pro background thread pool, which prior to 2.6, has not been made available to the public API.
The Pro background thread pool is divided into normal and high priority parts. The normal priority pool is intended for most operations and is appropriate for moderate to long running operations. The high priority pool is strictly reserved for short duration operations which are computational in nature and do not involve I/O operations. If you are unable to reliably determine if a particular request will be short or long, assume long and use the normal priority pool. Background operation priority is specified via a ArcGIS.Core.Threading.Tasks.TaskPriority
parameter passed as the first argument to BackgroundTask.Run().
To determine if an operation is suitable for use with the BackgroundTask, as opposed to the QueuedTask, it should meet the following criteria:
- The operation does not access or alter any Pro state (i.e. no CIM state so no reading/writing of layers, map, styles, layouts, or other project content). It is safe to read the state at the very beginning of the operation but a background operation cannot assume that the application state will remain static once it has started (as it can when within a QueuedTask).
- The operation does not require any input from the user once running (though a background task can still convey progress to the user through the UI)
A BackgroundTask is significantly different from a QueuedTask. Whereas QueuedTask queues operations onto a single thread (the MCT) and executes submitted operations in FIFO order, BackgroundTask runs submitted operations concurrently using whichever thread is available in the Pro background thread pool. Shared add-in state between concurrently executing background tasks must be protected with the appropriate locks, mutexes, etc, to prevent corruption. During the lifetime of a background operation users are free to unload the current project, change active views, and make alterations to the application. Background operations should be robust enough to handle such cases. Nested BackgroundTasks, if such should occur, are not in-lined. Contrast this with QueuedTask where nested QueuedTasks are in-lined (onto a single thread) and execute as if they were synchronous functions.
Although BackgroundTask is not really intended for use with objects that have thread affinity, thread affinity can be provided by using a TaskPriority of type TaskPriority.single
when invoking the lambda in BackgroundTask.Run to executes Tasks on a consistent thread:
//Use TaskPriority.single for background operations with thread affinity
await ArcGIS.Core.Threading.Tasks.BackgroundTask.Run(
TaskPriority.single, () => {
//Do Work
...
}, BackgroundProgressor.None);
In the following example, a utility method uses the BackgroundTask to generate a series of Preview Images from a collection of StyleItems. As application state is not being changed there is no need to tie up the QueuedTask with this operation. Additionally, as generation of preview images can be expensive, the BackgroundTask is being run using the default priority of TaskPriority.normal
:
using System.Windows.Media;
using System.Windows.Media.Imaging;
using ArcGIS.Core.Threading.Tasks;
...
//Generate preview images for the input style items
internal Task<List<ImageSource>> GeneratePreviewImagesAsync(IList<StyleItem> styleItems) {
//Application state is not affected so let's use a background thread...
//This is potentially long running so leave the priority at 'normal'
return BackgroundTask.Run(() => {
//make each image a little bigger (just for purposes of visibility)
var scale = new ScaleTransform() {
ScaleX = 2,
ScaleY = 2
};
var preview_images = new List<ImageSource>();
foreach (var style_item in styleItems) {
//If PreviewImage is null, a PreviewImage will be generated
var bmsource = (BitmapSource)style_item.PreviewImage;
//scale up the image and save it
preview_images.Add(new TransformedBitmap(bmsource, scale));
}
return preview_images;
}, BackgroundProgressor.None);
}
//Usage...
var styles = Project.Current.GetItems<StyleProjectItem>().ToList();
var theStyle = styles.First(style => style.Name == "ArcGIS 2D");
//Get all the point symbols from the 2D style
var point_symbols = await QueuedTask.Run(() => theStyle.SearchSymbols(StyleItemType.PointSymbol, ""));
//Generate preview images for each in the background
var preview_images = await GeneratePreviewImagesAsync(point_symbols);