ProConcepts Framework

Uma Harano edited this page Jun 26, 2018 · 20 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# and Visual Basic
Subject:       Framework
Contributor:   ArcGIS Pro SDK Team <arcgisprosdk@esri.com>
Organization:  Esri, http://www.esri.com
Date:          6/7/2018  
ArcGIS Pro:    2.2  
Visual Studio: 2015, 2017

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

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="1.1.2829">
    <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>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 an existing tab beside 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="1.1.2829">
    <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

Plug-ins

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; }
    protected internal 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
  • 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).

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.
DockPane DockPane is shown based on the associated condition. The DockPane object will not be loaded or created until the condition is initially met.
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. First a category is declared that is purely declarative—it has no active portion (code behind). The following DAML fragment shows an example category declaration:

  <categories>
    <insertCategory id="ProjectContainers"/>
  </categories>
  
  Next, anyone who wants to register a type in a category does so as shown below. A component can optionally have a content subelement where it can define any DAML you want. In the following example, just a few attributes have been added, but this can be any DAML desired:
  <categories>
    <updateCategory refID="ProjectContainers">
      <insertComponent id="MapContainer" className="MapContainer">
        <content type="Map" displayName="Maps"/>
      </insertComponent>
    </updateCategory>
  </categories>

You can also associate the category to any DAML commands by using categoryRefID and using the content subelement to define any custom data.

  <button id="openMap" caption="Open" className="Controls.Cut" categoryRefID="ProjectContainers">
    <tooltip/>
    <content type="Map" displayName="Maps"/>
  </button>

At run time, you can use the GetComponentElements function to retrieve all the components registered in a particular category. When a component is returned, you can call GetContent to get a Systm.DAML.Linq.XElement for the content node or ReadAttribute to simply read a string attribute. Call CreateComponent to instantiate a new instance of the component. Note CreateComponent throws exceptions when the category is associated to an existing DAML command.

  Collection<ArcGIS.Desktop.Framework.ComponentElement> components;
  components = Categories.GetComponentElements("ProjectContainers");
  
  // Check the components
  foreach (ComponentElement component in components)
  {
    string value = component.ReadAttribute("type");
    if (value != key)
      continue;
  
    // ProjectItemContainers have a two stage creation mechanism
    ProjectItemContainer container = null;
    try
    {
      container = component.CreateComponent() as ProjectItemContainer;
      if (container == null)
        return null;

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 in 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.

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 at 2.0. 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. Use the Checked property for a checked appearance.

  <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();
    }
  }

Check box

Check boxes work exactly the same as buttons.

  <checkBox id="acme_AutoSave"
            className="AutoSave"
            caption="Auto Save"
            keytip="AS">
    <tooltip heading="Auto Save" image="Images\AutoSave16.png">Automatically save edits.</tooltip>
  </checkBox

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 following UserControl 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 edits doubles 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"
              condition="acme_Mapping">
      <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. Dock panes are also conditional; they can be made to appear automatically whenever the application reaches a defined state. If a dock pane does not specify a state, it is considered global and will be presented automatically at startup. See the Conditions section for more details.

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.

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.

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

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.

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. 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 labeled “Home”:

  <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 in colored, named groups to improve clarity. These collections are called tab groups. On the ribbon displayed below, there is a tab group containing three tabs:

FeatureLayer2.png

Tab groups are declared within a tabGroups collection element and consist of an id, color, and caption. Tabs are associated with a tab group using the tabGroupID attribute.

  <tabGroups>
    <tabGroup caption="FEATURE LAYER" id="esri_mapping_featureLayerTabGroup">
      <color A="255" R="238" G="170" B="90" />
      <borderColor A="0" R="251" G="226" B="195" />
    </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.

Subgroups.png Subgroups2.png Subgroups3.png Subgroups4.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>

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>

Wizard

Wizards are somewhat like Property sheets, they are declared at the root level (outside of a module). Wizards are themselves purely declarative, you just have to create and list the pages. You cannot update a wizard. Wizards can either hide or show a list of their page constituents (see graphics below). If the list of pages is visible, users can jump to any page at any time unless CanJumpTo on the target page objects. A subtle red underscore indicates the currently invalid pages – the page tooltips also signal invalidity. When the page list is hidden, users cannot advance to the next page until the current page is valid. Finish is enabled only when all the pages are valid.

This DAML example shows the definition for the geodatabase "New Feature Class" wizard:

<wizards>
    <insertWizard id="esri_Geodatabase_FeatureClassWizard" caption="New Feature Class"
                  pageHeight="400" pageWidth="600"
                  isPageListVisible="true"
                  resizable="true">
      <page id="esri_Geodatabase_FeatureClassPage1" caption="Name" className="Wizards.WizardPage1">
        <tooltip>Step 1</tooltip>
        <content className="Wizards.WizardView1"/>
      </page>
  </wizards>

wizard

Use the static FrameworkApplication.ShowWizard method to open a wizard. Pass the id as well as any data you want the pages to work with. If the wizard is cancelled, false is returned; Finish returns true.

protected override void OnClick()
{
  bool? success = FrameworkApplication.ShowWizard("esri_Geodatabase_FeatureClassWizard", new TestData());
  ...

You create pages for the wizard by inheriting from WizardPage

internal class CustomWizardPage1 : ArcGIS.Desktop.Framework.Contracts.WizardPage {

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:Button_Burger>
                <frameworkControls:Button_Burger.PopupMenu>
                    <ContextMenu>
                        <MenuItem Header="Item 1"/>
                        <MenuItem Header="Item 2"/>
                    </ContextMenu>
                </frameworkControls:Button_Burger.PopupMenu>
     </frameworkControls:Button_Burger>

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

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

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, 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)))
      {
        if (LicenseInformation.IsAvailable(lc))
        {
          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 support

To drag and drop objects in a WPF application, you need to set the AllowDrop property to true for the target window, and add a drop event handler. See WPF tutorial.net for detailed steps.

The ArcGIS Pro framework help supports drag and drop operations for Panes and Dockpanes by providing the virtual functions OnDragOver and OnDrop to simplify the implementation.

Drop support

Dropping in a pane

Panes automatically have their OnDragOver() called. If the pane wants to allow the drop, it should set the drop effects accordingly. The implementation of OnDrop can mark the drop operation as handled, or it can let the operation pass down to other drop handlers—if there are any—by setting Handle = False. For example:

  public override void OnDragOver(DropInfo dropInfo)
    {
      if (dropInfo.Data is LayerViewModel && dropInfo.TargetItem is MapViewModel)
      {
        // Choose drop target adorner to Highlight or Insert
        dropInfo.DropTargetAdorner = DropTargetAdorners.Highlight;

        // Choose drag drop effects to Copy, Move, Link or Scroll
        dropInfo.Effects = DragDropEffects.All;   
      }
    }

  public override void OnDrop(DropData dropInfo)
    {
      foreach (var item in dropInfo.Items)
      {
        if (System.IO.Path.GetExtension(item.Data as string) == ".aprx")
          item.Handled = true;
      }
    } 

However, if a pane contains WPF controls that have their own drag and drop logic, you can set the pane’s DAML attribute isDropTarget to false to allow the inner controls to handle the operation instead. The default is true.

Dropping in a dock pane

Dock panes also have the option of allowing the drop target to either be the entire DockPane itself or a specific inner control such as TreeView or ListView.

  • To drop to the DockPane, set the DAML attribute isDropTarget to true.
  <dockPane id="TOC" caption="Contents" className="DockPanes.TOCContents" 
            dock="left" initiallyVisible="true" isDropTarget="true"> 

The DockPane just needs to override OnDragOver and OnDrop as described above.

  • To drop to a specific child control, set the DAML attribute isDropTarget to false. Here the default is false, meaning the DockPane should specify a UI element in XAML as the drop target. In the following example, only the ListBox handles the drop:
  <UserControl x:Class="ArcGIS.Desktop.Mapping.TOC.TOCDockPane"
  xmlns:dragDrop="clr-namespace:ArcGIS.Desktop.Framework.DragDrop;assembly=ArcGIS.Desktop.Framework" >
  ……….
     <Style x:Key="bookmarkListBoxStyle" TargetType="{x:Type ListBox}">
        <Setter Property="dragDrop:DragDrop.IsDragSource" Value="True" />
        <Setter Property="dragDrop:DragDrop.IsDropTarget" Value="True" />
        <Setter Property="dragDrop:DragDrop.DropHandler" Value="{Binding}" />
        <Setter Property="dragDrop:DragDrop.DragHandler" Value="{Binding}" />
      </Style>

Since the drop handler binds directly to the DockPane, the DockPane can still override OnDragOver and OnDrop to process these events.

To set the drop handler to an object other than DockPane, modify the binding.

  <TreeView x:Name="TOC" Grid.Row="0" ItemsSource="{Binding Maps}" 
                uiFramework:DragDrop.IsDropTarget="True"
                uiFramework:DragDrop.DropHandler="{Binding TOCMapViewModel.TOCDropHandler}" 
                uiFramework:DragDrop.DragHandler="{Binding TOCMapViewModel.TOCDropHandler}">
  </TreeView>

In the example above, TOCDropHandler should return an instance of DropHandlerBase, which implements OnDragOver and OnDrop.

  internal class TOCMapViewDropHandler : DropHandlerBase,  IDragSource
  {….}
  
  _tocDropHandler = new TOCMapViewDropHandler(this);
  public TOCMapViewDropHandler TOCDropHandler
  {
    get
    {
      return _tocDropHandler; // Instance of DropHandlerBase
    }
  }

Dropping in a WPF window

The framework also helps support drag and drop operations in WPF windows. The steps are similar to those described above.

  1. Add drop handler in XAML:
  <UserControl x:Class="UIFramework.WPFWindowVM"
               xmlns:uiFramework="clr-namespace:ArcGIS.Desktop.Framework.DragDrop;
				assembly=ArcGIS.Desktop.Framework"
               uiFramework:DragDrop.IsDropTarget="True"
			   uiFramework:DragDrop.DropHandler="{Binding DropHandler}"
               d:DesignHeight="300" d:DesignWidth="350">
  1. Create a custom drop handler class:
  internal class CustomDropHandler : DropHandlerBase
  {
    public override void OnDragOver(DropInfo dropInfo)
    {

      dropInfo.Effects = DragDropEffects.All;
    }

    public override void OnDrop(DropData dropInfo)
    {
      System.Windows.MessageBox.Show("custom handler");
    }
  }
  1. In the window’s ViewModel, add the binding property DropHandler, which returns an instance of a CustomDropHandler:
  internal class WPFWindowVM : SomeBaseClass
  {
      private CustomDropHandler _drophandler;
      public object DropHandler
      {
        Get{return _drophandler;}
      }……….}
  }

Custom drop handlers

You can handle drop operations on an external DockPane (that supports custom handlers) by registering a custom drop handler in DAML. Each drop handler should specify its supported data types as either file extensions or object types. The drop handler class must inherit from ArcGIS.Desktop.Framework.Contracts.DropHandlerBase. When data is dragged over a Pane or DockPane and the data types match, the appropriate drop handler is instantiated, and its OnDragOver and OnDrop are called.

  <dropHandlers>
    <insertHandler id="h1" className="Panes.Drophandler"
		dataTypes=".aprx|.mxd|UIFramework.TestApp.DockPanes.TOCContents+MapViewModel"/>
    <insertHandler id="h2" className="Panes.Drophandler2" dataTypes="*"/>
  </dropHandlers>

Drag support

Implement the IDragSource interface on your drag source to customize the drag behavior:

   public void StartDrag(DragInfo dragInfo)
   {
      // Block certain type, or change DragDropEffects
      LayerViewModel layer = (LayerViewModel)dragInfo.SourceItem;

      if (layer.FullName != "layer 2")
      {
        dragInfo.Effects = DragDropEffects.All;
        dragInfo.Data = layer;
      }
   }

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.

  uiFramework: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.ImageUrl = @"pack://application:,,,/ArcGIS.Desktop.Resources;component/Images/ToastLicensing32.png";

      FrameworkApplication.AddNotification(toast);

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 NotificationManager, 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
    {
      Error,
      Warning,
      Information
    }

    public static bool IsEnabled
    public static void Write(EventType type, string entry, bool flush = false)
  }
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.

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 QuededTask.Run, they are automatically integrated with various features within the application as follows:

  • The extended Progress/Cancelation 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 cancelation 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.

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 cancelation

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 CancelationToken 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 Cancelation

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

Home

Developing with ArcGIS Pro


Framework

    Add-ins

    Configurations

    Customization

    Styling


Content


CoreHost


DataReviewer


Editing


Geodatabase


Geometry

    Relational Operations


Geoprocessing


Layouts


Map Authoring


Map Exploration

    Map Tools


Raster


Sharing


Tasks


Utility Network


Workflow Manager


Reference

    2.0 Migration

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.