Skip to content

ProConcepts Framework

UmaHarano edited this page Nov 6, 2023 · 38 revisions

ArcGIS Pro is a highly configurable and extensible application. All software modifications and enhancements are achieved using add-ins. The add-in model provides a declaratively-based framework for creating a collection of customizations conveniently packaged in a single compressed file. Add-ins are easily shared, as they do not require installation programs or registration; add-ins are added to a system by simply copying them to a well-known folder and removed by deleting them from this folder. Add-ins can also be shared between users within an organization using a centralized network share.

Add-ins are authored using .NET along with Esri’s Desktop Application Markup Language (DAML). The DAML (an XML language created by Esri) describes the customizations; the .NET classes provide the custom behavior. The ArcGIS Pro software development kit (SDK) includes an Add-In Wizard that integrates with Microsoft Visual Studio to simplify development.

Language:      C#
Subject:       Framework
Contributor:   ArcGIS Pro SDK Team <arcgisprosdk@esri.com>
Organization:  Esri, http://www.esri.com
Date:          10/02/2023  
ArcGIS Pro:    3.2
Visual Studio: 2022

In this topic


Add-in extensibility points

The ArcGIS Pro add-in framework supports a host of extensibility points for add-ins. The following list shows the most common customizations:

  • Ribbon
    • Tabs/Contextual Tabs
    • Groups
    • Controls
      • Buttons
      • Split buttons
      • Button palettes
      • Tools
      • Tool palettes
      • Toolbars
      • Galleries
      • Combobox
      • Editbox
      • Menus
      • Dynamic menus
      • Custom controls
  • Panes
  • DockPanes
  • Property sheets/pages
  • Backstage tabs
  • Component categories

Configurations

An ArcGIS Pro configuration is an advanced customization of the application. Configurations are similar to add-ins but offer more ways to extend the application, helping you design a version of ArcGIS Pro that reflects your organization’s brand and workflows. Some of the key things you can do with configurations include the following:

  • Make a custom splash screen and a custom start page
  • Change the application title and icon
  • Rearrange or remove infrequently-used controls; insert new controls
  • Inject logic during startup to check licensing or alter the user interface depending on user roles
  • Control which add-ins load
  • Filter commands (command notification and the option to disable them ).

A configuration can also expose new functionality like modules, buttons, and dock panes, exactly like an add-in.

More information on Configurations can be found in ProConcepts Configurations and ProGuide Configurations

Plugin Datasource

Plugin Datasource was a pattern first released in ArcObjects to allow 3rd parties to integrate custom ("unsupported") data sources into Esri software. Starting at 2.3, the Pro framework was extended to incorporate custom plugin data sources into Pro. Whereas in ArcObjects, plugin data source (or "workspace") implementations are COM dlls, in Pro, plugin data sources are addins. As addins they enjoy many of the same benefits such as double-click registration via RegisterAddin.exe, xcopy-style deployment to well-known folders, forward compatibility, and, implementation in .NET using the ArcGIS.Core.Data API.

The underlying Geodatabase structure of a plugin is explained in ProConcepts Plugin Datasource and a step-by-step guide on how to implement a Plugin Datasource is explained in ProGuide Plugin Datasource. The Pro SDK, since 2.3, provides a project template, in C# only, for generating a Plugin Datasource .csproj from scratch.

plugin-template-vs2019.png

Custom plugin data sources are implemented entirely in .NET. They have a .esriPlugin extension and are deployed to the same well-known folder location as "regular" Pro add-ins with one exception. Whereas Pro addins are copied to a versionless "ArcGISPro" folder, the default plugin deployment folder is versioned (ArcGISPro2.4, ArcGISPro2.5, ArcGISPro2.6, ...). However, plugins still have the same loading characteristics as regular addins - such as previous versions will be loaded by later versions of Pro, Pro assembly references are automatically forwarded, latest version of a plugin wins, etc. The versioned default deployment folder is an idiosyncrasy of the way plugins are loaded in Pro.

DefaultAddinFolder.png

Plugins are loaded by the native (c++) code of Pro and the native code uses versioned addin folders (same as was with Arcobjects 10x) to discover addins (or "plugins", as in this case). Plugins are loaded by native code to allow them to integrate with both managed and native clients though this is an implementation detail that is not important for implementing or interacting with plugins in your addin code. Managed clients include the Pro application itself as well as 3rd party addins. Native code integration is restricted to the Pro application c++ tier to allow it to integrate with a feature class or table generated from the plugin data source (note: 3rd parties always interact with Pro plugins via .NET). As plugins are loaded by the native tier of Pro they do not show up in the Addin Manager tab on backstage. Consult ArcGIS Pro Plugin Registry Keys for the Plugin specific user and machine settings.

Custom plugin data sources outwardly appear as Geodatabase feature classes and tables in Pro, so they can be used as a data source for layers and stand alone tables in the application. If a project is saved that contains a layer or standalone table sourced from a plugin table, then when that project re-opens, the map or scene will automatically hydrate the underlying plugin table (from the custom plugin data source) same as it would do for any feature class or table from a core Datastore. Feature layers sourced on a plugin table can be drawn, identified, queried, symbolized, etc. same as any (feature) layer but their source (plugin) data cannot be edited.

Plugins contain a Config.xml not a Config.daml. Daml elements (buttons, tools, dockpanes, menus, property sheets, etc) cannot be added to a plugin. They are not supported. Plugins are headless and contain the custom plugin data source implementation only. Attempting to add a button, tool, dockpane, etc or other daml element to a plugin project via the SDK item templates will result in an error "Invalid ArcGIS Pro add-in project".

PluginErrorMessage.png

Within the plugin itself, only the Pro ArcGIS.Core APIs can be accessed (with regards to the Pro assemblies). Attempting to access class instances from the ArcGIS.Desktop.xxxxx extension APIs will crash the Pro application. ArcGIS.Desktop.xxxxx extension API dll references should not be added to Plugins to include the Pro SDK NuGet. Accessing a plugin (e.g. to create a layer, query to retrieve rows from the plugin, etc) requires a separate, Pro addin that instantiates the plugin via the ArcGIS.Core.Data API. Consult the Simple Point Plugin sample for a fully implemented plugin and addin that illustrate this pattern.

Display End user license agreement, EULA, with Addin installation

To display an End user license agreement (EULA) when an Add-in or a Configuration is being installed, add your EULA information to the root of the add-in .csproj or .vbproj as a EULA.rtf file. The file must be an rtf. It must be named "EULA". Set the Build Content property of the Eula.rtf to Content. When the add-in is installed using RegisterAddin.exe (as opposed to simply copying it) the EULA file will be extracted from the Add-In metadata and displayed in the Esri ArcGIS Add-In installation utility dialog. The end user will have to click the "I agree" check box in order to enable the "Install Add-In" button and proceed with your installation



Consult the Pro Concepts Advanced Topics, Display End user license agreement, EULA, with Addin installation section for further information.

Introduction to DAML (Desktop Application Markup Language)

Add-ins and configurations have declarative and active aspects. The declarative portion of an add-in and configuration is defined within a DAML file containing a collection of framework elements (mostly plug-ins) that describe the customizations in application terms. For example, an add-in might be comprised of a dock pane and a new tab with two groups and a collection of buttons. These DAML elements also describe the static aspects of the components including their captions, toolTips, images, and layout (position) details. The declarative portion also contains information needed by the framework to activate (create) the associated objects when appropriate. This portioning into declarative and programmatic aspects increases flexibility, simplifies the deployment and distribution of customizations, decreases the amount of code that needs to be written, and better leverages the just-in-time (JIT) strategy for intelligent activation and resource utilization.

The following DAML shows the beginnings of a new add-in; in this case, a new tab with a single button is added to the application.

<?xml version="1.0" encoding="utf-8"?>
<ArcGIS defaultAssembly="Acme.dll"
        defaultNamespace="Acme"
        xmlns="http://schemas.esri.com/DADF/Registry" 
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:schemaLocation="http://schemas.esri.com/DADF/Registry
		file:///C:/Program%20Files/ArcGIS/Pro/bin/ArcGIS.Desktop.Framework.xsd">

  <AddInInfo id="{3329a7d3-9f16-4642-9a70-475b421c77b5}" 
             version="1.0" desktopVersion="3.0.34791">
    <Name>Acme</Name>
    <Description>Acme Extension</Description>
    <Image>Images\AddinDesktop32.png</Image>
    <Company>Acme</Company>
    <Date>1/10/2020 10:28:50 AM, 2019</Date>
    <Subject>Mapping</Subject>
  </AddInInfo>

  <modules>
    <insertModule id="acme_MainModule" className="MainModule" 
                  autoLoad="false" caption="Acme">
      <tabs>
        <tab id="acme_MainTab" caption="Acme Tools" keytip="AT">
          <group refID="acme_mainGroup"/>
        </tab>
      </tabs>
      <groups>
        <group id="acme_mainGroup" caption="Tools" keytip="G1">
          <button refID="acme_FullExtent" size="large" />
        </group>
      </groups>
      <controls>
        <button id="acme_FullExtent" caption="FullExtent "
                className="FullExtent" loadOnClick="true"
                smallImage="Images\FullExtent16.png" 
                largeImage="Images\FullExtent32.png"
                condition="esri_mapping_mapPane"
				keytip="B1">
          <tooltip heading="Full Extent">
            Displays the current map at its full extent.<disabledText />
          </tooltip>
        </button>
      </controls>
    </insertModule>
  </modules>
</ArcGIS>

Note: DAML elements within one file can alter or remove elements in other files. For example, an add-in may simply inject a single button into a ribbon group defined by another add-in. The following example shows a button being added to a group in an existing tab in the mapping module. The button is positioned before a specific control:

<updateModule refID="esri_mapping">
  <groups>
    <updateGroup refID="esri_mapping_navigateGroup">
      <insertButton refID="acme_FullExtent" 
                    insert="before" 
                    placeWith="esri_mapping_zoomFullButton" 
                    separator="true"/>
    </updateGroup>
  </groups>
</updateModule>

All DAML elements must fall under the root ArcGIS element. The first child node is the AddInInfo element, which holds the metadata about the add-in itself. This includes a unique GUID identifier, its version, a name, and a description.

  <AddInInfo id="{3329a7d3-9f16-4642-9a70-475b421c77b5}" 
             version="1.0" desktopVersion="3.1">
    <Name>Acme</Name>
    <Description>Acme Extension</Description>
    <Image>Images\AddinDesktop32.png</Image>
    <Company>Acme</Company>
    <Date>5/28/2015 10:28:50 AM, 2015</Date>
    <Subject>Framework</Subject>
  </AddInInfo>

Customizations follow and are broken up into several main classifications including modules, categories, conditions, propertySheets, backstage, and dropHandlers. As you’ll see, most customizations appear under the modules element.

All root nodes perform one or more of three distinct actions: inserts, updates, and deletes. The type of operation is determined by the element name. For example, a new module is declared using the insertModule element; similarly, a module is updated using the updateModule element. Note, in cases where no other operation except inserts is valid, the insert prefix has been dropped.

  <modules>
    <insertModule id="acme_mainModule" caption="Acme" 
                  className="MainModule" autoLoad="false">
      ...
    </insertModule>
  </modules>

Each type of customization must be assigned a unique ID by the author. IDs are established when the element is inserted; customizations are referenced by their ID when they are being updated or deleted.

In the following excerpt, the caption of a previously inserted button is updated using the updateButton element. Note that the id attribute is used when declaring new objects, while the refID attribute is used to reference existing elements.

  <updateButton refID="esri_SubSystem_Button1" caption="New Caption"/>

Similarly, a button would be deleted using deleteButton.

  <deleteButton refID="esri_SubSystem_Button1"/>

DAML instructions (insertions, updates, and deletes) are processed from a variety of sources and combined by the framework at run time into a single in-memory representation for all customizations submitted by all parties without altering any of the original DAML files. To examine the complete DAML for each session of ArcGIS Pro add the command line option /dumpcombineddaml to ArcGISPro.exe:

  ArcGISPro.exe /dumpcombineddaml

Module Plug-in

Some customizations, such as menus, are purely declarative; their definition in DAML is all that is necessary for the framework to create and present them. Most customizations, however, have an active (code-behind) component and most of these inherit from the common base class PlugIn.

  public abstract class PlugIn : PropertyChangedBase
  {
    public string Caption { get; set; }
    public string DisabledTooltip { get; set; }
    public bool Enabled { get; set; }
    public string ID { get; }
    public object LargeImage { get; set; }
    public object SmallImage { get; set; }
    public string Tooltip { get; set; }
    public string TooltipHeading { get; set; }

    protected internal virtual void OnUpdate();
  }

Many of the methods and properties on PlugIn and its derived classes do not need to be overridden or implemented by the developer; for instance, the implementation of the Caption property—found on many plug-ins—is provided by the framework and will return whatever caption was supplied when the plug-in was declared using DAML. Only the protected virtual overrides where a specific behavior is required—such as OnClick—need be supplied by the developer.

As previously mentioned, all plug-ins require an alphanumeric identifier (ID). This ID is specified within the plug-in DAML when the plug-in is declared and is conceptually similar to the GUID used to uniquely name COM coclasses.

All plug-ins with an active component use the class and assembly attributes to connect the DAML to the managed code. The class name is the full class name, which includes the namespace. The assembly is expected to reside in the same folder as the DAML file, so do not provide a path. Note, the root ArcGIS node has the defaultNamespace and defaultAssembly attributes to mitigate unnecessarily repeating this information throughout the DAML.

<?xml version="1.0" encoding="utf-8"?>
<ArcGIS defaultAssembly="Acme.dll"
        defaultNamespace="Acme"

If your plug-in belongs to a different namespace, specify it as part of the class name. If the namespace is part of the default namespace, provide only the missing portion. In the following example, the FullExtentButton class is added to the Acme.Controls namespace; since the default name is Acme, the DAML class entry needs to read Controls.FullExtentButton.

namespace Acme.Controls
{
  sealed class FullExtentButton : Button
  {
  }
}
<button id="acme_FullExtent" caption="Full Extent" className="Controls.FullExtentButton" 
        loadOnClick="true" 
        smallImage="Images\GenericButtonBlack16.png" 
        largeImage="Images\GenericButtonBlack32.png">
</button>

The following Pro UI elements derive from Plugin:

  • Button
  • ComboBox
  • CustomControl
  • DynamicMenu
  • EditBox
  • Gallery
  • Pane
  • DockPane
  • Spinner

Modules

Modules act as the hub and central access point for their subsystem; if you need access to the functionality within a subsystem, you start with the module. Modules are singletons that are instantiated automatically by the framework when access is explicitly requested in executing code, or when the module becomes “relevant” due to a context shift. All program elements that are part of the module are explicitly declared as such; these elements include ribbon buttons, tools, galleries, combo boxes, edit boxes, palettes, and other controls, as well as application panes and docking panes. The well-defined relationship between a module and its associated components lets the application initialize, un-initialize, and remove entire subsystems as a whole.

Most of the logic at the UI level should reside in a module or a helper (non-UI) class that the module directly manages. For example, when writing a button, the button class itself should have no business logic in it; all the logic should be centralized in its parent module. Centralizing the business logic reduces the spaghetti-effect. For example, instead of several buttons each listening to a particular event, it’s better to have one module listen to the event and each button instead polls the module in OnUpdate.

Modules also support several patterns to make centralizing business logic much easier; for instance, modules automatically load whenever one of their plug-ins load. For example, in most cases, a button on the ribbon doesn’t load until it's clicked. When this happens, the button’s parent module is also loaded; similarly, when a dock pane loads, its parent module also loads.

Modules are loaded automatically when one of their constituent items is created. For example, if a button is created because a user clicked on it, its parent module will also be instantiated. Modules can also be loaded based on context; if a module specifies a condition, the framework will automatically create the module when the application state satisfies its condition. Loading a module based on a condition is helpful in scenarios where you want to load based on an event in the application but you don’t want to create an object (and load your dll) to listen for the event.

Modules also have a pattern for working with panes. Modules are automatically notified whenever one of its panes is activated, deactivated, opened, or closed. This means your module doesn’t have to listen to the framework’s ActivePaneChanged event and filter for the relevant panes; instead, modules are given direct notification that one of their panes changed.

To receive notifications, override:

  protected override void OnPaneClosing(Pane pane, CancelRoutedEventArgs e);
  protected override void OnPaneClosed(Pane pane);
  protected override void OnPaneOpened(Pane pane);
  protected override void OnPaneActivated(Pane incomingPane);
  protected override void OnPaneDeactivated(Pane outgoingPane);

Consult the ArcGIS Pro API Reference Guide for further information.

Declaring Modules in DAML

Modules are declared within the root ArcGIS element but must be further enclosed within a module's container element. The autoLoad attribute is used to control whether the module is loaded just-in-time (JIT)—the default—or automatically when the application starts. In almost all cases, autoLoad should be set to false.

  <modules>
    <insertModule id="acme_mainModule" caption="Acme" 
                  className="MainModule" autoLoad="false">
      <!--Declare additional customizations here..-->
    </insertModule>
  </modules>

If declaring a new module, all constituent plug-in declarations contained within the insertModule element are implicitly inserts, so the insert prefix on element names can be omitted (for example, insertButton becomes simply button).

Delegate Commands

Modules support DelegateCommands which are a pattern for simplifying the creation of buttons in the ribbon. Instead of writing a complete button plug-in class that inherits from the abstract button class, you can instead declare that your button is really just a static method on the module class.

In the sample below we are implementing a DelegateCommand using a static method (on the Module1 class) named OnCustomButtonClick. In the DAML, the button element's className attribute holds the id of the Module plus the name of the static method to call separated by a colon ':'.

  <insertModule id="MyAddIn_Module" className="Module1" autoLoad="false" caption="Module1">
     <tabs>...</tabs>
     <groups>...</groups>
     <controls>
        <button id="MyAddIn_Module_Button" className="MyAddIn_Module:OnCustomButtonClick" 
           caption="Button1" largeImage="Images\GenericButtonBlack32.png" 
            smallImage="Images\GenericButtonBlack16.png">
           <tooltip>Tooltip text</tooltip>
       </button>
     </controls>
  </insertModule> 

In Module1.cs, our static method OnCustomButtonClick implements the OnClick handler:

  internal class Module1 : Module {
     //Delegate command OnClick handler
     internal static void OnCustomButtonClick() {
          System.Diagnostics.Debug.WriteLine("Button clicked");
     }
   

Note that DelegateCommand “response” methods should normally be private or internal.

DelegateCommands can additionally support OnUpdate functionality via a static property that returns bool. The property must have the same name as the DelegateCommand OnClick method with the prefix Can.

In this example, the OnClick method is called OnCustomButtonClick so the property must be called **Can**OnCustomButtonClick.

  internal class Module1 : Module {
     private static bool _isEnabled = true;
  
     //Delegate command OnClick handler
     internal static void OnCustomButtonClick() {
          System.Diagnostics.Debug.WriteLine("Button clicked");
     }
  
     //Delegate command OnUpdate implementation
     internal static bool CanOnCustomButtonClick {
        get {
            //Module1 code must keep '_isEnabled' current
            return _isEnabled;
        }
     }

Configurable Extensions

Add-in developers wanting to implement proprietary licensing logic can do so by implementing a Configurable Extension. Configurable extensions were introduced in 10.x and allowed developers to add their Add-ins to the Arcmap Extensions dialog box. ArcGIS Pro provides the same framework for Pro Add-in developers.

Add-ins that implement the configurable extension pattern in Pro are shown on the licensing tab on the ArcGIS Pro application backstage as an External Extension.

Licensing_1_Backstage.png

When a user attempts to enable a configurable extension from backstage, the 3rd party developer can execute custom licensing code via IExtensionConfig (same as at 10x) to determine whether or not the enabling action is authorized.

Licensing_2_Enabled.png

Implementing a configurable extension for you Add-in is a two step process:

  1. Add an <extensionConfig .../> element to your Config.daml file within the <insertModule ...></insertModule> element.
  2. Implement IExtensionConfig on your Module class (optional, it is not required).

The <extensionConfig .../> DAML element controls the content of the licensing tab External Extension list entry for your Add-in. Product name and message can be dynamically updated (eg to indicate a status) within your IExtensionConfig implementation. Product name and message are refreshed each time the enabled checkbox (for your list entry) is clicked.

For example, the DAML shown below results in the following External Extension list entry:

Licensing_3_Extension.png

Notice that the extensionConfig hasExtendedInfo attribute is set to true. This tells Pro that the Add-in has an IExtensionConfig implementation on the Add-in Module. Failure to set the hasExtendedInfo attribute to true means that any IExtensionConfig implementation on your Add-in Module will be ignored. Add-ins with an <extensionConfig ... hasExtendedInfo="false"/> element in the DAML are listed on the licensing tab as permanently enabled (and they cannot be disabled).

Implementing IExtensionConfig is covered in detail in the ProGuide License Your Add-in.

JIT Loading

By default, Add-in modules are JIT loaded (<modules><insertModule .... autoLoad="false">...). Add-ins that are configurable extensions can also be JIT loaded and do not have to be autoloaded (i.e. autoLoad="true"). JIT loaded Add-ins implement custom enabling (or licensing) logic when they are activated. Activation will occur when:

  1. Any of your custom UI components is clicked on the UI (button, tab, etc) or shown (e.g. dockpane, pane)
  2. The licensing tab of ArcGIS Pro is opened on the backstage.

A convenient place to execute enabling or licensing logic would be in the constructor of your Add-in module. Add-in extension state can be set to one of three values:

  • Enabled: Developers should enable underlying add-in functionality.
  • Disabled: Developers should disable add-in functionality.
  • Unavailable: The extension is disabled and cannot be enabled via backstage. Developers should disable add-in functionality for the entire Pro session (that is, a restart should be required to clear an Unavailable state).

The add-in developer is responsible for determining how the corresponding extension state is propagated within the add-in. The recommended method is to use a condition to enable or disable your Add-in functionality.

For more information on how to implement a configurable extension please consult ProGuide License Your Add-in

Conditions and State

The framework incorporates a mechanism for triggering the activation of customizations based on user-defined conditions. Unlike classic events or callbacks, the binding between condition and customizations is provided statically—declaratively—using DAML. This mechanism provides a simplified and declarative means for expressing when various GUI elements such as ribbon tabs, dock panes, buttons, and tools should and shouldn’t be visible or enabled within the application. The goal here is to present an uncluttered user interface “tuned” for the activity currently at hand. Using conditions also ensures that code modules and their associated resources are loaded and consumed only when they are relevant. The use of conditions and state also simplifies coding by greatly reducing the need for complex and largely redundant event wiring associated with more traditional models.

Before discussing how context triggered activation actually works, two important terms need to be defined:

State—States are named Boolean values that symbolize a particular aspect of the application’s overall status; for example, whether a particular view is active, or whether a particular type of feature is selected. States are declared using ordinary character strings. To avoid name collisions, they are typically named using the PlugIn naming convention.

Condition—Conditions are DAML expressions composed of one or more states, such as (A or B), where both A and B are states. Conditions themselves are named so that they can be referenced by those DAML elements that permit the use of conditions; for instance, a custom ribbon tab can automatically become visible when a map view is active, and hidden when any other type of view is active.

Declaring State and Condition

State and condition are declared using DAML elements as follows:

A simple condition (consisting of only one state):

  <conditions>
    <insertCondition id="aSimpleCondition">
      <state id="someState"/>
    </insertCondition>
  </conditions>
  
  A more complex condition:
  
  <conditions>
    <insertCondition id="aMoreComplexCondition">
      <and>
        <state id="someState"/>
        <or>
          <state id="someOtherState"/>
          <state id="yetAnotherState"/>
        </or>
      </and>
    </insertCondition>
  </conditions>

The above condition evaluates to (someState AND (someOtherState OR yetAnotherState)).

Conditions are defined at the root level in the DAML file—outside the scope of any module block—since they are simply expressions (without an active aspect) and do not need to be associated with any controlling module. Conditions should be considered global in scope. The Boolean operators And, Or, and Not can be combined recursively to form complex conditional expressions if necessary, but conditions themselves cannot be used (recursively) in place of states within another condition block.

Conditions are associated with a particular plug-in using the condition attribute. The following XML fragment specifies that this tab should appear whenever the active view is a map view:

  <tab id="myTab" caption="New Tab" condition="esri_mapping_MapView"/>

State Activation

The framework defines a fixed set of activation behaviors that can be triggered using conditions. States are maintained in state tables, where state is said to be activated if the state exists in the table, and deactivated otherwise. While the application runs, the state tables within the framework are periodically monitored for changes. When a change is detected, the tables are matched against any conditions currently defined, and the appropriate activation (or deactivation) is triggered in response.

The following table summarizes the currently defined activation behaviors along with the type of Plugins they apply to:

Plugin Type Framework Provided Activation
BackStage BackStage tabs, like Controls, are disabled if they specify a condition that is not satisfied.
Controls (Buttons, Tools, etc.) Control plug-ins are enabled and disabled based on their associated condition. The Control Plugin object will not be loaded or created until the condition is initially met, and thereafter, OnUpdate will not be called unless the supplied context is currently satisfied. Note that the loadOnClick attribute is checked after the condition, so loadOnClick controls will still appear disabled if their condition hasn’t yet been satisfied.
Buttons and check boxes also have a checkedCondition whereby their checked state is automatically set based on their specified condition.
Module A module can load based on a condition. Instead of listening for an event which requires an active object, a module can automatically load when a state becomes active. Modules are not unloaded when the state deactivates.
Property Sheet Page Pages that specify a condition will not appear in a property sheet if the condition is not met.
Ribbon Tab Tab is shown or hidden based on the associated condition. When the tab first appears, other objects that appear on the tab may load if they are visible.

Implicit and Explicit State

States can be either implicit or explicit. Implicit states are those that are intrinsically defined and controlled by the framework itself; these states are activated and deactivated automatically.

The framework currently defines the following implicit states:

Implicit State Details
Active Pane The ID of the active pane is designated as an implicit state and activated when the pane is active.
Active Tab The ID of the active Tab is designated as an implicit state and activated when the tab is active.
Active Tool The ID of the active tool is designated as an implicit state and activated when the tool is active.
Module Loaded When a module is loaded, its ID is designated as an implicit state and activated. When unloaded, its ID is deactivated.

Explicit states are set manually using developer supplied code. The meanings of these states are usually defined by the developer and used to identify more specific types of context such as custom modes (“I’m editing”) or a custom status (“a raster layer is selected in the TOC”).

Explicit state changes are made by calling Activate or Deactivate on the State object:

  // Called when a raster layer is selected.
  State.Activate("esri_core_RasterLayerSelected");  
  
  // Called when editing mode is exited.
  State.Deactivate("esri_core_EditingModeExited");  

Locality of State

State tables are maintained at two levels within the framework: application level state, and pane level state. Each pane may have state that is relevant only to that instance and should not be altered if the user simply switches to another pane—for example, the current selection or current tool. For this reason, each pane instance maintains its own state table accessible via the Pane class:

  // Deactivate a state associated with a particular view.
  Pane.State.Deactivate("esri_mapping_FeatureSelected");  

Application level state contains global state relevant to the application as a whole such as the currently active view, or whether a particular module is currently loaded. Application level state is accessed via the Application class.

  // Activate a state associated the application as a whole.
  Application.State.Activate("esri_mapping_DigitizerEnabled");  

During condition matching, the framework will always consider the state associated with application level, as well as the state associated with the currently active pane. Thus, a condition will be satisfied if its expression evaluates positively on the combination of these two tables. It is important to activate or deactivate state at the appropriate level (depending on the type of state).

Consult the ConditionQuery community sample to find out more about conditions and states.

Component Categories

The framework supports a mechanism for registering components in a specific category. This mechanism relies on DAML declarations instead of registry settings. Pro contains a set of predefined categories (such as esri_editing_construction_point, esri_editing_construction_polyline, esri_editing_construction_polygon which are used in the creation of construction tools) but developers can also create their own categories.

categoryRefID

To register a DAML command (such as a button) within a Pro category use the categoryRefID attribute which should contain the id of the target category. In the example below, a tool is registering in the Editor construction tools category for polygons:

 <controls>
   <tool ... categoryRefID="esri_editing_construction_polygon" caption="Buffered Line">
     <tooltip heading="Buffered Line">
               Create a polygon with a fixed buffer.<disabledText /></tooltip>
     <content toolOptionsID="ConstructionToolWithOptions_BufferedLineToolOptions" />
   </tool>
 ...

At runtime, the editor will scan its construction tool categories (for point, polyline, polygon, etc.) and add instances of the construction tools it finds to the appropriate construction tool palettes on the create features dockpane. This use of categories allows Pro to dynamically configure various of its menus and options depending on which commands it finds registered in which categories.

Editor construction tool sample

A particular category may also allow participants in the category to provide custom data in the form of a <content .../> xml subelement. What that custom data is and its format is category dependent. Returning to our Editor construction tool example, construction tool commands can provide a toolOptionsID via the (category) "content" subelement. The Editor will launch (your) construction tool options view (assuming you provided one) using the provided toolOptionsID to lookup the view in its esri_editing_tool_options category. The DAML entry in the Config.daml looks similar to this:

 <controls>
    <controls>
   <tool ... categoryRefID="esri_editing_construction_polygon" caption="Buffered Line">
     ...
     <content toolOptionsID="ConstructionToolWithOptions_BufferedLineToolOptions" />
   </tool>
 ...
    ...
    </controls>
   </insertModule>
<modules/>
<categories>
    <!-- the options view model + view are registered in the "esri_editing_tool_options" 
         category -->
    <updateCategory refID="esri_editing_tool_options">
      <insertComponent id="ConstructionToolWithOptions_BufferedLineToolOptions" ...>
        <content className="BufferedLineToolOptionsView" />
      </insertComponent>
  ...

(The complete example can be found in the Editor Construction Tool With Options sample)

insertComponent

While commands use the categoryRefID attribute (basically most any DAML element that can be hosted on the ribbon is considered a command but the most common commands are buttons and tools), custom content that needs to register in a category uses an <insertComponent ...> element.

Note that the custom "BufferedLineToolOptions" uses an <insertComponent ...> element to register its custom UI within the esri_editing_tool_options category (BufferedLineToolOptions is using an "embeddable control" - see ProGuide Using Embeddable Controls for more information). To add components into a category add a <categories> section to the Config.daml containing an <updateCategory ...> child element identified with the id or "refID" of the category to be updated and add an <insertComponent ...> child element, one per content element to be registered.

If the content being registered consists of a "ViewModel-View" pair as is the case with embeddable controls then the <insertComponent ...> element id will identify the ViewModel. It must have a child <content...> element with a className attribute to identify the View.

<categories>
  <!-- A custom embeddable control is added to the "esri_editing_tool_options" category to
      allow the Buffered Line construction tool to show a custom options UI on the 
      create dock pane -->
    <updateCategory refID="esri_editing_tool_options">
      <!-- This is the View model -->
      <insertComponent id="ConstructionToolWithOptions_BufferedLineToolOptions" 
          className="BufferedLineToolOptionsViewModel">
        <!-- This is the View (User Control) -->
        <content className="BufferedLineToolOptionsView" />
      </insertComponent>

When the buffered line tool is activated on the create features dockpane its esri_editing_tool_options embeddable control is activated by the editor allowing users to type in a specific buffer distance:

Editor construction tool sample

Custom Categories

Developers can make their own categories in DAML and can register their own components within them. First the custom category is declared in the DAML. The category is purely declarative — it has no active portion (code behind). The following DAML fragment shows an example category declaration for a category called "ProjectContainers":

  <categories>
    <insertCategory id="ProjectContainers"/>
  </categories>

Custom components (whether generic C# classes, embeddable controls, etc.) register their type in the category within their config.daml using the <insertComponent ...> element as previously described. The requirements for what it "means" to be registered in your category, i.e. the contract, are defined by the category creator (i.e. "you"). The contract can be defined as an abstract class or an interface. In this example, the ProjectContainers category uses an abstract class, ProjectItemContainers, for the contract from which category components must derive and implement/override. The name is completely arbitrary.

//ProjectContainers components must derive from "ProjectItemContainers"
public abstract class ProjectItemContainers {
   public virtual string Foo(string name, string path, string type, string data);
}

To create a category component for ProjectContainers, we implement our custom MapContainer class and it must inherit from ProjectItemContainers. It implements (overrides) the relevant virtual methods - Foo() in this case. If an interface were being used as the contract the MapContainer class would be expected to implement the interface.

  //Derive from ProjectItemContainer 
  public sealed class MapContainer : ProjectItemContainer {
    . . .

    public override Item Foo(string name, string path, string type, string data) {
       //TODO implement Foo as required
    }

The completed MapContainer class in our add-in is registered with the category in the config.daml within a <categories> section and <insertComponent ...> element as previously described.

<categories>
  <updateCategory refID="ProjectContainers">
    <!-- our custom MapContainer class that derives from ProjectItemContainer -->
    <insertComponent id="MapContainer" className="MapContainer">
      <content type="Map" displayName="Maps"/>
    </insertComponent>
  </updateCategory>
</categories>

At run time, the creator of the ProjectContainers category enumerates all registered components using the framework provided Categories.GetComponentElements method. The components can be in the same or different add-ins. The Categories.GetComponentElements is typically called early in an add-in's lifecycle (eg within Module.Initialize) and should only be called once. Components cannot dynamically register themselves within a category so there is no point calling this method repeatedly within a session. Pass in the id to your component category as the parameter to the GetComponentElements method ("ProjectContainers" in this case). GetComponentElements returns a ArcGIS.Desktop.Framework.ComponentElement instance (API reference topic10086.html) for each entry it finds within the given category.

Test each returned component element for the presence of your contract (whether interface or abstract class or both). When a component is returned, you can also call ComponentElement.GetContent() to get a Systm.DAML.Linq.XElement for the content subelement node (if present) or use component.ReadAttribute to read an attribute value directly off the content element. Call ComponentElement.CreateComponent() to instantiate a new instance of the component (in this case an instance of our "MapContainer" class). If the CreateComponent call fails, an exception will be thrown.

  Collection<ArcGIS.Desktop.Framework.ComponentElement> components;
  components = Categories.GetComponentElements("ProjectContainers");
  
  // Check the components
  foreach (ComponentElement component in components)
  {
    //read the value of the <content type="" ...> attribute
    string value = component.ReadAttribute("type");
    if (value != key)
      continue;//category components that do not provide a "type" are skipped
  
    //ProjectItemContainers have a two stage creation mechanism
    ProjectItemContainer container = null;
    try
    {
      //test the component element for "ProjectItemContainer"
      container = component.CreateComponent() as ProjectItemContainer;
      if (container == null) {//"container" is NOT a ProjectItemContainer
        continue;//Skip component - throw exception etc
      //TODO - hold on to the instance of your ProjectItemContainer, eg in 
      //a list...

Two fully worked examples of custom categories are available in the Pro SDK Samples:

Configure Gallery shows implementation of a custom category to configure the loading of buttons and tools into a gallery on Pro's ribbon. Custom Categories shows implementation of a custom category to configure a Pro Window.

Daml Elements

Backstage

Backstage is a full-screen user interface view that exposes additional functionality for the application and current project. Backstage consists of tabs and buttons. Each tab is scoped to a particular task and presents its own user interface. Buttons are simply commands that perform an operation and have no additional user interface in the backstage.

BackstageNew.png

Backstage tabs are also contextual and will appear disabled if they specify a condition that has not been satisfied.

Like panes, dock panes, and property pages, a custom backstage tab has two components: a component class that derives from BackstageTab and a view class that derives from FrameworkElement, typically a UserControl. Backstage tabs must be defined in DAML.

  <backstage>
    <insertButton refID="acme_Save"/>
    <insertTab id="acme_Open"
                caption="Open"
                className="AcmeOpenProjectCmd"
                keytip="OO">
      <content className="AcmeOpenProjectView"/>
    </insertTab>
    <insertButton refID="acme_Exit"/>
  </backstage>

Container controls

All the ribbon controls you've seen so far have been declared under the controls element as they represent individual controls. The remaining ribbon controls are container controls, meaning they hold a collection of controls. For example, a menu is a list of controls. Each type of container control has its own section in DAML.

Gallery

A Gallery is a control that displays a collection of related items or commands on the ribbon. If there are too many items in the gallery, an expand arrow is provided to display the rest of the collection in an expanded pane. Galleries typically provide a richer representation of the choices offered, each often representing a preview of the result if chosen. Galleries can be organized to show multiple rows and columns simultaneously and are excellent choices when you don’t want to be constrained by the smaller one dimensional area offered by a menu.

The following image shows the Basemap drop-down gallery:

Basemap2.png

Galleries can present a condensed grid on the ribbon using the in-line gallery representation. The items presented in this fashion are often either the most common or most recently used items depending on the implementation. The following image shows the Layer Templates in-line gallery:

LayerTemplates2.png

The actual contents of a gallery are populated at run time. Relatively static aspects such as the caption, drop-down image, item size constraint, ToolTip, and so on, are specified declaratively. The itemSizeString is used to specify the maximum width of items displayed in the gallery.

 <gallery id="esri_mapping_basemapGallery" className="Ribbon.BasemapGalleryViewModel"
   caption="Basemap" extendedCaption="Choose basemap" keytip="BM" 
   itemsInRow="3" helpContextID="" loadingMessage="Loading..." 
   itemWidth="140" itemHeight="115" 
   dataTemplateFile=
     "pack://application:,,,/ArcGIS.Desktop.Mapping;component/Map/Ribbon/GalleryTemplates.xaml"
   templateID="BaseMapTemplate" showItemCaption="true" showItemCaptionBelow="true"
   resizable="true" condition="esri_mapping_BasemapGalleryCondition"
   largeImage="Images/Basemap32.png">
   <tooltip heading="">
    Choose a basemap for your map. The basemap is the reference data that displays under the notes and other 
    GIS data you have added to the map.<disabledText></disabledText>
   </tooltip>
 </gallery>

Gallery items are typically modeled through the GalleryItem class. GalleryItems have the following properties: Icon, LargeIcon, Text, Group, and Tooltip. Custom GalleryItems can be created through inheritance to encapsulate any additional properties and/or behavior as needed. Gallery items are represented in the UI via an ItemTemplate.

To better support a responsive UI experience, the framework provides a waiting spinner and loading message on the gallery’s drop-down list when it's trying to asynchronously load a large number of items. The LoadingMessage can be updated at run time or set statically using the loadingMessage attribute. Note, to get this default behavior, no heavy code should be put in the gallery’s constructor since it will block the UI thread and prevent the spinner from appearing. The loading message only appears in drop-down galleries (not in in-line galleries).

All items added to the ItemCollection must be created on the main UI thread as these ultimately become the content of buttons added to the gallery pop-up control.

Galleries are declared in DAML under the galleries element.

  <gallery id="acme_MapGallery" 
     className="acme_Maps"    
     caption="Maps" 
     itemsInRow="3" 
     itemWidth="140" 
     dataTemplateFile="pack://application:,,,/Acme;component/Styles/GalleryTemplates.xaml" 
     templateID="BaseMapTemplate" 
     showItemCaption="true" 
     resizable="true" 
     largeImage="Images\Basemap32.png">
    <tooltip>Choose a map.</tooltip>
    <button refID="acme_Button1"/>
    <button refID="acme_Button2"/>
  </gallery>

Galleries can also have a menu presenting additional options. This menu can contain another nested gallery.

Galleries are implemented by inheriting from the framework Gallery base class.

  sealed class MapGallery : ArcGIS.Desktop.Framework.Contracts.Gallery
  {
    private bool _initialized;
  
    protected override void OnDropDownOpened()
    {
      LoadItems();
    }
  
    private void LoadItems()
    {
      if (_initialized)
        return;
  
      for (int x = 0; x < 28; x++)
      {
        GalleryItem galleryItem = new GalleryItem("GalleryItem " + x.ToString(), 
              null, "tip: " + x.ToString());
        this.Add(galleryItem);
      }
      _initialized = true;
    }
  
    protected override void OnClick(GalleryItem item)
    {
      System.Windows.MessageBox.Show(item.Text);
    }
  }

Galleries can specify a custom template in their declaration. A simple default template is used for all galleries that do not specify one. This template assumes a collection of GalleryItems; if you're using your own item template, you can fill the collection with whatever type is appropriate. Note, when specifying a custom template, you must list the file and its key.

Given the previous example:

  <gallery id="esri_mapping_basemapGallery" ...
   dataTemplateFile="pack://application:,,,/ArcGIS.Desktop.Mapping;component/Map/Ribbon/GalleryTemplates.xaml"
   templateID="BaseMapTemplate">
       <tooltip heading="">
              ...
       </tooltip>
  </gallery>

This gallery uses a DataTemplate GalleryTemplates.xaml that has the key BaseMapTemplate. The template might look something like this:

  <!-- In GalleryTemplates.xaml -->
  
  <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
  
  <DataTemplate x:Key="BaseMapTemplate">
    <Grid>
      <StackPanel Orientation="Vertical" Margin="5,0,5,0">
        <Grid Margin="5">
          <Image Source="{Binding Thumbnail}" MaxHeight="87" MaxWidth="120">
            <Image.Effect>
              <DropShadowEffect Color="#FF565454" Opacity="0.4" />
            </Image.Effect>
          </Image>
        </Grid>
        <TextBlock Text="{Binding Title}" HorizontalAlignment="Center" />
      </StackPanel>
    </Grid>
  </DataTemplate>
  
  ...

Alternatively, you can create a public DataTemplateSelector and specify its resource key as templateID if you have more than one data template for your data. To do that, you need to implement a DataTemplateSelector class, for example:

  public class BaseMapTemplateSelector : System.Windows.Controls.DataTemplateSelector
  {
    public DataTemplate BaseMapTemplate { get; set; }
    public DataTemplate OtherMapTemplate { get; set; }

    public BaseMapTemplateSelector(){}
    ///<summary>When overridden in a derived class, returns a System.Windows.DataTemplate 
    ///based on custom logic.</summary>
    ///<returns>Returns a System.Windows.DataTemplate or null. The default value is null.
    ///</returns>    
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
      BaseMap basemap = item as BaseMap;
      if (basemap != null)
        return this.BaseMapTemplate;

      return this.OtherMapTemplate;
    }
  }

The DataTemplateSelector will specify which template to use in GalleryTemplates.xaml

  <DataTemplate x:Key="BaseMapTemplate">
    ...
  </DataTemplate>
  <DataTemplate x:Key="OtherMapTemplate">
   ...
  </DataTemplate>
  
  <testAppGallery:BaseMapTemplateSelector 
       BaseMapTemplate="{StaticResource BaseMapTemplate}"
       OtherMapTemplate="{StaticResource OtherMapTemplate}"
       x:Key="BaseMapTemplateSelector"/>
  

Now specify the resource key as the templateID: templateID="BaseMapTemplateSelector".

  <!-- DAML -->
  <gallery id="esri_mapping_basemapGallery" ... 
     dataTemplateFile="..." templateID="BaseMapTemplateSelector">
  </gallery>

Note data template or template selector can also be set in Gallery or ComboBox’s baseclass through:

  public object ItemTemplate { get; set; }

To specify tooltips for gallery items and not just the in-ribbon gallery itself, use the ActiPro ScreenTipService Attached Property in a gallery data template.

  <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:ribbon="http://schemas.actiprosoftware.com/winfx/xaml/ribbon">
  
     <DataTemplate x:Key="BaseMapTemplate">
        <Grid ... 
        ribbon:ScreenTipService.ScreenTipHeader="{Binding Name}" 
        ribbon:ScreenTipService.ScreenTipDescription="Some description">
  
       ...
       <!--<Grid.ToolTip> DO NOT DEFINE TOOLTIP IN THIS WAY
          <TextBlock Width="Auto" Text="{Binding Name}" />
       </Grid.ToolTip>-->
       ...

Menu

Menus are purely declarative and are added under the menus element. Menus can hold a variety of controls including buttons, dynamic menus, galleries, and other menus. When declared, menus are assigned an ID, a caption, and an image.
When initially defined, all menu items are simply listed within the menu element in the order they should appear.

  <menu id="acme_mainMenu"
        caption="Main Menu"
        largeImage="Images\MenuImage32.png"
        smallImage="Images\MenuImage16.png">
    <button refID="acme_paste"/>
    <button refID="acme_pasteSpecial"/>          
  </menu>

Existing menus can be easily modified using DAML. The example below adds a new menu item just after menuItem2

  <menus>
    <updateMenu refID="exampleMenu">
      <insertButton refID="menuItem2a" placeWith="menuItem2" insert="after"/>
    </updateMenu>
  </menus>  

By referencing a menu and setting the attribute inline="true", the same menu can be shared by other menus

  <menu id="changeCase" caption="Change case">
    <button refID="bold"/>
    <gallery refID="pasteGallery" inline="true"/>
    <button refID="Save"/>
  </menu>
  
  <menu id="mainMenu" caption="MainMenu">
    <menu refID="changeCase" inline="true"/>
    <button refID="ShowPeopleSheet" separator="true"/>
  </menu>
  <menu id="AnotherMenu" caption="AnotherMenu">
    <button refID="Save" separator="true" />
    <gallery refID="pasteGallery"/>
    <menu refID="changeCase" inline="true"/>
    <button refID="ShowPeopleSheet" separator="true"/>
    <dynamicMenu refID="dynoMenu"/>
  </menu>

Context Menu

A context menu is a menu that is retrieved programmatically. It is defined in the DAML using a <menu ...></menu> element. Pro context menus can be bound to XAML elements to have the Pro Framework retrieve the context menu. For example, given this context menu definition in the Config.daml:

  <!-- in the Config.daml -->
  <menu id="acme_module_ContextMenu" caption="Context Menu">
    <button refID="acme_module_Save" separator="true" />
    ...

we define a context menu property "GetMenu" in our view model

 public System.Windows.Controls.ContextMenu GetMenu {
   //Create the context menu as needed
   get { return FrameworkApplication.CreateContextMenu("acme_module_ContextMenu"); }
 }

In our WPF UI (eg a user control on a dock pane) we bind the view model property to the ContextMenu of a WPF element (a stackpanel in this case):

<StackPanel Orientation="Horizontal" ContextMenu="{Binding GetMenu}">

Now, whenever the context menu is requested via the UI (e.g. right click on this element), the context menu defined in our config.daml will be shown. Having the framework create a ContextMenu instead of declaring one inline in XAML, means it can be easily extended with DAML. Consult FrameworkElement.ContextMenu Property in the MSDN for more information on ContextMenu.

Context Menu Item Keytip Access

A shortcut key can be assigned to an item on context menu through DAML using the "keytip" attribute. It is usually one of the letters of the menu item's caption. When the context menu pops up, the first matching letter of the caption will be underlined. When the key is pressed, the corresponding command will be invoked. Note that collision should be avoided when defining keytips for items on the same context menu.

KeyTipShortcut.png

<menus>
  <menu id="exampleMenu" caption="SomeMenu" >
    <button refID="menuItem1"/>
    <button refID="menuItem2"/>
    <button refID="menuItem3"/>
  <menu/>
</menus>

<button id="menuItem1" caption="Example" className="SomeClass" keytip="E"/>

Retrieving Context

Context menus can access context ("what is selected") from the Catalog Pane, Catalog View, and the Contents pane (a.k.a "TOC"). The Contents pane provides context only for maps and scenes. Layout context is not currently supported.

ContextPanes.png

To get the catalog context, first retrieve the active window from the framework application. This will be whichever dockpane or view pane currently has focus. Cast the window to IProjectWindow (using "as"). If the active window is either the Catalog Pane or a Catalog View, the cast will succeed otherwise the window will be null.

var window = FrameworkApplication.ActiveWindow as ArcGIS.Desktop.Core.IProjectWindow;

What is currently selected can be accessed off the IProjectWindow.SelectedItems member. 0 or more items can be selected. Note: If the item(s) is a project content item, it can be further cast to the relevant "ProjectItem" class (i.e. "project items" like map, style, layout, scene, toolbox, etc.). Refer to ProConcepts Content and Items.

 //get the currently selected item(s) in the catalog pane (note: more
 //than one item can be selected) window will be null if the ActiveWindow is not the 
 //Catalog dock pane or a Catalog view.
 var window = FrameworkApplication.ActiveWindow as ArcGIS.Desktop.Core.IProjectWindow;
 var item = window?.SelectedItems.First();

To get the context off the map or scene TOC, use the MapView.Active.GetSelectedLayers() member to get the list of currently selected layers and MapView.Active.GetSelectedStandaloneTables() to get the currently selected stand-alone tables.

Palette (button and tool)

Tool palettes are ostensibly menus, but designed specifically for grouping a set of related tools together. When a tool is selected from the palette of tools, it becomes the active tool and is displayed as the new palette button. Like split buttons, palettes are purely declarative, only the actual buttons and tools have an active portion.

  <toolPalettes>
    <toolPalette id="examplePalette" caption="Example">
      <tool refID="tool1"/>
      <tool refID="tool2"/>
    </toolPalette>
  </toolPalettes> 

The first tool listed in the palette is the initial tool displayed in the panel.

Split button

Split buttons group related buttons together. The associated group is displayed when the arrow portion of the split button is clicked. The first button listed should be the most commonly used button in the collection, and it is presented directly on the ribbon. The remaining buttons are used less frequently and are added to a drop-down menu. Split buttons are purely declarative, they have no associated action portion. Each button participating in the control has an active portion but not the split button itself.

Paste.png

  <splitButtons>
    <splitButton id="acme_pasteSplitBtn">
      <button refID="acme_paste" />
      <button refID="acme_pasteSpecial" />
      <button refID="acme_pasteWithFormatting" />
    </splitButton>
  </splitButtons>

Instead of a collection of less commonly used buttons, split buttons can instead present a gallery. The first element is still a button. As you’ll see later, galleries can additionally present their own menu of buttons.

gallery1.png

  <splitButtons>
    <splitButton id="acme_pasteSplitBtn">
      <button refID="acme_paste" />
      <gallery refID="acme_pasteGallery" />
    </splitButton>
  </splitButtons>

Controls

Overview

Controls include any of the simple widgets that can appear on the application ribbon, including Button, Tool, CheckBox, ComboBox, EditBox, LabelControl, DynamicMenu, and CustomControl. All simple ribbon controls are declared in a module’s controls element. Although controls are declared in a controls section, they are referenced when defining a group. This allows the same control to appear in multiple groups without having to re-declare the entire control.

The following declaration includes a few of the most common attributes associated with buttons:

  <insertModule id="acme_MainModule" className="AcmeMain" caption="Acme">
    <controls>
      <button id="acme_AddFromFileButton"
              className=" AddFromFile"
              caption="From File"
              largeImage="Images\AddFromFile32.png"
              smallImage="Images\AddFromFile16.pngg">
        <tooltip heading="Add from file" image="Images\AddFromFile16.png">
          Add spatial data files to the project
          <disabledText>Requires an open project with a map</disabledText>
        </tooltip>
      </button>
    </controls>
  </insertModule>

As previously stated, framework plug-ins are not permitted to exist untethered and must be linked with a module. In the case illustrated above, a new button—made by ACME—is declared and added to a module. This logically links the button to the ACME Module. If at some point, the framework determines that the module should be unloaded, the newly added button will also be unloaded.

Existing controls can also be updated or deleted using a plug-in configuration file, but not all attributes can be updated. Those that might alter the runtime behavior of the plug-in (such as class, assembly, and condition) can only be set through an insert.

The following example updates the caption on the button added in the previous example:

  <updateModule refID="acme_MainModule">
    <controls>
      <updateButton refID=" acme_mapping_AddFromFileButton" caption="New Caption"/>
    </controls>
  </updateModule>

All control elements share several attributes. The loadOnClick attribute determines when the button should be created by the framework. By default, controls appear enabled but are not actually instantiated until they are clicked. This simple JIT strategy improves resource utilization and startup time by deferring the instantiation of controls until they are initiated by the end user. Non-visible controls are never loaded until they become visible (or are executed programmatically), regardless of the value assigned to loadOnClick.

ToolTips are defined using the ToolTip sub-element and can span as many lines as necessary. The image attribute is used to supply an image that will appear next to the tip text. Command ToolTips also support a disabledText element; this string becomes the ToolTip whenever the command is disabled.

Most controls support multiple sizes in the ribbon. For example, a button can render small (small icon only), middle (small icon with text), and large (large icon over text). Use the smallImage and largeImage attributes to specify unique images for the different sizes. Images don’t have to be graphics, you can also use XAML. Note, images are not flipped when running right to left; if the image should flip when running in this mode (for example, arrow buttons), set the flipImageRTL attribute to true. You can also use overlayLargeImage and overlaySmallImage to draw a graphic or XAML overtop of the corresponding images.

The following image shows three buttons (in red squares) in the three supported sizes:

Ribbon2.png

 <button refID="esri_mapping_zoomFullButton" size="small" />
 <button refID="esri_core_editCutButton" size="middle" />
 <button refID="esri_geoprocessing_selectByAttributeButton" size="large"/>

The disableIfBusy element is used to signal that the control should be disabled whenever the primary worker thread is busy. This prevents work from queuing up. This element is true by default. Controls that always need to be enabled should set this to false.

All control declarations support a condition attribute allowing the assignment of a condition. If the specified condition isn’t met, the control will be automatically disabled by the framework. In addition, controls remain unloaded until their condition is met. If no condition is specified, the control is assumed to be always relevant. See the Conditions and state section for more information.

Controls are implemented by inheriting from the appropriate plug-in derived base class.

The Button class shown below is typical of controls: The virtual OnClick is overridden by the leaf class and used to perform whatever custom behavior is desired.

  sealed class ShowPeopleSheet : ArcGIS.Desktop.Framework.Contracts.Button
  {
    protected override void OnClick()
    {
      User user = new User();
      user.FirstName = "Michael";
      user.LastName = "Faraday";
      PropertySheet.Show("UserManager", null, user);
    }
  }

Controls can update their properties—for example, caption and ToolTip—at run time, and these changes will automatically reflect in the application.

  protected override void OnClick()
  {
    this.Caption = "New Caption";
    this.Tooltip = "New Tooltip";
    this.Checked = true;
  }

All controls can be accessed and updated at run time using the FrameworkApplication.GetPlugInWrapper function. The Checked property is used by buttons and checkboxes to show a checked appearance.

  protected override void OnClick()
  {
    IPlugInWrapper wrapper = FrameworkApplication.GetPlugInWrapper("acme_ZoomBtn");
    wrapper.Caption = "New Caption";
    wrapper.Tooltip = "New Tooltip";
  }

To support accessibility, an appropriate keytip should be supplied for each control. The character or characters chosen should make sense and should not conflict with any existing keytips (for controls on the same ribbon); keytips can consist of multiple characters to disambiguate if necessary.

Extended Captions

Many captions are ambiguous and can be duplicated many times; e.g. FORMAT. To resolve this, use the extendedCaption attribute to provide more details. The extendedCaption appears alongside the caption in parentheses in the Customize the Ribbon and Task dialogs.

  <tab id="esri_layouts_polygonFormatTab" caption="FORMAT" extendedCaption="polygons" 
              condition="esri_layouts_polygonCondition" 
              tabGroupID="esri_layouts_polygonTabGroup">
      <group refID="esri_layouts_polygonFormatGroup"/>
      <group refID="esri_layouts_textFormatGroup"/>
      <group refID="esri_layouts_sizeAndPositionGroup"/>
  </tab>

Button

Buttons are simple controls that respond to an OnClick event.

  <button id="acme_AddFromFileButton"
      className=" AddFromFile"
      caption="Add from file"
      keytip="AF"
      largeImage="Images\AddFromFile32.png"
      smallImage="Images\AddFromFile16.pngg">
    <tooltip heading="Add from file" image="Images\AddFromFile16.png">
      Add spatial data files to the project
      <disabledText>Requires an open project with a map</disabledText>
    </tooltip>
  </button>

Buttons are implemented by deriving from the Button base class.

  sealed class AddFromFile : ArcGIS.Desktop.Framework.Contracts.Button
  {
    protected override void OnClick()
    {
      base.OnClick();
    }
  }

You can also use the overlayLargeImage and overlaySmallImage DAML attributes to draw a graphic or XAML overtop of the corresponding button images. If the image should flip when running right-to-left, e.g. arrow buttons, set the flipImageRTL attribute to true.

Check box

Check boxes work exactly the same as buttons. They are defined using a checkbox tag in the DAML and derive from the CheckBox base class. Use the IsChecked property to give a checked appearance.

  <checkBox id="acme_AutoSave"
            className="AutoSave"
            caption="Auto Save"
            keytip="AS">
    <tooltip heading="Auto Save" image="Images\AutoSave16.png">Automatically save edits
    </tooltip>
  </checkBox
  internal class AutoSave: ArcGIS.Desktop.Framework.Contracts.CheckBox
  {
    public AutoSave()
    {
      IsChecked = true;
    }

    protected override void OnClick()
    {
      // TODO - add specific customization here as necessary
    }
  }

Combo box

The combo box allows users to select an item from a drop-down list or optionally to enter new text in the text box of the control. ComboBox typically contains a collection of ComboBoxItem objects but can contain a collection of objects of any type (such as string or image).

The IsEditable and IsReadOnly attributes specify how the ComboBox behaves.

IsReadOnly true IsReadOnly false
IsEditable true Cannot select an item in the ComboBox by entering a string. Can select an item in the ComboBox by entering a string.
Cannot enter a string that does not correspond to an item in the ComboBox. Can enter a string that does not correspond to an item in the ComboBox.
Can select part of the string in the ComboBox text box. Can select part of the string in the ComboBox text box.
Can copy the string in the ComboBox text box but cannot paste a string into the ComboBox text box. Can copy or paste the string in the ComboBox text box.
IsEditable false Can select an item in the ComboBox by entering a string. Can select an item in the ComboBox by entering a string.
Cannot enter a string that does not correspond to an item in the ComboBox. Cannot enter a string that does not correspond to an item in the ComboBox.
Cannot select part of the string in the ComboBox. Cannot select part of the string in the ComboBox.
Cannot copy or paste the string in the ComboBox. Cannot copy or paste the string in the ComboBox.

In most cases, combo boxes are filled with ComboBoxItem objects, but you can fill them with any object you’d like. When custom items are added to a combo box, a custom item template is usually used.

  <comboBox id="acme_ComboBox1" 
            caption="ComboBox 1" 
            className="ComboBox1" 
            itemWidth="140" 
            isEditable="false"
            isReadOnly="true" 
            resizable="true">
    <tooltip heading="Tooltip Heading">Tooltip text<disabledText /></tooltip>
  </comboBox>

Combo boxes are implemented by deriving from the ComboBox base class.

  sealed class AcmeCombo : ArcGIS.Desktop.Framework.Contracts.ComboBox
  {
    public AcmeCombo()
    {
      FillCombo();
    }
  
    private void FillCombo()
    {
      // Add 6 items to the combobox
      for (int i = 0; i < 6; i++)
      {
        string name = string.Format("Item {0}", i);
        Add(new ComboBoxItem(name));
      }
  
      // Select first item
      SelectedItem = ItemCollection.FirstOrDefault();
    }
  
    protected override void OnSelectionChange(ComboBoxItem item)
    {
      // Do work    
    }
  }

Custom Control

To create a custom control, write a general WPF System.Windows.Controls.UserControl and associate it with a customControl tag in the Config.DAML. For example:

  <customControl id="Module1_CustomControl1" caption="A Custom Control" 
                 className="CustomControlViewModel" loadOnClick="false">
            <content className="CustomControlView"/>
            <tooltip heading="">Illustrates a Custom Control</tooltip>
  </customControl>

and in the Add-in add the custom control ViewModel and View that are referenced in the DAML (CustomControlViewModel and CustomControlView in the above example). For example, this would be the definition of the CustomControlView xaml:

  <UserControl x:Class="CustomControlView"
               xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
               xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
               xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
               xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
               mc:Ignorable="d" 
               d:DesignHeight="68" d:DesignWidth="90">
      <Grid>
          ...
      </Grid>
  </UserControl>

Custom controls support an isDropDown attribute in DAML which signals the framework to draw the custom control as the pop-up window part of a drop down button. This is very similar to a drop down gallery except the author has complete control over the pop-up UI. Custom control should have a height of 22 pixels for the small group item size and 68 pixels for the large group item size.

Dynamic menu

Unlike regular menus, which are purely declarative (defined only in DAML), dynamic menus are populated at run time. Derived classes must add items in their OnPopup override, which is invoked before the menu opens. Dynamic menus can contain simple items consisting of a caption and image, or references to existing DAML controls including other dynamic menus. The OnClick override is invoked with the specified index when an item is clicked. All menu items are cleared after the pop-up closes.

Dynamic menus can reside directly on the ribbon or on other menus. If the dynamic menu is on a menu and the inline attribute is set to true, the items will be added directly onto the hosting menu instead of being added to a pull-right submenu.

  <dynamicMenu caption="Acme Menu"
                className="DynoMenu"
                id="acme_dynoMenu"
                largeImage="Images\MenuImage32.png"
                smallImage="Images\MenuImage16.png" >
    <tooltip heading="Heading" image="Images\MenuImage16.png">Some text</tooltip>
  </dynamicMenu>

Dynamic menus are implemented by deriving from the DynamicMenu base class. The following example shows three types of buttons being added: a delegate menu item, a regular menu item, and several DAML buttons:

  sealed class DynoMenu : ArcGIS.Desktop.Framework.Contracts.DynamicMenu
  {
    internal delegate void MathAction(double number);
  
    protected override void OnPopup()
    {
      double angle = 28.0;
  
      MathAction action = Test;
  
      this.Add("item 1", @"pack://application:,,,/Images/Image1.png", true, true, 
               false, action, angle);
      this.Add("item 2", @"pack://application:,,,/Images/Image2.png");
      this.AddSeparator();
      this.AddReference("acme_basicButton");
      this.AddReference("acme_basicMenu");
      this.AddReference("acme_basicGallery");
    }
  
    protected override void OnClick(int index)
    {
      switch (index)
      {
        case 1:
          System.Windows.MessageBox.Show("2nd item");
          break;
      }
    }
  
    private void Test(double angle)
    {
      System.Windows.MessageBox.Show(angle.ToString());
    }
  }

Edit box

Edit boxes provide a convenient means for users to enter text within a control on a ribbon tab. The edit box can be configured to appear with or without a caption. The sizeString attribute is used to establish the width of the control through a string that is representative of the kind of input that will appear in the control; the edit hint can also be updated at run time. Use the DataType attribute (string, double, or int) to specify the type of edit box and the format attribute to control the representation of numeric values.

  <editBox id="acme_Currency" caption="Currency:"
           className="CurrencyEditBox"
           dataType="double"
           format="C" keytip="CC" 
           sizeString="$9.99"/>

Edit box controls are implemented by deriving from the EditBox base class.

  sealed class CurrencyEditBox : ArcGIS.Desktop.Framework.Contracts.EditBox
  {
    protected override void OnEnter()
    {
      // Do work
    }
  }

C# format examples:

  decimal value = 123.456m;
  Console.WriteLine(value.ToString("C2"));
  // Displays $123.46

  value = -12345;
  Console.WriteLine(value.ToString("D"));
  // Displays -12345
  Console.WriteLine(value.ToString("D8"));
  // Displays -00012345

LabelControl

If you just want to add a string to the ribbon, you can use LabelControl.

  <labelControl id="acme_label" 
                caption="the quick brown fox jumps over the lazy dog" 
                hasTwoLines="true"/>

AddinTab2.png

Spinner

The Spinner control is like an edit box, except it additionally provides increment and decrement buttons and supports a range established with minimum and maximum attributes. You can also specify default and increment values. Use the format attribute to control how the double is displayed. For example, use "C" to present a currency or F4 for a double with four decimals. In addition, use the suffix attribute to tag on a trailing string such as a percent sign (%).

Spinners do not support delay loading with loadOnClick, they are instantiated when they become visible.

  <spinner id="acme_spinner" 
           caption="Spin" 
           className="RotateSpinner" 
           format="F1" 
           defaultValue="45"
           maximum="90" 
           minimum="0"
           increment="1" 
           keytip="dz" 
           suffix="&#176;">
    <tooltip image="Images\Angle16.png" heading="Rotate">Specify rotation angle
      <disabledText>No item selected.</disabledText>
    </tooltip>
  </spinner>

Spinner.png

Spinner controls are implemented by deriving from the Spinner base class.

  sealed class RotateSpinner : ArcGIS.Desktop.Framework.Contracts.Spinner
  {
    protected override void OnValueChanged(double? value)
    {
      // Do work
    }
  }

Tool

Tools look very similar to buttons, except they automatically stay checked after being selected. Tools also work in conjunction with the current pane.

  <tool id="acme_zoomTool"
        caption="Zoom"
        className="ZoomTool"
        keytip="ZT">
    <tooltip heading="Zoom" image="Images\Zoom16.png">Zoom the map.
      <disabledText>No active map.</disabledText>
    </tooltip>
  </tool>

Tools are implemented by deriving from the Tool base class. Tools can opt to override the mouse and keyboard events if need be. In this example, the Tool wants to receive the OnMouseUp event so it must first signal to Pro that it is handling the OnMouseDown otherwise Pro will consume the message.

  sealed class ZoomTool : ArcGIS.Desktop.Framework.Contracts.Tool
  {
    protected override void OnMouseDown(MouseButtonEventArgs e)
    {
      // Do work - set handled to true to receive mouse messages
      e.Handled = true;
    }
  
    protected override void OnMouseUp(MouseButtonEventArgs e)
    {
      switch (e.ChangedButton)
      {
        case MouseButton.Right:
          e.Handled = true;
          ContextMenu menu = FrameworkApplication.CreateContextMenu("acme_AddDataMenu");
          menu.IsOpen = true;
          break;
        // etc …
        }
    }
  }

A default tool can be assigned when declaring a pane; this tool will be automatically selected when an instance of the pane is created. When working with multiple panes, the active tool may change automatically to reflect the incoming pane. When returning to the original pane, the system will try to re-activate the most recently used tool for the pane type.

Dock pane

Dock panes are modeless dialog boxes that can be docked at the top, right, left, or bottom of the view area within the application. Dock panes can also be grouped with other dock panes and docked relative to each other (that is, below, above, and so on) and can also be un-docked and floated. Finally, dock panes support being pinned and un-pinned so that they slide back into the application frame to save space. The application framework persists and preserves the docking state of each dock pane so that when shown in subsequent sessions, they appear in the same position.

Dock panes are singletons—there is never more than one instance of a particular dock pane—and once created, they are not destroyed until the application is closed.

Dock panes are defined in the same fashion as panes. When the framework creates a dock pane, it will also create its content class and set the content’s data context to the dock pane. This automatic binding allows you to use the MVVM pattern when developing panes and dock panes. For example, a well-designed dock pane will have as little logic as possible in its XAML code behind; instead, all of the controls in the XAML should be bound to properties in the dock pane base class. The dock pane and pane bases classes can be regarded as view models.

  <dockPanes>
    <dockPane id="acme_TOCDockPane" 
              caption="Contents" 
              className="TOCDockPaneViewModel" 
              dock="group"  
              dockWith="esri_core_ProjectDockPane">
      <content className="TOCDockPaneView"/>
    </dockPane>
  </dockPanes>

Dock panes can be declared so that they are positioned relative to other dock panes using the dockWith attribute. In the example above, the dock pane declares that the first time it appears it should be grouped with the esri_coreProjectDockPane dock pane. Note, from here on, every time this dock pane is presented, its location will come from its persisted dock state to support user relocation.

All dock panes must derive from the DockPane base class which in turn inherits from the PlugIn base class. Dock panes are managed by the DockPaneManager class available via FrameworkApplication.Dockpanes. Dock panes are found rather than created since they are logically singletons. Use FrameworkApplication.DockPanes.Find to find and create dock panes.

  class TOCDockPaneViewModel : ArcGIS.Desktop.Framework.Contracts.DockPane
  {
    protected override Task InitializeAsync()
    {
      // Initialize instance...
      return Task.FromResult(0);
    }
  
    protected override Task UninitializeAsync()
    {
      // Uninitialize instance...
      return Task.FromResult(0);
    }
  
    protected override void OnActivate(bool isActive)
    {
      // Called when activated/deactivated
    }
  }

Pane

The framework supports multiple panes, allowing users to display and interact with multiple subjects. Only one pane can be active at a time. The active pane establishes what is available on the ribbon; switching between different panes can result in changes to what tabs and controls are available. The active pane represents what the user is working with at any given time and therefore provides the primary context for the application. You can open many panes at the same time, and they can be grouped, tiled (horizontally or vertically), or floated - but only interactively. Panes cannot be placed programmatically.

DockPane2.png

Panes are declared within a panes collection element:

  <panes>
    <pane id="acme_MapPane" 
          caption="Map" 
          className="MapPane" 
          smallImage="Images\Map16.png" 
          loadingMessage="Initialize..."
          defaultTab="acme_MapTab" 
          defaultTool="acme_ZoomTool">
      <content className="MapPaneView"/>
    </pane>
  </panes>

Panes can be associated with a default tab, and/or a default tool, so that if no other relevant tool or tab is already selected, the default will automatically be selected when the pane is activated.

Panes are composed of three parts: a DAML definition, a controller, and a view. The controller is similar to what you’ve seen with custom controls and must inherit from the framework Pane base class. The view component is the custom appearance of the control, and this class typically derives from a WPF UserControl. The DAML className attribute defines the controller, and the content element defines the view. When the pane in instantiated, the controller is set as the data context of the view. This allows for a view/view-model relationship and all the benefits that accompany this pattern.

  class MapPane : ArcGIS.Desktop.Framework.Contracts.Pane
  {
    protected override Task InitializeAsync()
    {
      // Initialize instance...
      return Task.FromResult(0);
    }
  
    protected override Task UninitializeAsync()
    {
      // Uninitialize instance...
      return Task.FromResult(0);
    }
  
    protected override void OnActivate(bool isActive)
    {
      // Called when activated/deactivated
    }
  }

Panes are managed by the PaneCollection class, which is available via FrameworkApplication.Panes. Panes are created programmatically using the PaneCollection.Create method. This method has an additional overload that allows the caller to pass user defined data; the appropriate constructor will be called on the pane derived class depending on which version of Create is called. After the pane’s constructor is called, the pane’s InitializeAsync function is called to give it a chance to initialize asynchronously if necessary. Similarly, when a pane is closed, its UninitializeAsync function is called. The PaneCollection class also holds the ActivePane and has methods for finding and activating existing panes.

Each pane instance has an associated instance identifier InstanceID that allows users to differentiate between multiple instances of the same pane. Panes can further override ContentID to provide a custom identifier. ContentID correlates to the id defined on the <pane ...></pane> element in the DAML.

Panes directly receive mouse and keyboard events; if not handled, the events are forwarded to the current tool if active.

As you’ll see in more detail in the Undo/Redo section, Panes and DockPanes play a critical role in governing application operations.

Pane Captions with Count

In certain cases, it proves helpful for a pane's caption to reflect the count of other panes of the same type that are currently opened (e.g., 1 of 2). For instance, if multiple instances of the same pane is opened, updating the captions to display "1/2" and "2/2", etc allows users to easily identify and differentiate between the open instances. To accomplish this, set the GroupID property of the Pane to a unique value. The application keeps track of the number of Panes opened with the same GroupID and this count is presented in the Pane captions; for example, 'MyMap (1/2)' and 'MyMap (2/2)'. Refer to CustomPaneWithContents Sample for a complete implementation that demonstrates this.

var customPane = FrameworkApplication.Panes.Create(...);
customPane.GroupID = "My Custom Pane";

PaneNumbering.png

Property sheet and page

Property sheets hold a collection of individual unrelated property pages. Each page contains controls for setting a group of related properties. A property sheet can be shown modal or modeless.

Layerproperties2.png

By default Property Sheets have their own unique size and position, but by setting the instanceID attribute one can group Property Sheets to share the same size and position.

Property sheets are purely declarative; they are defined only in DAML and have no corresponding managed class. Property pages, like panes and dock panes, have both declarative and active components; the active portion of all property pages must derive from the ArcGIS.Desktop.Framework.Contracts.Page class. The view class typically derives from the WPF UserControl.

  <propertySheets>
    <insertSheet id="acme_mainSheet"
                  caption="Options">
      <page id="acme_mainOptions"
            caption="Settings"
            className="SettingsPage">
        <content className="SettingsPageView"/>
      </page>
              
    </insertSheet>      
  </propertySheets>

Property pages only load when they become visible. They are not loaded when the sheet loads—the page has to be visited. When a page does load, its Page.IsLoading property is first set to true. Next, its Page.InitializeAsync function is executed. When the returned task completes, its IsLoading property is reset to false.

Property pages have the option of being conditional. If a condition has been specified for the page in DAML, the page will not appear with the sheet unless its condition is currently satisfied.

Pages can also be grouped within a sheet by setting the sheet's hasGroups attribute to true and specifying a group heading by setting the page's group attribute.

When a property sheet is displayed with either PropertySheet.Show or PropertySheet.ShowDialog, data can be passed in as one of the arguments, and all of its pages can access the data using their Page.Data property.

The property sheet dialog box includes a title bar and several buttons: OK, Cancel, and Apply. The Apply button is only presented when the sheet is modeless. The OK button remains enabled as long as all pages report that they are valid (Page.IsValid). The Apply button enables once any page in the sheet sets its Page.IsModified property to true and all page are valid. Once Apply or OK are clicked, each page that has set its IsModified flag to true will have its CommitAsync invoked. Similarly, if the Cancel button is clicked, CancelAsync is invoked. Property pages can optionally show a “Reset Page” button by overriding their CanReset property to return true. When the page is modified, the Reset Page button will be enabled. Clicking on it invokes their override function ResetAsync() which should reset the page to defaults.

Finally, when a property sheet closes, each loaded page will have its Page.Uninitialize invoked where it can perform any necessary cleanup.

Tab and group

You’ll note that declaring a control does not establish where and how the control will actually appear. The location of controls on the ribbon, including their size and relative placement, is established using the group and tab elements.

ArcGIS Pro uses the ribbon paradigm popularized by newer versions of Microsoft Office. The ribbon consists of a single fixed toolbar containing one or more tabs. The number of available (visible) tabs can vary dynamically depending on the state of the application. Tabs are activated through user interaction (clicks), or when directed by code running within the application.

Each tab is composed of one or more groups, small rectangular regions having a caption and containing one or more controls. The representation of controls within groups varies depending on how frequently the control is expected to be used, and it is configured declaratively in the DAML. Frequently used controls should be large and obvious, while less frequently used tools should be smaller. In addition, controls are typically paired with a caption to make their function more obvious.

Tabs2.png

Groups can optionally support a “dialog launcher” button—a small link widget located next to the group caption—which is used to bring up a dialog box where more obscure functions can be accessed. This is specified using the launcherButtonID attribute. Groups are declared as lists of controls within a groups container element.

 <groups>
   <group id="esri_mapping_navigateGroup" caption="Navigate" 
           launcherButtonID="esri_mapping_navigationOptionsButton" 
           smallImage="Images/3DNavigationTool16.png" 
           launcherKeytip="NG">
     <tool refID="esri_mapping_exploreSplitButton" size="large"/>
     <button refID="esri_mapping_zoomFullButton" size="small"/>
     <button refID="esri_mapping_fixedZoomInButton" size="small"/>
     <button refID="esri_mapping_prevExtentButton" size="small"/>
     <button refID="esri_mapping_zoomToSelectionButton" size="small"/>
     <button refID="esri_mapping_fixedZoomOutButton" size="small"/>
     <button refID="esri_mapping_nextExtentButton" size="small"/>
     <gallery refID="esri_mapping_bookmarksNavigateGallery" inline="false" size="large"/>
   </group>  
   <group id="esri_mapping_labelingLayerGroup" caption="Layer"
         smallImage="Images/LabelingRibbonLayer16.png" keytip="ZL">
     <button refID="esri_mapping_labelLayerEnableButton" size="large"/>
   </group>
    <group id="esri_mapping_labelingGroup" caption="Labeling"
           smallImage="Images/MapRibbonLabeling16.png" keytip="ZL">
      <button refID="esri_mapping_labelPauseButton" size="middle" />
      <button refID="esri_mapping_labelViewUnplacedButton" size="middle"/>
      <menu refID="esri_mapping_labelingOptionsMenu" size="middle"/>         
    </group>
 </groups>

In the example above, several groups are declared, and each group lists its contents. Note that the elements refer to previously declared controls. Notice how buttons, for example, specify their image in their declaration under controls, but their size is specified in the group. This is because the same control can appear differently in more than one group (large in group A, but small in group B). Use a label control to provide additional textual information for a group; use a separator to segregate multiple controls in the same group.

The visibility of the group can be controlled via its "condition" attribute. If the associated condition is true then the group will be visible (on a tab or menu when the tab or menu is visible). If the condition is false then the group will not be visible (when the tab or menu is visible). Using conditions on groups, therefore, it is possible to toggle group visibility in response to different actions or events within the Pro session.

The group element has an "appearsOnAddInTab" attribute. If this attribute is set to "true" all controls defined within that group will be placed in the ADD-IN tab. This is a quick method to get your controls onto the Pro ribbon without the need to define a new Tab. The code snippet below creates a "Dynamic Menu" group with one "Tool" on the ADD-IN tab.

  <group id="DynamicMenu_Group1" caption="Dynamic Menu" appearsOnAddInTab="true">
    <!-- host controls within groups -->
    <tool refID="DynamicMenu_FeatureSelectionDynamic" size="large" />
  </group>

Like controls, existing groups can be modified in DAML. The following example inserts a new button into an existing group and removes a button:

  <updateModule refID="esri_mapping">
    <groups>
      <updateGroup refID="esri_mapping_navigateGroup">
        <insertButton refID="acme_FullExtent" 
                      insert="before" 
                      placeWith="esri_mapping_zoomFullButton" 
                      separator="true"/>
        <deleteButton refID="esri_mapping_fixedZoomOutButton"/>
      </updateGroup>
    </groups>
  </updateModule>

Once a group is declared, it can be referenced and placed on tabs. Tabs are declared as lists of groups within a tab's collection element. In the following example, the previously declared groups are added to a new tab with an id “esri_mapping_homeTab”:

  <tabs>
    <tab id="esri_mapping_homeTab" 
         caption="MAP" 
         condition="esri_mapping_pane_core" 
         keytip="M">
      <group refID="esri_core_clipboardGroup" />
      <group refID="esri_mapping_navigateGroup" />
      <group refID="esri_mapping_mapGroup" />
      <group refID="esri_mapping_selectionGroup" />
      <group refID="esri_mapping_inquiryGroup" />
      <group refID="esri_mapping_labelingGroup" />
    </tab>
  </tabs>

Like controls, a tab’s relevance can be governed using a condition. The condition attribute is used to control whether or not the tab is visible, and thus available. Conditions are not used to control tab activation, only tab availability. If no condition is specified, the tab will always be visible.

Collections of related tabs can be grouped together to improve clarity. These collections are called tab groups. On the ribbon displayed below, there is a tab group containing three tabs:

TabGroup.png

Tab groups are declared within a tabGroups collection element and consist of an id. Tabs are associated with a tab group using the tabGroupID attribute. Previously at 2x the tab groups supported a caption and a background color, now they have a background color of Esri_White.

  <tabGroups>
    <tabGroup id="esri_mapping_featureLayerTabGroup">
    </tabGroup>
  </tabGroups>
  
  <tab id="esri_mapping_featureLayerAppearanceTab" 
       caption="APPEARANCE" 
       condition="esri_mapping_onlyFeatureLayersSelectedCondition" 
       tabGroupID="esri_mapping_featureLayerTabGroup" 
       keytip="JA">
    <group refID="esri_mapping_layerScaleVisibilityGroup"/>
    <group refID="esri_mapping_layerEffectsGroup" />
    <group refID="esri_mapping_layerSymbology" />
    <group refID="esri_mapping_layerExtrusion" />
    <group refID="esri_mapping_layer3DGroup" />
  </tab>

Tab groups are typically used in situations where the user temporarily enters a mode, such as “editing graphic elements.” Tab groups are not usually used with tabs that are perpetually visible (global tabs). Tabs in a tab group should have the same condition. If any tab in a tab group has a relevant condition, the entire tab group is displayed.

Tab Conditions

Like controls, a tab’s relevance can be governed using a condition. The condition attribute is used to control whether or not the tab is visible, and thus available. Conditions are not used to control tab activation, only tab availability. If no condition is specified, the tab will always be visible. Groups within tabs are also conditional. This should prevent the need for creating multiple tabs that look nearly identical.

To support accessibility, an appropriate keytip should be supplied for each Tab. The character/s chosen should make sense and should not conflict with any existing keytips; keytips can consist of multiple characters to disambiguate if necessary.

Subgroups

Subgroups are optionally declared inside groups. Subgroups provide finer control over ribbon scaling and ensure that the user experience is optimal when the application window is resized. Each subgroup can hold up to 3 controls. A subgroup has two attributes: size and verticalAlignment

<subgroups>
    <!-- Can only have 3 items in a subgroup-->
    <subgroup id="esri_core_editBtns" size="MediumThenSmallWhenSmall" 
                                          verticalAlignment="Center" >
      <button refID="esri_core_editCutButton"/>
      <button refID="esri_core_editCopyButton"/>
      <button refID="esri_core_editCopyPaths"/> 
    </subgroup>
</subgroups>

verticalAlignment can be "Center" or "Top" (Default)

size can be one of the following:

size Attribute Description
AlwaysLarge Child controls always use a Large variant size no matter what the ribbon size is.
AlwaysMedium Child controls always use a Medium variant size no matter what the ribbon size is.
AlwaysSmall Child controls always use a Small variant size no matter what the ribbon size is.
Default Child controls use a Large variant size when the ribbon size is Large. They change to a Medium variant size when the ribbon size is Medium. They change to a Small variant size when the ribbon size is Small. This option provides the largest number of variants for child controls.
LargeThenMediumWhenMedium Child controls use a Large variant size when the ribbon size is Large. They change to a Medium variant size when the ribbon size is Medium or Small.
LargeThenMediumWhenSmall Child controls use a Large variant size when the ribbon size is Large or Medium. They change to a Medium variant size when the ribbon size is Small.
LargeThenSmallWhenMedium Child controls use a Large variant size when the ribbon size is Large. They change to a Small variant size when the ribbon size is Medium or Small.
LargeThenSmallWhenSmall Child controls use a Large variant size when the ribbon size is Large or Medium. They change to a Small variant size the ribbon size is Small.
MediumThenSmallWhenMedium Child controls use a Medium variant size when the ribbon size is Large. They change to a Small variant size when the ribbon size is Medium or Small.
MediumThenSmallWhenSmall Child controls use a Medium variant size when the ribbon size is Large or Medium. They change to a Small variant size the ribbon size is Small.

Large
subGroups2_new.png

Medium
subGroups3_new.png

Small
subGroups4_new.png

Multiple subgroups can be declared within a group.

<subgroups>
  <subgroup id="esri_datasourcesraster_georefAdjustSubGroup1" 
         verticalAlignment="Top" size="Default">
    <button refID="esri_datasourcesraster_georefAutoFitToRefButton"/>
    <button refID="esri_datasourcesraster_georefImportControlPointsButton"/>
    <button refID="esri_datasourcesraster_georefAddControlPointsButton" 
          separator="false" />
  </subgroup>
  <subgroup id="esri_datasourcesraster_georefAdjustSubGroup2" 
         verticalAlignment="Center" size="LargeThenSmallWhenMedium">
    <gallery refID="esri_datasourcesraster_georefXformTypesGallery"/>
    <button refID="esri_datasourcesraster_georefAutoAdjustButton" 
          separator="false" />	
  </subgroup>        
  <subgroup id="esri_datasourcesraster_georefAdjustSubGroup3" 
         verticalAlignment="Top" size="MediumThenSmallWhenSmall">
    <button refID="esri_datasourcesraster_georefUpdateDisplayButton"/>
    <button refID="esri_datasourcesraster_georefResetXformButton"/>	
  </subgroup> 
</subgroups>

Each of the subgroup can then be placed in a desired group(s).

<group id="esri_datasourcesraster_georefAdjustGroup" caption="Adjust" 
  smallImage="pack://application:,,,/ArcGIS.Desktop.Resources;component/Images/RasterGeoRefTransform16.png">
  <subgroup refID="esri_datasourcesraster_georefAdjustSubGroup1"/>
  <subgroup refID="esri_datasourcesraster_georefAdjustSubGroup2"/>
  <subgroup refID="esri_datasourcesraster_georefAdjustSubGroup3"/>
</group>

Scaling controls using sizePriorities

The group DAML element has a sizePriorties property that can be used to prioritize collapsing of groups when shrinking the application. By default, controls within groups start shrinking from right to left. This default behavior can be changed by configuring the sizePriorties attribute of the group element.

A lower priority means that the size variant (large, medium, small, collapsed) will be applied sooner (large controls) and a larger priority means that the variant will be applied later (small controls). The priority number is in relation to the other groups, so specific numbers don't mean anything, just higher or lower. The first number in the array is used for comparison. The priorities are large to medium, medium to small, and finally small to collapsed. It is recommended to use subgroups to control how groups collapse.

In the DAML code snippet below, 3 groups are defined. The group element with id SizePrioritiesTesting_Group2 has the lowest priority of the 3 groups since it has the lowest first number in the sizePriorities attribute value array (10,70,110) with a value of 10. So this group will be the first group to shrink, followed by the group with id SizePrioritiesTesting_Group1. The last group to shrink is group with id SizePrioritiesTesting_Group3.

      <groups>
        <group id="SizePrioritiesTesting_Group1" caption="2nd to shrink" sizePriorities="20,60,100">
          <subgroup refID="sub1a" />
          <subgroup refID="sub1b" />
        </group>
        <group id="SizePrioritiesTesting_Group2" caption="1st to shrink" sizePriorities="10,70,110" >
          <subgroup refID="sub2a" />
          <subgroup refID="sub2b" />
        </group>
        <group id="SizePrioritiesTesting_Group3" caption="Last to shrink" sizePriorities="30,80,120">
          <subgroup refID="sub3a" />
          <subgroup refID="sub3b" />
        </group>
      </groups>
      <subgroups>
        <subgroup id="sub1a" size="Default">
        ...

sizePriorities

Toolbar

The ribbon group supports an inner collection/grouping of controls into a toolbar. Toolbars are purely declarative, and their definition describes what the toolbar looks like according to how much space is available on the ribbon. Toolbars are designed to draw as one long control; if there are size constraints, the toolbar will stack its groups to save real estate. For example, when there is a lot of room on the ribbon, such as when the user expands the main window, all of the toolbar’s groups appear in one long row. As the application shrinks, the toolbar will move successive groups under one another. Only one toolbar should be declared in a group. The following example shows a toolbar with several combo boxes and buttons:

  <toolbars>
    <toolbar id="esri_mapping_labelTextSymbolFontToolbar">
      <group>
        <comboBox refID="esri_mapping_labelTextSymbolFontFamilyComboBox" />
        <comboBox refID="esri_mapping_labelTextSymbolFontSizeComboBox"/>
        <button refID="esri_mapping_labelTextSymbolIncreaseSizeButton"/>
        <button refID="esri_mapping_labelTextSymbolDecreaseSizeButton" />
      </group>
      <group>
        <comboBox refID="esri_mapping_labelTextSymbolFontStyleComboBox" />
        <customControl refID="esri_mapping_labelTextSymbolColorPicker"/>
      </group>
    </toolbar>
  </toolbars>

Command Search

The "Command Search control" located next to the applciation title in ArcGIS Pro’s ribbon can be used to quickly find and execute commands by typing keywords into the edit box located on the control. When plugins such as a button or tool control are added to Pro's UI using an add-in, they are automatically "discoverable" by the Command Search control on the application's ribbon.

In the screenshot below, "Create Multi-Point Graphics" button has been added to the SDK tab on Pro's ribbon using an add-in. The button control is listed in the Command Search control's result when a search string (DAML button ID in this case) is supplied to the control. Note: The plugin's DAML ID, Caption or Tooltip can be used as search strings in the Command Search control.

command-search.png

To exclude commands from appearing in the Command Search results, use the daml attribute hidden="true". This is an optional attribute, the default value is "false". Button and tools created by an add-in are automatically "discoverable" by the Command Search control.

<button id="CommandSearchTesting_Multi_Point_Graphics" hidden="true" caption="Create Multi-Point Graphics" 
                className="CommandSearchButton" loadOnClick="true" 
                smallImage="pack://application:,,,/ArcGIS.Desktop.Resources;component/Images/EvilGenius16.png" 
                largeImage="pack://application:,,,/ArcGIS.Desktop.Resources;component/Images/EvilGenius32.png">
 <tooltip heading="Create Multi-Point Graphics">Sketch a line to create multi-point graphics<disabledText />
 </tooltip>
</button> 

Refer to the Pro Guide Command Search for more information on Command Search.

Suggested Commands

When focus is initially shifted to within the Command Search control by clicking or tabbing into it, recently used commands and a composite list of suggested commands are visible. The suggested items listed will vary depending on the currently active tab.

suggested-commands2.png

The list of suggested commands is determined at runtime rather than statically using DAML in order to provide a more appropriately tuned list to the state the application is within. Each ribbon tab is declared within the scope of a module, and it is the parent module that determines the suggested command list. While a particular tab may contain groups associated with a diverse array of functionality (associated with other modules), it remains the responsibility of the module that initially declared the tab to act as a representative of all the groups and provide an appropriate list.

Modules are coded to provide the suggested command list through the GetSuggestedCMDIDs override on the Module class; the method is called on the appropriate module whenever the user switches the active ribbon tab. If the module associated with the currently active tab does not implement this override, the global default list (statically defined by the application) will be presented to the user. For a better user experience, it is important for developers to provide an appropriate list by implementing this override.

The example below demonstrates a module which provides different lists of suggestions depending on the specified tab ID.

  public override string[] GetSuggestedCMDIDs(string activeTabID) 
  {
    if (string.Compare(activeTabID, "CommandSearchTesting_Tab1", true) == 0)
    {
      return new[] { "CommandSearchTesting_Multi_Point_Graphics", 
                     "esri_core_showOptionsSheetButton", "esri_mapping_clearSelectionButton" };
    }
    else
    {
      return new string[] { "esri_core_openProjectButton", "esri_core_showProjectView" };
    }
  }

UI controls

Burger button

ArcGIS Pro has a few UI controls that can be used in your add-ins and configurations. They are listed below:

The burger button is a menu for settings or advanced features. It is typically in the right-hand corner of dockpanes. This control has a dependency on ArcGIS.Desktop.Shared.WPF.dll from {ArcGIS Pro Installation folder}\bin folder.

xmlns:frameworkControls="clr-namespace:ArcGIS.Desktop.Framework.Controls;assembly=ArcGIS.Desktop.Framework"
            <frameworkControls:BurgerButton>
                <frameworkControls:BurgerButton.PopupMenu>
                    <ContextMenu>
                        <MenuItem Header="Item 1"/>
                        <MenuItem Header="Item 2"/>
                    </ContextMenu>
                </frameworkControls:BurgerButton.PopupMenu>
     </frameworkControls:BurgerButton>

burgercontrol

Circular animation

The circular animation control is a simple animation control. It is a ring of circles with different opacity. The animation occurs when we rotate the ring.

xmlns:controls="clr-namespace:ArcGIS.Desktop.Framework.Controls;assembly=ArcGIS.Desktop.Framework"
<controls:CircularAnimationControl Foreground="Maroon" SpeedRatio="0.5" Visibility="Visible"/>

CircularAnimation

DateTimePicker Control

The DateTimePickerControl provides a UI for displaying or choosing a date/time. The Date picker control allows the user to select a date/time by either typing it into a text field or by using a drop-down in the calendar.

xmlns:mapping="clr-namespace:ArcGIS.Desktop.Framework.Controls;assembly=ArcGIS.Desktop.Framework"
<mapping:DateTimePickerControl x:Name="DateTimePickerControl" />

This control can also be configured to display the date only, date and time only, and also display a text box with the selected date. ShowDate, ShowTime and ShowTextBox are the public properties of the DateTimePickerControl that allow these configurations.

Info button

The Info button control is a 'more information' button showing a simple tooltip.

xmlns:controls="clr-namespace:ArcGIS.Desktop.Framework.Controls;assembly=ArcGIS.Desktop.Framework"
<controls:InfoButton Margin="5" Header="Max Scale" 
   Description="This is the maximum amount of detail available at that extent for vector tile layers"/>

InfoButton

Message label

ArcGIS message label control is a custom label control for displaying Error, Warning, Confirmation and Information messages.

xmlns:frameworkControls="clr-namespace:ArcGIS.Desktop.Framework.Controls;assembly=ArcGIS.Desktop.Framework"

<frameworkControls:MessageLabel MessageType="Confirmation" Severity="High" ShowHelp="True" 
Content="Please enter an integer" Width="300" Visibility="Visible" />

MessageLabel

Search textbox

The Search TextBox control is a custom search control in ArcGIS.Desktop.Framework.dll that provides an optional search history menu.

xmlns:controls="clr-namespace:ArcGIS.Desktop.Framework.Controls;assembly=ArcGIS.Desktop.Framework"
<controls:SearchTextBox InfoText="Search features" SearchMode="Manual" 
    Search="SearchTextBox_Search" Width="200"/>

SearchTextBox-Manual

xmlns:controls="clr-namespace:ArcGIS.Desktop.Framework.Controls;assembly=ArcGIS.Desktop.Framework"
<controls:SearchTextBox InfoText="Search features" SearchMode="Auto" ShowHistory="True" Search="SearchTextBox_Search" Width="200" />

SearchTextBox-Auto

TabIndicator

Pro uses the TabIndicator topic20582.html on its content panes to display a collection of images. These images are presented in a horizontal view at the top of the dockpane with a custom indicator embellishment. You can click on the images to view the options for that choice.

You can create your own content panes (Dockpane, etc..) and include the TabIndicator custom control to get the same look and feel as ArcGIS Pro. The TabIndicator will display your collection of items with the images.

  ...
 <UserControl.Resources>
   <ResourceDictionary>
      <!--Datatemplate to view the items in the  Sub Panel Indicator list box control-->
      <DataTemplate x:Key="SubPanelListItem" DataType="{x:Type localMenu:OptionsMenuItem}">
         <Image Stretch="Fill" Width="24" Height="24" Source="{Binding Path=ImageSource}" >
            <Image.ToolTip>
              <ToolTip Content="{Binding Path=OptionString}"/>
            </Image.ToolTip>
          </Image>
       </DataTemplate>
  ...
...
 <Grid>
        <StackPanel Grid.Row="0">
            <frameworkControls:TabIndicator HorizontalAlignment="Stretch"
                    Margin="0,0,6,0"
                    HorizontalContentAlignment="Left"
                    ItemTemplate="{StaticResource SubPanelListItem}"
                    ItemsSource="{Binding OptionsMenu}" 
                    SelectedItem="{Binding SelectedOption}">
            </frameworkControls:TabIndicator>
...

Waiting cursor

The waiting cursor control is a custom waiting indicator control in ArcGIS.Desktop.Framework.dll. The animation starts when the control is loaded and stops when invisible or unloaded.

WaitingCursor

WebViewBrowser control

ArcGIS Pro WebViewBrowser control is a thin wrapper based on Microsoft's WebView2 control and is provided for use in your add-in UIs. To use the control:

  • Add references to the following ArcGIS Pro Microsoft WebView2 assemblies (do not use the WebView2 NuGet):
    • <ArcGIS Pro Install Folder>\bin\Microsoft.Web.WebView2.Core.dll
    • <ArcGIS Pro Install Folder>\bin\Microsoft.Web.WebView2.Wpf.dll

Set Copy Local = false on the assembly references. Note: Consult the WebView2 manifest in the Pro bin\WebView folder for the current WebView2 fixed version runtime in use by ArcGIS Pro.

  • Add a reference to the Pro WebViewBrowser control in your add-in UI xaml where needed (eg on a dockpane):
 <UserControl x:Class="..."
   ...
   xmlns:fwk="clr-namespace:ArcGIS.Desktop.Framework.Controls;assembly=ArcGIS.Desktop.Framework"
   ...>
   <UserControl.Resources>...</UserControl.Resources>
   <Grid>
     <Border BorderBrush="Red" BorderThickness="2,2">
       <fwk:WebViewBrowser Source="{Binding SourceUri, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
     </Border>
   </Grid>
 </UserControl>

and in the code behind (View Model):

private const string StartUri = "https://www.esri.com";
private Uri _sourceUri = new Uri(StartUri);
/// <summary>
/// SourceUri is used to interface with the WebViewBrowser control
/// </summary>
public Uri SourceUri
{
	get { return _sourceUri; }
	set
	{
		SetProperty(ref _sourceUri, value, () => SourceUri);
		if (_sourceUri.AbsoluteUri != _navInput)
		{
			_navInput = _sourceUri.AbsoluteUri;
			NotifyPropertyChanged(() => NavInput);
		}
	}
}

private string _navInput = "https://www.esri.com";
/// <summary>
/// NavInput is used to provide a text input field for navigation in the UI
/// </summary>
public string NavInput
{
	get { return _navInput; }
	set
	{
		SetProperty(ref _navInput, value, () => NavInput);
	}
}

Note: User data such as cookies, permissions, and cached resources of the WebViewBrowser Control goes to the user's data folder. Each user data folder is unique to a user and is created automatically by Microsoft's WebView2. The user data folder for WebView2 usually goes to \{LocalApplicationData}\{ProductName}\WebView. WebView's user data folder in ArcGIS Pro, therefore, is typically found at C:\Users\{UserName}\AppData\Local\ESRI\ArcGISPro\WebView depending on the path of your respective {LocalApplicationData} folder.

A sample using the WebViewBrowser Control can be found at WebViewBrowser Control.

XAML Icons

To better support high resolution monitors, ArcGIS Pro uses vector icon images (xaml) to display content on the ribbon as these scale much better than rasters. High resolution monitors will always use xaml icons, when available, whereas low resolution monitors “typically use” xaml icons when scaling is set at 125% or more. Add-in developers can also continue to use png (raster) images as icons in the UI Controls on the ArcGIS Pro ribbon as normal.

The following steps describe how to use vector xaml icons in your add-ins for the ribbon using a DAML <button ...> as the example. The complete example can be found in the XAML-Icons sample.

  1. Create your icons as vector based xaml icons – Example: EvilGenius16.xaml and EvilGenius32.xaml in the XAML-Icons sample. These are the icons you want to use on your custom button on ArcGIS Pro’s ribbon. You will need 2 versions of the xaml icons to support ArcGIS Pro dark and light themes.
  2. Add your dark theme xaml icons to the DarkImages folder. Add your light theme xaml icons to the Images folder (An Add-In or Configuration project in Visual Studio contains both an Images and a DarkImages folder.) You should add regular pngs as well to support lower resolution scenarios.
  3. Set the Build Action property of these xaml files to be “Content”.
  4. In your project’s Config.daml, locate the button element. Change the button element’s smallImage and largeImage attributes from the default set by the SDK template to your Images\<YourVectorImage><16 or 32>.xaml (note: reference your xaml icons in the Images folder not the DarkImages folder - also note the use of a backslash \ for the path separator).
 <controls>
  <!-- add your controls here -->
  <!--This button uses a vector based XAML Icon-->
  <button id="XAML_Icons_XAMLIcons" caption="Button 1" className="XAMLIcon" 
           loadOnClick="true" smallImage="Images\EvilGenius16.xaml" 
                              largeImage="Images\EvilGenius32.xaml">
    <tooltip heading="XAML Icon">This button uses a vector based XAML Icon<disabledText/>
    </tooltip>
   </button>
 </controls>

When ArcGIS Pro loads your add-in, it will create the button with your vector xaml icon(s). Since your add-in includes the vector xaml icons for dark and light themes in the Images and DarkImages folder, your button will automatically show the correct icon for the theme being used by the application.

Runtime API

Add-in metadata

Add-ins that want to interrogate the metadata for all probed add-ins (for the given session) can use the FrameworkApplication.GetAddInInfos() method which returns a collection of ArcGIS.Desktop.Framework.AddinInfos.

Each AddinInfo provides the following metadata per add-in:

public class AddInInfo : PropertyChangedBase
  {
    public AddInInfo();

    public string Name { get; set; }        //<AddInInfo ..><Name>
    public string Description { get; set; } //<AddInInfo ..><Description>
    public string ImagePath { get; set; }   //<AddInInfo ..><Image>
    public string Author { get; set; }      //<AddInInfo ..><Author>
    public string Company { get; set; }     //<AddInInfo ..><Company>
    public string Date { get; set; }        //<AddInInfo ..><Date>
    public string Version { get; set; }     //<AddInInfo version="...">
    public string FullPath { get; set; }    //The well-known folder location
    public string DigitalSignature { get; set; } //the signature or "None"
    public bool IsCompatible { get; set; } 
    public bool IsDeleted { get; set; }
    public string TargetVersion { get; set; } //<AddInInfo desktopVersion="...">
    public string ErrorMsg { get; set; }
    public string ID { get; set; }            //<AddInInfo id="...">
    public ImageSource Image { get; set; }
  }

If the system contains no addins then the returned list can contain a single null entry. Here is an example showing the proper handling:

  //retrieve add-in metadata for the current session:
  var addin_infos = FrameworkApplication.GetAddInInfos();
  StringBuilder sb = new StringBuilder();

  foreach (var info in addin_infos)
  {
     if (info == null)
        break;//no addins probed

     //Get the add-in metadata
     sb.AppendLine($"Addin: {info.Name}");
     sb.AppendLine($"Description {info.Description}");
     //etc
  }
  MessageBox.Show(sb.ToString(), "Addin Infos");

Events

The framework provides an event mechanism that enables communication between loosely coupled components in the application. This mechanism allows publishers and subscribers to communicate through events without having a direct reference to each other. This helps with application modularization.

The framework maintains a weak delegate reference to the subscriber’s handler on subscription. This means the reference that the framework holds to the subscriber will not prevent garbage collection of the subscriber. The weak delegate reference relieves the subscriber from the need to unsubscribe to enable proper garbage collection. This should be regarded as a safety net; however, it is recommended that subscribers should unsubscribe.

Publishing

Publishers raise an event by retrieving the event from the EventAggregator and calling the Publish or PublishAsync methods. For example, the following code demonstrates publishing the LayerSelectionChanged event:

  LayerSelectionEventArgs layerSelectionChangedArgs = new LayerSelectionEventArgs(
                                                        _layer, _toc.LayerSelection);
  
  FrameworkApplication.EventAggregator.GetEvent<LayerSelectionChangedEvents>().Publish(
                                                              layerSelectionChangedArgs);

The PublishAsync method returns the caller a Task that does not need to process immediately. This is useful in cases where the event sinks need to make asynchronous calls in reaction to the event, and you as the publisher need to wait until all of the child tasks have completed and responded before continuing.

Subscribing

For a minimal or default subscription, the subscriber must provide a callback method with the appropriate signature that receives the event notification. For example, the handler for the LayerSelectionChangedEvent requires the method take a string parameter as shown here:

  FrameworkApplication.EventAggregator.GetEvent<LayerSelectionChangedEvents>().Subscribe(
                                                                  OnLayerSelectionChanged);
  
  public void OnLayerSelectionChanged(LayerSelectionEventArgs e)
  {}

Subscribing using Strong References

If you're raising multiple events in a short period of time and have noticed performance concerns with them, you may need to subscribe with strong delegate references and manually unsubscribe from the event when disposing the subscriber.

  bool keepSubscriberReferenceAlive = true;
  
  FrameworkApplication.EventAggregator.GetEvent<LayerSelectionChangedEvents>().Subscribe(
                                   OnLayerSelectionChanged, keepSubscriberReferenceAlive);

RelayCommand

One of the important components of the Model-View-ViewModel (MVVM) programming pattern is the Command. Commands allow a separation between the view (user interface) and the command handler method via data binding. Commands implement Microsoft's ICommand interface which is part of the .NET Framework. ArcGIS Pro framework provides the RelayCommands class topic10252 to implement a Command's "Execute" and "CanExecute" functionality.

The framework's RelayCommand implementation is different from other RelayCommand implementations in that by default it will add the command to the Pro application's main message pump meaning its CanExecute function (topic10265) will automatically be called several times a second. If you do not need this behavior, set supportsOnUpdate parameter to false in the appropriate constructor. If you do use this behavior, make sure you Disconnect (topic10266) the command from the pump when your dialog closes.

RelayCommands automatically disable whenever the primary worker thread is busy. To override this behavior set disableWhenBusy parameter to false in the appropriate constructor.

Note, RelayCommands in the message pump are automatically disabled once the Pro application begins to shutdown.

Check-in and Check-out licences

For scenarios where the Pro application is using concurrent use licensing add-ins can check out and in licenses from and to the shared pool of licenses respectively.

License management within the concurrent use scenario is provided by the ArcGIS.Core.Licensing.LicenseInformation static class. Use LicenseInformation to check the availability of licenses, their expiration date, and to check them out and in. Calls to LicenseInformation to check out and in licenses in a non-concurrent use environment are ignored.

In the following examples, all the available licenses are checked out via LicenseInformation.CheckoutLicense and stored in a list of licenses. A message box is opened showing the list of licenses checked out as well as any whose checkout failed. When the user acknowledges the message box all the checked out licenses are checked back in again via LicenseInformation.CheckinLicense.

using ArcGIS.Core.Licensing;

internal class CheckoutButton : Button
{
  protected override void OnClick()
  {
    StringBuilder sb = new StringBuilder();
    StringBuilder sb2 = new StringBuilder();

    List<LicenseCodes> checkedOut = new List<LicenseCodes>();
    foreach (LicenseCodes lc in Enum.GetValues(typeof(LicenseCodes)))
    {
      var count = LicenseInformation.GetAvailabilityCount(lc);
      if (count > 0)
      {
        if (LicenseInformation.CheckoutLicense(lc))
        {
          checkedOut.Add(lc);
          sb.AppendLine($"{lc.ToString()} successfully checked out");
        }
        else
        {
          sb2.AppendLine($"{lc.ToString()} IsAvailable but check out failed");
        }
      }
    }
    //User must acknowledge messagebox
    System.Windows.MessageBox.Show(sb.ToString() + "\r\n\r\n" + sb2.ToString(),
                                                       "Extensions checked out");

    //check all the licenses back in again
    foreach (var lc in checkedOut)
      LicenseInformation.CheckinLicense(lc);

  }
}

A message box similar to the following would be shown. It's exact content would be determined by your individual concurrent licensing setup:

checkout_msg.png

Undo/Redo framework

To participate in the undo/redo framework, an operation must be created and added to the appropriate ArcGIS.Desktop.Framework.OperationManager. Within the application, each pane and dock pane can have its own OperationManager that will determine how its operations are managed. For example, different maps have their own operation stack; deleting a feature in one map will not be undoable if the focus switches to a different map. Although each pane and dock pane are given the opportunity to provide their own OperationManager, most panes of a particular type elect to share the same one. For example, all map panes rely on an OperationManager managed by a Map object and, similarily, layout panes share the OperationManager managed by the Layout object. This way, all the map panes for the same map and all the panes for the same layout each share the same OperationManager. For example, deleting a feature will show up in the undo/redo stack for all map panes showing that map and, similarly, deleting an element from a layout would show in the undo/redo stack for all layout panes showing that layout.

To create an operation, add a class that derives from Operation, and call Do() or DoAsync() on the applicable OperationManager. If your operation requires data, pass this in using a constructor override.

Operations can also be categorized so that operations belonging only to a specific category can be undone. For example, ArcGIS Pro has editing and mapping operations; if these operations are intermixed, users can elect to undo the editing operations and skip the mapping operations. Categorized operations must be mutually exclusive.

Drag and drop

Drag-and-drop commonly refers to a method of data transfer that involves using a mouse (or some other pointing device) to select one or more objects, dragging these objects over some desired drop target in the user interface (UI), and dropping them. Drag and drop behavior always involves two parties: The drag source from which the source object being dragged originates and the drop target which is the intended recipient (of the source). The drag source and drop target can be the same or different windows. See Drag and Drop in WPF for details on implementing drag and drop from scratch and Drag and Drop Overview for additional background information.

Details of Pro support for drag and drop within add-ins follows:

Drop support

Dropping on a pane

Panes and Dockpanes inherit two virtual methods OnDragOver and OnDrop from ArcGIS.Desktop.Framework.Contracts.PaneBase. For panes, OnDragOver and OnDrop is always called. However, dock panes must explicitly set themselves as drop targets by adding isDropTarget="true" to their DAML declaration*.

 <!-- declaring a drop-able dockpane in the config.daml -->
  <dockPanes>
    <dockPane id="DragAndDrop_DropDockpane1" caption="Drop Dockpane" className="..." 
           ... isDropTarget="true">
          <content className="DropDockpane1View" />
        </dockPane>
      </dockPanes>

*isDropTarget="true" sets the entire dock pane as a drop target. See Dropping on a control for configuring individual controls to be drop targets (in which case, isDropTarget="false").

OnDragOver
OnDragOver allows your UI to preview the dragged data when the mouse cursor passes over the top of your pane or dockpane. Whatever is being dragged is contained within the dropInfo.Data parameter. For example, if you support drop of file geodatabase folders you might want to examine the data to determine if it contained the path to a file geodatabase folder. Additionally, if the drag originated within the Pro application (as opposed, to say, from the Windows Explorer), the associated dropInfo.DragInfo can provide information on the source of the drag. If the payload is not relevant the add-in should set the dropInfo.Effects to DragDropEffects.None otherwise it should set the dropInfo.Effects accordingly.

OnDrop
Assuming the dropInfo.Effects was set to something other than DragDropEffects.None in OnDragOver and the payload is "dropped" over your pane/dockpane, OnDrop(DropInfo dropInfo) is called. Add-ins typically process two potential sources of data in their OnDrop implementation: data dragged from Pro catalog and data dragged from Windows Explorer. Data dragged from Pro catalog will be formatted as one or more ArcGIS.Desktop.Core.ClipboardItems. Data dragged from Windows Explorer will consist of one or more strings. Each string will contain the full path to the source data (being dragged). Data is extracted from the dropInfo and handled by your add-in logic as needed. OnDrop should set dropInfo.Handled = true to prevent the drop from being further processed.

Here is an example of an OnDragOver and OnDrop override:

  //just assume we handle it
  public override void OnDragOver(DropInfo dropInfo) {
      dropInfo.Effects = DragDropEffects.All;
  }

 public override async void OnDrop(DropInfo dropInfo) {
   //check for List<ClipboardItem> - this is a drag originating with Catalog
   string data_path = "";
   if (dropInfo.Data is List<ClipboardItem> clipboardItems)
   {
     //in this example, we handle just the first item if there are multiples being dragged
     var thisItem = clipboardItems.First();
     data_path = thisItem.CatalogPath;
   } 
   //Dragged from Windows Explorer?
   else if (dropInfo.DragEventArgs.Data.GetDataPresent(DataFormats.FileDrop))
   {
     data_path = dropInfo.Items[0].Data.ToString();//just the first in this example
   }
   if (string.IsNullOrEmpty(data_path))
     return;//we don't handle this
   //TODO
   //process the drop
   ...

   //set to true indicating we handled the drop
   dropInfo.Handled = true;
}

Dropping on a control

Dock panes have the option of allowing the drop target to either be the entire DockPane itself or confining it to one or more "inner" controls such as Textboxes, ListBoxes, TreeViews, ListViews, etc. To confine the drop target to just one or more specific controls:

  1. Set isDropTarget = false on the dockpane declaration in the config.daml.
  2. Add the attached properties DragDrop.IsDropTarget and DragDrop.DropHandler (from the ArcGIS.Desktop.Framework.DragDrop namespace) to each control on the dockpane for which you want drop supported. DragDrop.DropHandler should bind to the property on the dockpane that provides the implementation for OnDragOver and OnDrop. This can be the dockpane itself or a separate class that derives from ArcGIS.Desktop.Framework.Contracts.DropHandlerBase.

Assume the dockpane provides the "DropHandler" implementation:

<UserControl x:Class="DropDockpane1View"  ...
   xmlns:dragDrop="clr-namespace:ArcGIS.Desktop.Framework.DragDrop;
                   assembly=ArcGIS.Desktop.Framework">
  ...
  <!-- we allow drop on a text box - the dockpane implements OnDragOver and OnDrop-->
  <TextBox MinWidth="250" Text="{Binding ...}" 
          dragDrop:DragDrop.IsDropTarget" Value="True"
          dragDrop:DragDrop.DropHandler" Value="{Binding}" />
  //drop is implemented by the dockpane (OnDragOver and OnDrop)
  //drop allowed on the textbox only so isDropTarget=false in the config.daml
  internal class DropDockpane1 : DockPane {
    public override void OnDragOver(DropInfo dropInfo) {
       ...
    public override void OnDrop(DropInfo dropInfo) {

In this example, assume the "DropHandler" implementation is provided by a separate DropHandlerBase implementation instead of the dockpane (notice the difference in the DropHandler Binding statement)

  <!-- we allow drop on a text box - a DropHandlerBase implements OnDragOver and OnDrop-->
  <TextBox MinWidth="250" Text="{Binding ...}" 
          dragDrop:DragDrop.IsDropTarget" Value="True"
          dragDrop:DragDrop.DropHandler" Value="{Binding DropHandler}" />
  //provide a DropHandlerBase implementation in the add-in
  public class MyCustomHandler : DropHandlerBase {
     
    public override void OnDragOver(DropInfo dropInfo) {
       ...
    public override void OnDrop(DropInfo dropInfo) {

  //expose the DropHandlerBase on the dock pane
  //drop allowed on the textbox only so isDropTarget=false in the config.daml
  internal class DropDockpane1 : DockPane  {
         ...
         public MyCustomHandler DropHandler => _dropHandler;

The implementation of OnDragOver and OnDrop is the same as before.

Rather than adding the attached properties directly to the individual control(s), they can be also be within a style per control type and the style(s) referenced instead. Which approach you use is really just a matter of preference. Here's an example that uses a style to assign the attached "drop" properties to the textbox:

 <!-- define drop support as a style. Apply the style to the textbox -->
 <Style x:Key="MyTextBoxStyle" TargetType="{x:Type TextBox}">
        <Setter Property="dragDrop:DragDrop.IsDropTarget" Value="True" />
        <Setter Property="dragDrop:DragDrop.DropHandler" Value="{Binding}" />
 </Style>

 <!-- any Textbox that is to support drop references the style -->
 <TextBox MinWidth="250" Text="{Binding ...}" Style="{StaticResource MyTextBoxStyle}" />

Drag and drop for a dock pane is also covered in the ProGuide Dockpanes

Dropping on a Pro window

The framework also support drag and drop operations in Pro windows (i.e. dialogs). The steps are similar to those described above, only, in this case you are adding the attached properties to either the Pro window itself or one or more child controls within it.

  <controls:ProWindow x:Class="DropExample.CustomProWindow"
               xmlns:dragDrop="clr-namespace:ArcGIS.Desktop.Framework.DragDrop;
				assembly=ArcGIS.Desktop.Framework"
               ...>
    <Grid>...
      <TextBox Text={Binding ...} dragDrop:DragDrop.IsDropTarget="True"
               dragDrop:DragDrop.DropHandler="{Binding WindowDropHandler}" .../>

Pro windows derive from ArcGIS.Desktop.Framework.Controls.ProWindow which does not provide OnDragOver and OnDrop overrides. Therefore, drop behavior for a Pro window must be handled via a drop handler implementation exposed as a property on the window's view model:

  //provide a DropHandlerBase implementation in the add-in
  public class MyCustomHandler : DropHandlerBase {
     //implemented as before with the dockpane
     ...
 
 //expose the drop handler on the window view model.
 public partial class CustomProWindow: ProWindow, INotifyPropertyChanged {

    public CustomProWindow() {
      InitializeComponent();
      (this.Content as FrameworkElement).DataContext = this;
      ...

   //drophandler referenced in the xaml via dragDrop:DragDrop.DropHandler
   public MyCustomHandler WindowDropHandler => _dropHandler;

Custom drop handlers in DAML

A custom drop handler can be registered within your config.daml to override the default drop behavior of Pro panes and/or dockpanes. This can be useful if you want your add-in to augment the default drop behavior for a map to handle a proprietary data type or any spatial data type not handled "out of the box" when dropped on a map in Pro. The custom drop handler registers the data types it supports (typically a collection of file suffixes) within the config.daml. When a file of that specified file type is being dragged over Pro, your custom drop handler will be invoked before the underlying Pro pane to handle, or not handle, the drop. The same rules apply as previously discussed for handling OnDragOver and OnDrop. If the handler does not handle the drop then it is passed on to the underlying pane or dockpane.

In this example, a custom DropHandler is registered in the config.daml for excel datatypes.

  <dropHandlers>
    <insertHandler id="DragDropExample_ExcelDropHandler" 
         className="ExcelDropHandler" dataTypes=".XLSX|.xlsx|.XLS|.xls" />
  </dropHandlers>

The full implementation of a custom handler for excel data can be found in the Pro SDK samples: ExcelDropHandler

Note: The Pro SDK provides an item template ArcGIS Pro Drop Handler that will automatically generate the Config.daml entry and stubs out the associated DropHandlerBase implementation.

Drag support

Drag support can be added to a dockpane (or pane but that is unusual) and/or individual controls on the dockpane using the DragDrop.IsDragSource and DragDrop.DragHandler attached properties in conjunction with an implementation of ArcGIS.Desktop.Framework.DragDrop.IDragSource.
IDragSource can be implemented on the dockpane directly or on a separate custom drag handler (which can be the same class that implements drop behavior via the DropHandlerBase base class if desired). IDragSource can be added to any class and does not require a specific inheritance hierarchy as is the case with drop.

IDragSource.StartDrag is invoked on the drag source when dragging has been initiated. Withing StartDrag, the IDragSource can check what is being dragged via the dragInfo.SourceItem property (or via its own context mechanism). If the drag is being initiated on something the drag source supports, your IDragSource allows it to continue otherwise the drag is stopped.
It is the responsibility of the IDragSource to ensure that the dragInfo.Data is correctly set when the drag is allowed to continue. The dragInfo.Data will be null when it is passed in to your StartDrag callback. dragInfo.Data can be set to any format, however, to support drag off your dockpane to one of Pro's dockpanes or panes (eg the Map, Catalog, etc), the data payload should be formatted as a list of ClipboardItems. StartDrag must also set the dragInfo.Effects to something other than System.Windows.DragDropEffects.None. Leaving dragInfo.Effects set to DragDropEffects.None (the default) effectively terminates the drag.

Assume in the below example, there is a treeview on a custom Dockpane that is displaying the feature classes and tables contained within a file geodatabase. Items in the treeview are represented by a custom GDBBaseItem class defined in the add-in. Each GDBBaseItem provides the path to, and name of, a feature class or table shown in the tree view.

Users drag any file geodatabase onto the custom dockpane to populate the treeview. Users can likewise drag off any feature class or table from the treeview and onto the map. The map's default drop handler adds the dropped data as a layer (or standalone table).

 <!-- enable drag on our treeview -->
 <TreeView ItemsSource="{Binding GDBItems}" ...
           dragDrop:DragDrop.IsDragSource="True"
           dragDrop:DragDrop.DragHandler="{Binding}">

and in the code behind:

  //our custom wrapper of a feature class or table for the treeview
  public class GDBBaseItem {
      public string Path ...
      public string Name ...

      //get an iteminfovalue for use with the ClipboardItem
      public virtual ItemInfoValue GetItemInfoValue() {
         var uri = Path + @"\" + Name;  //i.e. a "catalog" path format
         var gdb_item = ItemFactory.Instance.Create(uri);

         //ClipboardItem needs an ItemInfoValue
         return new ItemInfoValue() {
            name = gdb_item.Name,
            title = gdb_item.Name,
            catalogPath = gdb_item.Path,
            type = gdb_item.Type,
            typeID = gdb_item.TypeID,
            isContainer = "false"
         };
       }
    
 //Custom drag for the dockpane - note the IDragSource declaration
 internal class DragDockpane1ViewModel : DockPane, IDragSource {

   //IDragSource implementation
   public void StartDrag(DragInfo dragInfo)
    {
      if (dragInfo.SourceItem == null)
        return;
      var gdbItem = dragInfo.SourceItem as GDBBaseItem;
      ...
      //Add the drag content as a List<ClipboardItem>
      List<ClipboardItem> clip_items = new List<ClipboardItem>();
      clip_items.Add(new ClipboardItem() {
        ItemInfoValue = gdbItem.GetItemInfoValue()
      });
      dragInfo.Data = clip_items;
      dragInfo.Effects = DragDropEffects.Copy;
    }

The complete sample of DragAndDrop shown above can be accessed here: DragAndDrop

Note: You can optionally change the drag adorner, which is a transparent image that shows a preview of the data being dragged, by setting the attached property DragAdornerTemplate.

  dragDrop:DragDrop.DragAdornerTemplate="{StaticResource LayerDragAdorner}"

Notifications

Toast notifications

Toast notifications appear as transient popups in the upper right corner of the screen and are used to make the user aware of some change in the system. Notifications can come in from multiple points within the software and will stack up to four levels deep, appearing for a short time or until the user clicks on the notification. Notifications are queued so that even if more than four come in during a short period, they all will eventually appear.

Notifications can be associated with a title, a caption, and an image. You also handle the click notification so that the Toast performs a particular action when clicked. Create a Notification using the ArcGIS.Desktop.Framework.Notification class, topic10177.html. Add the notification to Pro via the static ArcGIS.Desktop.Framework.FrameworkApplication.AddNotification() method, topic10141.html.

  Notification toast = new Notification();
  toast.Title = "Attention!";
  toast.Message = "This is an example notification";
  toast.ImageSource = Application.Current.Resources["ToastLicensing32"] as ImageSource;

  FrameworkApplication.AddNotification(toast);

With ArcGIS Pro 3.1, more complex content can be rendered within the toast notification area. This would include hyper links, texts with various styles, sizes, and colors applied.

  Notification toast = new Notification();
  toast.Title = "Attention!";
  toast.Message = "{\\rtf1{\\colortbl ;\\red33\\green181\\blue54;}" +
                "This is a message\\par{\\b {\\field{\\*\\fldinst{HYPERLINK \"https://www.esri.com/\" }}" +
                "{\\fldrslt{\\ul\\cf1 Hyperlink\\cf0 }}}}}";
  toast.ImageSource = Application.Current.Resources["ToastLicensing32"] as ImageSource;           
  
  FrameworkApplication.AddNotification(toast);

toast.png

Quick Access Toolbar, QAT

The Quick Access Toolbar (QAT) is a small, customizable toolbar that exposes a collection of commands that are specified by the application or selected by a user. The toolbar appears by default in the top-left corner of the application but users can move it below the ribbon. The QAT also has a pull-down menu where users can hide (or show) commands on the toolbar.

QAT1.png

The QAT is modified interactively via the Customize Quick Access Toolbar QAT menu. Selecting "More Commands..." opens the Project Options property page from which you can select additional commands to be added to the QAT or remove existing ones.

QAT2.png

The QAT cannot be modified via DAML though it is possible to modify the QAT within a Configuration by overriding its ConfigurationManager.OnCreateQuickAccessToolbar callback. Please refer to the ProConcepts Configurations OnCreateQuickAccessToolbar for more information.

Notifications dockpane

The Notifications dockpane provides a central place for users to see application and project notifications. Notifications are used throughout Pro and include software updates, display warnings, layer and package updates, and general messages.
Notifications are organized into two categories: Application and Project. To submit a notification to the Notifications Dockpane, use the ArcGIS.Desktop.Framework.NotificationItem class, topic18271, and add the NotificationItem to the ArcGIS.Desktop.Framework.NotificationManager via NotificationManager.AddNotification(), topic18300.html. NotificationItems can specify a message, a longer detailed message, an image, a date, an action (i.e. a callback), and even a context menu item. The custom callback (if specified) controls what happens when a notification is clicked.
When a notification is received by the NotificationManfager, the Notifications button in the upper-right corner of the Pro application has a blue dot indicating that notifications are available. Clicking on the button will show the Notification dockpane if it is not already visible. Refer to ArcGIS Pro Notifications.

Notifications2.png

Notifications.png

This code example creates a project-level notification item and a custom delegate for when the notification item is clicked. When the notification item is clicked, the delegate will be invoked (showing a message box in this particular case).

   //delegate is custom - your design (name, params, functionality, etc)
    public delegate void MyCustomDelegate(int x);

   ...
    //Elsewhere....the callback itself for when the notification item is clicked
    private void ShowMessage(int x)
    {
      System.Windows.MessageBox.Show("This is the delegate. Param x: " + x.ToString());
    }
    ...
   
    //Create a Notification Item in response to whichever event or circumstance
    //
    var custom_delegate = new MyCustomDelegate(ShowMessage);//optional - for callback

    //Note: 2nd param: isApplicationLevel = true for application level, 
    //                 isApplicationLevel = false for Project level
    var ni = new NotificationItem("Module1_CustomNotification", false, "Custom Item", 
                                  NotificationType.Custom, "some details here", true, 
                                  DateTime.Now, "", "View Item", custom_delegate, 
                                  new object[] { 3 }));

    //add the notification item to the manager
    //a blue dot will appear on the notifications icon on the Pro ribbon
    NotificationManager.AddNotification(ni);
    ...
    

Diagnostic Mode (event logging)

ArcGISPro supports a diagnostic mode which is enabled through the following command line switch:

/enablediagnostics

The mode provides a simple, single application wide logging facility (file based). Each logged event is simply a string with a time stamp and a type: warning, error, or information.  The log format and usage is similar to the Windows Event log but stored within an independent XML file.  Event strings can contain carriage returns and will be formatted neatly within the XML log file accordingly (see example below).

Events written to the event log should be clear and unambiguous since they are intended to aid external users or developers in diagnosing issues. Like exception strings, diagnostic mode event strings are English only.

WARNING: You should never log strings to the event log that might contain identifying information, passwords, or confidential information.

A fresh empty log file is created each time the application is started. The log file is created within the user’s Documents folder under an ArcGIS\Diagnostics subfolder.  The log file itself is called ArcGISProLog.xml.  If the log ever exceeds 10 MB, older entries will be aged out of the log.

Example contents of a log:

<?xml version="1.0"?>
<EventLog ver="1">
  <Event time="Wed Oct 14 10:12:31 2015" type="Info">Application startup.</Event>
  <Event time="Wed Oct 14 10:12:38 2015" type="Error">
      An error was encountered when importing document “junk.mxd”.
      One or more layers were invalid.
  </Event>
</EventLog>

3rd parties can add their own output to the event log using the EventLog static class exposed from ArcGIS.Desktop.Framework.Utilities.  Note that calling EventLog.Write() is a no-op if diagnostic mode hasn’t been enabled. Therefore there is no need to write logic into your add-in to determine if you are in diagnostic mode or not. Events written to the log will be flushed to disk at application shutdown (even when an exception brings the app down), however, you have the option of writing them immediately if you want them to appear in the log file while the application is still running.

  //class located in ArcGIS.Desktop.Framework.Utilities
  public static class EventLog
  {
    public enum EventType
    {
      Debug,
      Error,
      Warning,
      Information 
    }

    public static bool IsEnabled
    public static void Write(EventType type, string entry, bool flush)
    public static void Write(EventType type, string entry, uint elapsed, bool flush)
    public static void Write(EventType type, string entry, string func, string code, uint elapsed, bool flush)
  }

In addition, three optional arguments can be written using overloads of the Write method.

  • elapsed: an unsigned integer containing the number of milliseconds that have elapsed performing the operation associated with the event.
  • func: the name of the function associated with the event.
  • code: a generic code associated with the specified function.

If specified, these optional bits of information will appear as attributes on the event entry. Note that whenever possible, these events should be used instead of encoding similar information into the information argument, as this standardizes the format and makes it much easier to perform queries based on these fields later on during log analysis.

  EventLog.Write(EventLog.EventType.Warning, 
    String.Format(
      "Add-In {0} has been disabled due to an exception thrown during component initialization.", 
         id));

Diagnostic Counters

The EventLog class also includes diagnostic counter methods which let you associate a count (or numeric value) with a named counter. When the application running in diagnostic mode terminates, all of the currently registered counters will appear in a report at the end of the log.

EventLog.IncrementCounter("MyCounterName");

Working with Multithreading in ArcGIS Pro

ArcGIS Pro differs markedly from existing ArcGIS for Desktop applications in that it is built with a multithreaded architecture designed to leverage modern CPUs/GPUs with multiple execution cores. For the add-in developer extending ArcGIS Pro, this means an altered programming model and the need to familiarize yourself with a few new concepts that may appear puzzling at first. As with anything new, working with these patterns will gradually become easier, and the benefits of multithreading will become increasingly clear. There is also a Pro Concepts Asynchronous Programming in ArcGIS Pro that can be consulted for more information.

Challenges for the Multithreading Programmer

The following four key differences distinguish any multithreaded application—including ArcGIS Pro—from a classic single threaded application:

  • To ensure a responsive user experience, the graphical user interface (GUI) thread must be able to take input from the user and produce graphical output smoothly and without interruption. This means that the execution of coded actions must be performed asynchronously on separate worker threads; the GUI thread should never perform work or blocking waits of any kind. This is in contrast to the existing ArcGIS for Desktop applications where most work is performed directly on a single GUI thread.

  • While work is executing on background threads, users must be presented with a logically consistent and informative user interface. Commands, tools, and various other parts of the user interface should be enabled or disabled appropriately based on what operations are executing, and appropriate feedback should be provided. If a long running operation is logically cancellable, an option to cancel should be offered.

  • Conflicting operations should not be executed simultaneously and should always be performed in an appropriate logical sequence. For example, operations on a map cannot be executed while the project that contains the map is still in the process of loading, and a selected set of features cannot be deleted until the selection itself has been fully computed. Most operations initiated through user interaction are logically order dependent and should be executed serially.

  • Care must be taken to ensure that access to volatile state—that is, access to non-constant variables within the program—is properly synchronized when such state is shared between threads. For example, if a collection object is shared between a worker thread and the GUI thread, both threads need to coordinate access to the collection so that one thread isn’t reading items from the collection while the other is simultaneously adding or removing items. This kind of protective coding is common to all kinds of multithreaded programing and is normally accomplished using a lock. In an application where multiple independent parties can extend the application behavior, coordinating operations can become unworkably complex without a common framework to manage how components work together.

A full treatment of multithreaded programming is beyond the scope of this document, but the following information will cover the most common patterns along with how Esri’s APIs and threading model should be used to tackle each of the previously listed challenges.

The ArcGIS Pro internal threading model

Esri engineers have placed a high priority on making ArcGIS Pro as easy to program against as possible in the new multithreaded architecture. To this end, ArcGIS Pro incorporates the latest asynchronous language features from Microsoft along with new application-specific threading infrastructure tailored to reduce coding complexity.

In most cases, add-in developers should only need to contend with two threads: the user interface thread, and a single specialized worker thread provided by the application. Internally, ArcGIS Pro uses a large number of threads for purposes including rasterization, graphics rendering, data loading, and select geoprocessing algorithms that leverage parallelism to speed computation. To keep all of these activities running smoothly and without conflicts requires a considerable amount of coordination and associated complexity; for this reason, these threads are entirely internal and isolated from developers within the implementation of the public SDK. When a method in the public API is called, the internal implementation may—when applicable—split the operation up and delegate fragments to one or more of these specialized internal threads, or queue operations that will ultimately be executed within an external process or web service.

Threadingmodel.png

Tasks and the task asynchronous pattern

Methods within ArcGIS Pro SDK fall into three categories:

  • Asynchronous methods that can be called on any thread. Methods of this type are named using an Async suffix and usually return Tasks. In some cases, both a synchronous and an asynchronous version of a method may be provided.

  • Synchronous methods that should be called on the worker thread only. Methods of this type are annotated within the API reference, and a code tip will appear when hovering over the method.

  • Synchronous methods that should be called on the GUI thread only. These types of methods are usually associated with WPF.

If a method on a particular object is called on the wrong thread, the call will generate an ArcGIS.Core.CalledOnWrongThreadException exception. If unsure about a particular case, you can refer to the SDK component help or Microsoft provided help to determine whether a particular method or property has a restriction.

Within the SDK—particularly within the ArcGIS.Core namespace—worker thread bound methods and properties tend to be very fine grained. To reduce the overhead associated with scheduling and thread context switches, these methods are synchronous and must be coded using tasks.

Microsoft’s .NET Task Parallel Library TPL and the associated programming pattern known as the Task Asynchronous Pattern TAP simplify the authoring of asynchronous code within a multithreaded application. The Task class is used to represent an operation executed asynchronously.

In the following example, the PrintReportAsync method is invoked and immediately returns a Task object to the caller. Meanwhile, the printing function continues to run in the background on another thread.

  private void Button_Click(object sender, RoutedEventArgs e)
  {
    Task t = PrintReportAsync("HP1");
    // Wait until the task is done.
    t.Wait();
    MessageBox.Show("Printed report is ready!");
  }

The author of the example wants to show a message when the printing is complete and uses the Wait method on the returned Task object to suspend the calling thread until the task is done. This approach has two major problems: First, since the calling thread cannot do anything else while it is waiting, it’s actually less efficient than simply calling a synchronous version of the print function. Second, since the calling thread is a GUI thread in this case, the user interface will freeze. A suspended thread obviously cannot process user input, render graphical elements, or do anything at all for that matter. For these reasons, you should never use the Wait method on a GUI thread.

Luckily, .NET introduced the language features async and await. The async modifier marks the method so that the compiler knows that the method is asynchronous and will be using the await operator. The await operator is most helpful, as this is used to call methods asynchronously and afterward, force the calling thread to automatically return to the next line and continue execution once the asynchronous operation has completed. The calling thread—normally the GUI thread—is not blocked and is free to take other actions while the Task on the worker thread is still running.

Note that the author now accomplishes the original goal with very little change, but doesn’t hang the user interface.

  private async void Button_Click(object sender, RoutedEventArgs e)
  {
    Task t = PrintReportAsync("HP1");
    // Wait (without blocking) until the task is done.
    await t;
    // Return here when task is done.
    MessageBox.Show("Printed report is ready!");
  }

Using Run

When an asynchronous function is unavailable, you can easily write your own wrapper functions that internally execute one or more synchronous methods. The following sample uses the static Run method to queue the execution of the function WorkFunc to a random thread in the Task thread pool. Note that the click method immediately returns to the caller, while the WorkFunc continues to execute on the worker thread.

  private void Button_Click(object sender, RoutedEventArgs e)
  {
    Task t = Task.Run((Action)WorkFunc);
  }
  private void WorkFunc()
  {
    // Do Work
  }

Instead of using a separate function, an anonymous function—called a lambda—can be employed. Using lambdas keeps the worker code within the same function and lets you use arguments and local variables within the lambda as if they were part of the containing function.

  private void Button_Click(object sender, RoutedEventArgs e)
  {
    int steps = GetSteps();
    Task t = Task.Run(() =>
    {
      // I can use the variable “steps” here even though I'm in a
      // different function running on a different thread!
      // Do work
    });
  }

Tasks can also be parameterized to return a particular type, as the result of whatever the lambda computes.

  Task<double> t = Task.Run<double>(()=>
  {
    double result;        
    // Compute floating point result here...
    return result;
  });

The await operator can also be used in-line to obtain the result of the asynchronous function, and without having to extract it from the returned Task.

  private async void Button_Click(object sender, RoutedEventArgs e)
  {
    double computedValue = await Task.Run<double>(()=>
    {
      double result = 42.0;        
      // Compute floating point result here...
      return result;
    });
    // Execution automatically resumes here when the Task above completes!
    MessageBox.Show(String.Format("Result was {0}",  computedValue.ToString()));
  }

There is a small overhead associated with await, so it’s always more efficient to call multiple synchronous methods within your own lambda than to call many asynchronous functions using await. This is particularly true when coding loops, where the cost of using await through hundreds or thousands of iterations will become substantial.

Using QueuedTask

While Tasks are a regular fixture within any add-in code, Tasks need to be dispatched in ArcGIS Pro differently from traditional TAP. The framework provides a custom Task scheduler that should be used when dispatching Tasks that make calls to synchronous methods within ArcGIS Pro SDK. Rather than calling Task.Run however, add-in developers should call QueuedTask.Run instead.

  Task t = QueuedTask.Run(()=>
  {
    // Call synchronous SDK methods here
  });

The QueuedTask class is used instead of the Task class for the following reasons:

Queuing and concurrency control

When Tasks are dispatched using Task.Run, the associated Task will execute on a random thread in the managed thread pool each time it’s called. If a subsequent call to Task.Run is called from anywhere else in the application, the new Task will start running immediately on yet another thread—potentially while the first Task is still running on the first thread. Going back to the list of challenges inherent in multithreaded code, it should be obvious that executing unorganized operations concurrently is likely to lead to crashes and corruption of application state. The queuing behavior of QueuedTask.Run ensures the proper ordering of calls and reduces the risk of conflicts. Remember that the parallelism going on within ArcGIS Pro is accomplished internally; this simplifies the public programming model and greatly reduces the likelihood of conflicts.

Affinity and state

For performance reasons, ArcGIS Pro maintains considerable state on specific threads and in many cases, uses objects that have thread affinity. Thread affinity means that an object is tied to a particular thread and should not be interacted with from any thread but the thread it has affinity with. Affinity constraints are common in operating systems and components, including database connections, windows, controls, input queues, timers, WPF Bitmaps, and COM servers. In WPF for example, calling methods on any object derived from the WPF DependencyObject class will result in an exception if the call is made from a thread the object wasn’t created on.

Threads in the managed thread pool are also incompatible with most COM components, so you should not attempt to use Task.Run with code that might execute COM components directly or indirectly.

Application integration

When Tasks are dispatched using QueuedTask.Run, they are automatically integrated with various features within the application as follows:

  • The extended Progress/Cancellation framework where progress, including the programmable progress dialog, is displayed and hidden automatically and where cancellation state is properly communicated between relevant parts of the application.

  • The application busy state system where UI elements such as buttons and tools are automatically enabled and disabled when Tasks are running. Task execution can also be coordinated with critical phases such as view creation and application shutdown.

  • Queued Tasks are enlisted in the framework’s diagnostic facilities, when enabled. This lets developers monitor the sequence of running Tasks, the functions Tasks are executing, and the duration of execution. This kind of information is invaluable in debugging and performance analysis.

Blocking the Gui Thread

Care should be taken when writing the code that will be passed to the QueuedTask.Run within the lambda. Once inside the lambda, any code that triggers a popup or prompt (such as a MessageBox) or other UI window (e.g. a modal dialog) that requires an acknowledgement from the user should be avoided. Displaying any such UI from within the QueuedTask.Run will block the QueuedTask (and all other operations queued on the QueuedTask) from proceeding. The only UI that should be shown from within a QueuedTask.Run is either Progress or Cancelable Progress. Both of these run asynchronously without blocking the QueuedTask. Refer to the Progress and cancellation section below.

Acceptable cases for using Task.Run

There are cases where the use of Task.Run is acceptable—such as when executing independent background operations consisting entirely of managed code—so long as the particular managed components in use do not have thread affinity. The developer takes full responsibility for handling cancellation, displaying progress, enabling/disabling the UI appropriately, coordinating operations, and handling logical conflicts.

Using BackgroundTask

Introduced at 2.6 is the ArcGIS.Core.Threading.Tasks.BackgroundTask. BackgroundTask, similar to the QueuedTask, can be used to schedule and dispatch tasks that are compatible for use with Pro's underlying COM components. However, unlike the QueuedTask, the BackgroundTask does not manage concurrency by ordering, or "sequencing", the underlying tasks. Instead, BackgroundTask gives add-in developers access to Pro's background thread pool (as opposed to the QueuedTask "managed" CIM Thread Pool) and is appropriate for non-modal operations that must meet the following criteria:

  • The operation should not rely on continual access to application or CIM state; though it can for instance, read CIM state at the beginning via a "GetDefinition()" call on the relevant model object. CIM state should be written, however, on a QueuedTask.

  • The operation should not alter any application or CIM state while running (eg adding or modifying project, map, or layout content) and should not enforce a modal user experience; For example, it should not show a modal progress dialog or disable the entire user interface.

  • The operation does not require any input from the user once it is running.

Operations running within the context of a BackgroundTask.Run cannot make any assumptions concerning what the user may do while the background operation is running (in other words, unlike with a QueuedTask, an operation cannot assume a static application state). The application busy state system does not disable UI elements such as buttons and tools when a BackgroundTask operation is running (unlike when a QueuedTask operation is running and UI elements are disabled). This means users are free to unload the current project, delete data, or make alterations to any aspect of application or CIM state. Operations running via a Background Task, therefore, must be robust enough to handle such cases. The operation should also be able to react to state changes that can logically cancel the running background task.

TaskPriority

The shared background thread pool is divided into a normal and high priority part. 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 running, assume long and use the normal priority pool.

Task priority can be specified via a ArcGIS.Core.Threading.Tasks.TaskPriority parameter passed as the first argument to BackgroundTask.Run(). The default, if no TaskPriority is specified, is to use the normal priority pool. If an operation has thread affinity concerns and needs to run on a consistent background thread then TaskPriority.single can be used. Use of TaskPriority.single has a similar semantic to a QueuedTask.Run in that only one "single" operation can run on the background thread pool at any given time effectively queueing the background task operations.

  Task t = BackgroundTask.Run(()=>
  {
    //Default is to run on the background normal priority pool
  });

  Task t2 = BackgroundTask.Run(ArcGIS.Core.Threading.Tasks.TaskPriority.high, ()=>
  {
    //Short duration operation being run on the background high priority pool
  });

  Task t3 = BackgroundTask.Run(ArcGIS.Core.Threading.Tasks.TaskPriority.single, ()=>
  {
    //Operation with thread affinity requirements being run sequentially on the 
    //background pool
  });
  

BackgroundTask.Run can optionally use a ArcGIS.Core.Threading.Tasks.BackgroundProgressor. The BackgroundProgressor differs from the ArcGIS.Desktop.Framework.Threading.Task.Progressor and derived classes in that BackgroundProgressor does not support a progress dialog.

Background operations should never put the application into a modal state and so progress dialogs are not allowed. BackgroundProgressors cannot be created directly and instead are obtained from the associated source object, BackgroundProgressorSource.

Locking Guidelines

Here are a few basic recommendations for using locks safely. This is not a definitive list and there are exceptions to some of these rules in very particular scenarios, but adherence will reduce the risk of problems such as GUI hangs, deadlocks, and crashes.

  1. Never make cross-thread blocking calls from inside a lock. This includes Dispatcher.Invoke methods.
  2. Never call any kind of function from inside a lock if you don’t have complete knowledge and control over what that function does. This includes publishing an event or invoking an abstract callback.
  3. Locks should be placed as close to the resource they protect as possible, not in a method that calls a method, that calls another, that ultimately accesses the resource; it’s too easy to miss a code path where the lock is skipped.
  4. Operations within a lock should be short in duration and should not depend on another lock. Never enter a lock for an extended period of time as the UI might be waiting on that lock and you’ll hang the GUI as a result. Consider fine grained locking, or chunked operations using block copies to handle longer processing times associated with a protected resource.
  5. Code should enter and leave a lock within the same function; i.e. never author a method that only enters or only leaves a lock, and relies on subsequent calls to yet another function to leave it.
  6. Locks need to be placed around both reads and writes on a protected resource.
  7. Use a lock guard, i.e. lock(_lock){...}, to ensure that locks are properly exited if an exception occurs while code is executing within a locked region.
  8. Do not enter locks or make cross thread calls from the constructors/destructors of global or static objects.

Also be careful with what you do in ObservableCollection<T>::CollectionChanged events published from collections with an established binding lock (via BindingOperations.EnableCollectionSynchronization(collection,_lock) for instance). These events will actually be fired from within the lock (refer to recommendation 2).

Progress and cancellation

Asynchronous methods may sometimes accept a Progressor argument, an object that is used by the caller to configure the progress dialog box and cancellation settings, and to coordinate communication between the caller and callee. Asynchronous methods that are not cancelable take a Progressor class, while cancelable methods take a CancelableProgressor class.

Progressor objects follow the pattern established by Microsoft’s CancellationToken and cannot be created directly; instead, the developer must create a ProgressorSource or CancelableProgressorSource.

The “source” objects allow you to configure how the progressor will handle progress without exposing these settings to external code, which might access the progressor. The ProgressorSource object exposes the following constructors:

  public ProgressorSource(Action<Progressor> callback)
  public ProgressorSource(ProgressDialog progDlg) 
  public ProgressorSource(string message, bool delayedShow = false)

The first override takes a delegate that will be called at regular intervals while the task is running. This option is appropriate when you want to provide specialized feedback during task execution.

The second override takes a separately constructed progress dialog object. If not already shown, the progressor will automatically show this progress dialog box when the task starts executing and automatically hide it when the task completes. If the dialog box is already visible, the progressor will update the contents of the dialog box while running, and it will be the developer’s duty to hide the progress dialog box when appropriate. This option is appropriate when you want to manually control progress dialog box visibility, such as when you need to keep the progress dialog box up across several separate tasks.

The third override will automatically create and show a progress dialog box when the task starts executing and hide it when the task completes. The delayedShow parameter controls whether the progress dialog box should show immediately or delay its appearance to allow quick tasks to complete and avoid appearing at all if unnecessary. If the task is expected to execute quickly, set this parameter to true. If you expect the task to take more than a second or two to complete, set delayedShow to false so that the progress dialog box appears immediately to convey a more responsive feel.

CancelableProgressors require an additional argument that specifies what the cancel message should say. The cancel message will appear as soon as the user clicks the Cancel button on the dialog box.

  public CancelableProgressorSource(Action<CancelableProgressor> callback);
  public CancelableProgressorSource(ProgressDialog progDlg);
  public CancelableProgressorSource(string message, string cancelMessage, 
             bool delayedShow = false);

Example Method Implementation Using Cancellation

The specialized CancelableProgressor exposes a CancellationToken property that can be used to communicate cancellation. Within the method’s implementation, code running in loops should check the IsCancellationRequested property and exit the method by throwing an OperationCanceledException (which acknowledges the request for cancellation) as demonstrated below:

  public Task<long> CalcFactorialAsync(int x, CancelableProgressor progressor)
  {
    return QueuedTask.Run<long>(() =>
    {
      long result = 1;
  
      for (int i = 1; i < x; ++i)
      {
        if (progressor.CancellationToken.IsCancellationRequested)
          throw new OperationCanceledException();
  
        result *= i;
      }
  
      return result;
    });
  }

Using the Integrated Progress Dialog within Asynchronous Methods

If the Progressor has been configured to show progress, the running task can update what information is displayed on the progress dialog box using the progressor (both Progressor and CancelableProgressor support progress dialogs):

  public Task<long> CalcFactorialAsync(int x, Progressor progressor)
  {
    return QueuedTask.Run<long>(() =>
    {
      long result = 1;
  
      for (int i = 1; i < x; ++i)
      {
        progressor.Message = string.Format("Working on step:{0}", i);
  
        result *= i;
      }
  
      return result;
    }, progressor);
  }

Common complications

Constant state assumptions

Consider the following example authored by an add-in developer. This call is invoked from the GUI thread, and the intent here is to delete the specified layer from the active view’s map.

  private Task DeleteSelectedLayerAsync(Layer layer)
  {
    return QueuedTask.Run(() =>
    {
        MapView.Active.Map.RemoveLayer(layer);
    });
  }

Though straightforward in appearance, this function will occasionally result in an exception when put into use within the application. The mistake here was to assume that the state of the system remains static across threads. Previously queued operations may be running, and these need to complete before another operation can start executing. During that time, the state of the application may change due to user interaction or the result of operations still running. In this case, the active view may have become a table before the lambda actually started executing, in which case, the map will be null resulting in an exception. The safe approach is to avoid “chaining” calls on member variables or variables passed between threads; use local variables as a snapshot of the application state when the method was called since they won’t change out from under you.

  private Task DeleteSelectedLayerAsync(Layer layer)
  {
    // Take a “snapshot” of the map on the active view.
    Map m = MapView.Active.Map;
    return QueuedTask.Run(() =>
    {
        m.RemoveLayer(layer);
    });
  }

Programmers in a multithreaded environment should also code defensively. Consider a task that alters how a particular layer is symbolized. If such a task ends up queued behind another task that happens to remove this same layer from the map, the second operation is logically invalidated by the first. To handle this properly, the second task should be coded to display a warning, or abort the operation silently when it learns that the layer was deleted.

Thread safe data binding with WPF

By default, WPF data bound collections must be modified on the thread where the bound WPF control was created. This limitation becomes a problem when you want to fill the collection from a worker thread to produce a nice experience. For example, a search result list should be gradually filled as more matches are found, without forcing the user to wait until the whole search is complete.

To get around this limitation, WPF provides a static BindingOperations class that lets you establish an association between a lock and a collection (e.g., ObservableCollection<T>). This association allows bound collections to be updated from threads outside the main GUI thread, in a coordinated manner without generating the usual exception.

  BindingOperations.EnableCollectionSynchronization(Items, _lockObj); 

In the example above, the _lockObj member variable—of type Object—is typically instantiated when the containing class is created and will serve as the coordinating lock. Once EnableCollectionSynchronization is called, WPF will enter the specified lock whenever reading from or writing to the bound collection. As the owner of the collection, you are likewise obligated to enter the lock when reading from or writing to the collection.

ReadOnlyObservableCollection wrappers are commonly used to enforce read-only semantics on observable collection properties. To properly set up the multithreaded synchronization, you’ll need to call EnableCollectionSynchronization on the wrapper instead of the collection itself, since it’s the wrapper that WPF will actually be binding to.

  internal class HelloWorld
  {
    private ObservableCollection<string> _items = new ObservableCollection<string>();
    private ReadOnlyObservableCollection<string> _itemsRO;
    private Object _lockObj = new Object();
    internal HelloWorld()
    {
      _itemsRO = new ReadOnlyObservableCollection<string>(_items);
      BindingOperations.EnableCollectionSynchronization(_itemsRO, _lockObj);
    }
  // The public property used for binding 
  public ReadOnlyObservableCollection<string> Items { get { return _itemsRO; } }

  //Within the worker function below, the lock is entered before altering the collection:
  public void FillCollectionAsync()
  {
    QueuedTask.Run(() =>
    {
      // Reads and Writes should be made from within the lock
      lock (_lockObj)
      {
         _items.Add( GetData() );
      }
    });
  }

“Live” objects as properties

Care should be taken when exposing objects—especially collections—as public properties if the collection is likely to change on a separate thread. If someone gets and holds such a property and later starts enumerating through it thread A, an exception may be generated if your own code modifies the collection on thread B since there is no lock to collaborate with. Handing out read-only snapshots of the collection is safer.

Invoking code on the GUI thread

There are occasionally instances where, while your code is running along on a worker thread, you encounter a situation where you need to ask for input from the user before proceeding. You should not try to present a dialog directly from the worker thread as windows have thread affinity. A window or dialog created on the worker thread will not connect to the GUI thread’s input queue and will not honor the z-order and focus policy set by the GUI thread. In general, you can execute code on the GUI thread from a worker thread using the application’s dispatcher object.

This can be done synchronously.

  FrameworkApplication.Current.Dispatcher.Invoke(()=>
  {
    // Do something on the GUI thread
    System.Windows.MessageBox.Show("Ready!");
  });

Or asynchronously:

  FrameworkApplication.Current.Dispatcher.BeginInvoke(()=>
  {
    // Do something on the GUI thread
    System.Windows.MessageBox.Show("Ready!");
  });

Developers should try to collect needed information from the user on the GUI thread before executing work so that you don’t have to use this trick. Blocking calls made between threads risk deadlocks and hold up operations running on the worker thread.

Asynchronous exception handling

Like synchronous functions, asynchronous functions can throw exceptions. This introduces an interesting problem since the caller provides the try/catch on one thread, and the exception is thrown on another. In addition, the calling frame isn’t usually still on the stack when the exception is thrown.

However, .NET allows you to use async/await with try/catch so that if an exception is thrown by the code executing within the task, you’ll be able to catch back where the asynchronous function was called. Note that the asynchronous function must return Task or Task<T> for asynchronous exceptions to be properly conveyed (not void).

  try
  {
    var result = await PrintMapAsync();
  }
  catch (Exception e)
  {
    // handle exception.
  }

If an exception is thrown from the worker and you didn’t provide a try/catch around where you awaited the call, the .NET runtime will plug the exception—as an inner exception—into a UnobservedException.

Unobserved exceptions usually show up only when the exception object is collected by .NET’s garbage collection thread, nowhere near where the exception actually occurred. If you get one of these, examine the inner exception to obtain the faulting call stack. In VisualStudio’s watch window, you can use the $exception pseudo variable to examine the current exception object.

Freezable Objects

WPF defines a pattern where certain kinds of objects can be “frozen.” Once an object is frozen, changes cannot be made to the object without generating an exception. Freezing objects can improve performance in some situations, and it also lets you share the object between threads (see thread affinity). For example, if a BitmapImage is created on a worker thread, you cannot later use it on the GUI thread unless you freeze it first.

Consider a common case where databinding is used in conjunction with images that have been generated on a worker thread. The example class VM below, is exposes a property called Img:

  public class VM : INotifyPropertyChanged
  {
    public BitmapImage Img { get { return _image; } }
    ...
  }

This property returns an instance of BitmapImage (a Freezable object) which is then databound to a button in XAML:

  <Button>
    <Image Source="{Binding Img}"></Image>
  </Button>

The underlying bitmap is periodically updated on the worker thread as follows; note that the bitmap will be created on the worker thread:

  public Task Refresh()
  {
    return QueuedTask.Run(()=>
    {
      var uri = GenerateThumbnail();
      Img = new BitmapImage(uri);
    });
  }

In the process of rendering the user interface, WPF will attempt to access the bitmap property from the GUI thread… but this will result in an exception because the Bitmap is still unfrozen and thus anchored to its parent worker thread. This issue can be resolved by simply Freezing the Bitmap after updating it.

  var uri = GenerateThumbnail();
  Img = new BitmapImage(uri);
  Img.Freeze();

Note: not all classes that inherit from System.Windows.Freezable can be frozen. Use the CanFreeze property to verify.

Further reading

Task Asynchronous Pattern - MSDN

Best practices in Asynchronous Programming – Stephen Cleary

The MVVM design pattern - MSDN

Developing with ArcGIS Pro

    Migration


Framework

    Add-ins

    Configurations

    Customization

    Styling


Arcade


Content


CoreHost


DataReviewer


Editing


Geodatabase

    3D Analyst Data

    Plugin Datasources

    Topology

    Object Model Diagram


Geometry

    Relational Operations


Geoprocessing


Knowledge Graph


Layouts

    Reports


Map Authoring

    3D Analyst

    CIM

    Graphics

    Scene

    Stream

    Voxel


Map Exploration

    Map Tools


Networks

    Network Diagrams


Parcel Fabric


Raster


Sharing


Tasks


Workflow Manager Classic


Workflow Manager


Reference

Clone this wiki locally