Loadable pattern for asynchronous resources

Resources such as layers, maps, portal items, tasks, and so on, commonly rely on remote services or datasets on disk to initialize their state. The nature of accessing such data requires the resources to initialize their state asynchronously. The loadable design pattern unifies the behavior that different resources use to load metadata asynchronously, and resources that adopt this pattern are referred to as "loadable." The pattern also provides a mechanism to retry if previous attempts to load failed so that you can properly handle and recover from exceptional circumstances such as network outages or service interruption. Loadable resources appropriately handle concurrent and repeated requests to load in order to accommodate the common practice of sharing the same resource instance among various parts of an application, and also permit cancellation so that you can cancel loading a resource, for example, if the service is slow to respond. Finally, loadable resources provide information about their initialization status through explicit states that can be inspected and monitored.

Loadable API

Classes that conform to the loadable pattern implement the ILoadable interface.

Load status

The LoadStatus property reflects the state of the loadable resource. Four states are possible.

  • NotLoaded—the resource has not been asked to load its metadata and its state isn't properly initialized yet.
  • Loading—the resource is in the process of loading its metadata asynchronously.
  • FailedToLoad—the resource failed to load its metadata (for example, due to network outage, or the operation was cancelled, and so on.) The error encountered is available in the LoadError property.
  • Loaded—the resource successfully loaded its metadata and its state is properly initialized.

The following state transitions represent the stages that a loadable resource goes through.

Stages of a loadable resource

The ILoadable interface includes events for responding to changes in a resource's load status.

Loading

A resource commences loading its metadata asynchronously when LoadAsync is invoked. At that time, the load status changes from NotLoaded to Loading. When the asynchronous operation completes, the Loaded callback is invoked. If the operation encounters an error, the object's LoadError property is populated, and load status is set to FailedToLoad. If the operation completes successfully, LoadError is null and the load status is set to Loaded, which means the resource has finished loading its metadata and is now properly initialized.

Many times, the same resource instance is shared by different parts of the application. For example, a legend component and a table of contents component may have access to the same layer, and they both may want to access the layer's properties to populate their UI. Or the same portal instance may be shared across the application to display the user's items and groups in different parts of the application. In order to accommodate this type of application development pattern and make things simple for you, LoadAsync supports multiple "listeners". It can be called concurrently and repeatedly, but only one attempt is ever made to load the metadata. If a load operation is already in progress (Loading state) when LoadAsync is called, it simply piggy-backs on the outstanding operation and the callback is enqueued to be invoked when that operation completes. If the operation has already completed (Loaded or FailedToLoad state) when LoadAsync is called, the callback is immediately invoked with the past result of the operation, be it success or failure, and the state remains unchanged. This makes it safe for you to liberally invoke LoadAsync on a loadable resource, without having to check if the resource is already loaded or not, and without worrying that it will make unnecessary network requests every time.

If a resource has failed to load, calling LoadAsync on it subsequently will not change its state. The callback will be invoked immediately with the past load error. In order to retry loading the resource, you must use RetryLoadAsync instead. The advantage of this approach is that if, hypothetically, LoadAsync were to retry loading a failed resource, an app could easily end up making many asynchronous requests to load the same resource just because it shared the same resource instance in different parts of the application and each part tried to load the resource without first checking if it was already loading. This pattern allows LoadAsync to be invoked indiscriminately without worrying that it might add overhead, and makes retrying a separate and explicit scenario using RetryLoadAsync.

Retry loading

A resource retries to load its metadata when RetryLoadAsync is invoked, but only if it previously failed to load (FailedToLoad state) or wasn't loaded to begin with (NotLoaded state). The resource transitions to the Loading state and makes a new attempt to fetch its metadata. The Loaded event callback is invoked when the asynchronous operation completes.

If the resource has already fetched its metadata and has initialized its state (Loaded state), retry doesn't do anything. Instead, retry immediately calls the callback with the past result and the state of the object remains unchanged. The object's metadata isn't fetched again.

If an asynchronous operation is already in progress (Loading state) when RetryLoadAsync is called, it simply piggy-backs on the outstanding operation and the callback is enqueued to be invoked when that operation completes.

The main use case for this method is if the loadable failed to load previously, for example, due to network outage or service interruption. It is not meant to refresh the metadata for an already loaded resource which should instead be accomplished by creating a new instance of the loadable resource.

Cancel loading

A resource cancels any outstanding asynchronous operation to load its metadata when CancelLoad is invoked and it transitions from Loading to FailedToLoad state. The LoadError property will contain information that reflects that operation was cancelled.

This method should be used carefully because all enqueued callbacks for that resource instance will get invoked with an error stating that the operation was cancelled. Thus, one component in the application can cancel the load initiated by other components when sharing the same resource instance.

The CancelLoad method does nothing if the resource is not in Loading state.

Conveniences

Cascading load dependencies

It is common for a loadable resource to depend on loading other loadable resources to properly initialize its state. For example, a portal item cannot finish loading until its parent portal finishes loading. A feature layer cannot be loaded until its feature service table is first loaded. This situation is referred to as a load dependency.

Loadable operations invoked on any resource transparently cascade through its dependency graph. This helps simplify using loadable resources and puts the responsibility on the resource to correctly establish and manage its load dependencies.

The following code example shows how this cascading behavior leads to concise code. Loading the map causes the portal item to begin loading, which in turn initiates loading its portal. You do not have to load each one of them explicitly.

// use the authentication manager to generate a credential for the portal
var cred = await AuthenticationManager.Current.GenerateCredentialAsync(
    new Uri(portalUrl),
    username,
    password);


// pass the user's credential when creating the portal instance
var myPortal = await ArcGISPortal.CreateAsync(new Uri(portalUrl), cred);


// get a portal item (using its ID)
var webMapItem = await PortalItem.CreateAsync(myPortal, portalItemID);


// create a new map from the item, load the map
var myMap = new Map(webMapItem);
await myMap.LoadAsync();


// make sure the map loaded
if (myMap.LoadStatus != LoadStatus.FailedToLoad)
{
    // add the map to the map view (dispatch to the UI thread)
    await Dispatcher.InvokeAsync(() => MyMapView.Map = myMap);
}

It is possible that dependencies may fail to load. Some dependencies might be critical without which the initiating resource cannot load successfully, for example, a portal item's dependency on its portal. If a failure is encountered while loading such a dependency, that error would bubble up to the resource that initiated the load cycle, which would also fail to load. Furthermore, retrying to load that resource would automatically retry loading the failed dependency. Other load dependencies may be incidental, such as a map's dependency on one of its operational layers, and the resource may be able to load successfully even if one of its dependency fails to load.

Overriding state before initialization

A loadable resource's state is not properly initialized until it has finished loading. Accessing properties of the resource before the resource is loaded could return null or uninitialized values that might change when the resource finishes loading. Therefore, it is always advisable to wait until the resource has finished loading before accessing its properties. However, many times, especially while prototyping, you may want to change a property value before the resource is loaded without regard for its proper initialized value. For instance, you may want to change the scale visibility of a layer or the initial viewpoint of a map. To simplify this workflow without requiring that the resource first be loaded, loadable resources permit overriding their properties before the resource has finished loading and the overridden value will take precedence over the value specified in the resource's metadata.

The following code example shows how you can override the min/max scale properties of a layer without having to load it first.

// create a new layer, set min/max scale
var censusLayer = new ArcGISMapImageLayer(new Uri("http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer"));
censusLayer.MinScale = 5000;
censusLayer.MaxScale = 1000000;


// handle the Loaded event
censusLayer.Loaded += (s, e) =>
{
    // after loading, min/max scale should have values provided above
    Debug.Assert(censusLayer.MinScale == 5000);
    Debug.Assert(censusLayer.MaxScale == 1000000);
};


// load the layer
await censusLayer.LoadAsync();


In this topic
  1. Loadable API
  2. Conveniences