ProConcepts Custom Items - kataya/arcgis-pro-sdk GitHub Wiki

In Pro, all content is represented as Items (Item topic 9110). To learn more about items in Pro consult ProConcepts: Project Items. In this ProConcepts we cover how to 'customize' ArcGIS Pro's Catalog to include "custom items" and "custom project items".

Language:      C#
Subject:       Content
Contributor:   ArcGIS Pro SDK Team <[email protected]>
Organization:  Esri, http://www.esri.com
Date:          11/24/2020  
ArcGIS Pro:    2.7  
Visual Studio: 2017, 2019

In this topic

Overview

In the Pro Catalog panes, all content is an "Item". Content that is browsed on disk or online that is external to the project are considered items. Content that is persisted into a project (aprx) are considered project items. Custom items are a special type of item that allows 3rd parties to add (or "include") their proprietary file types into Pro and to define the custom behavior, context menu items, etc. for "that" particular content type. Without the custom item, Pro would "skip" the corresponding file content item when browsing or searching on disk and it otherwise would not be shown (in catalog panes or browse dialogs).

Custom items, however, are not a mechanism for extending or overriding existing item types already recognized by Pro (not all of which are shown in catalog or browse). If Pro already has an Item implementation for a particular file type then attempting to replace it or override it with a custom item will be ignored. The complete list of content types already supported by Pro can be found in the supported data types and items document in the Pro help. None of these data types can be implemented with a custom item.

In the screenshot below, a custom item has been defined for a file content type of ".quake" and as a result, "quake content" is shown in the catalog pane whenever a ".quake" file is browsed on disk (".quake" is a made-up xml file format used in the ProjectCustomItemEarthQuake sample to illustrate use of custom items)

Sample Custom Item

In the second screen shot, the same ".quake" file has also been added as content to the project. Notice that earthquakes.quake appears under a custom project container "Earthquakes". Adding ".quake" content to the project (to include the "Earthquakes" container) requires implementation of a custom project item (which derives from the custom item base class). Custom project items (or references to their underlying file content) are persisted in the project whenever the project is saved.

Sample Project Custom Item

A Use Case for Custom Items

One of the primary use cases for custom items is their use in-conjunction with a plugin datasource. Plugin datasources are custom datasources that can be used by Pro to read proprietary GIS data as a PluginDataStore with (read only) access to its content as tables and feature classes (same as a plugin workspace provides via 10.x Arcobjects in Arcmap).
In addition to being able to add the custom datasource content (via the plugin) to maps and scenes, it is also typically desirable to add search and browse support for the custom datasource content as well. This is where the custom item comes in. Pairing a custom item with a plugin datasource allows your custom content to be integrated both into the Pro catalog and browse experience and as a consumable datasource for content in the map as well.
The combination of a Plugin datasource and Custom Item is illustrated in the ProDataReader sample. The ProDataReader uses a plugin to consume a variety of different data types including photos with GIS metadata (.jpg), and GPX tracking data (.gpx) files. ProDataReader uses custom items to integrate the browsing and consumption of the different data types seamlessly into the Pro catalog.

ProDataReader sample:

Sample Custom Item and Plugin Datasource

A Note for Developers

When working on Custom Items (or Custom Project Items) be aware that Custom Item classes are also registered for use by the ArcGIS Pro Indexer when ArcGIS Pro starts. This registration is done through an entry in this file:

%AppData%..\Local\ESRI\SearchResources\ItemInfoTypesExt.json

Usually this is of no consequence, but it is advisable to delete this file in-between sessions when you are developing your Custom Item and when you are refactoring your code.
It may also be beneficial to disable Pro indexing on your development machine (in which case the ItemInfoTypesExt.json file will be ignored). Disable Pro indexing via the Pro backstage "Options". Select "Indexing" within the Application group and click on the "Don't create index" option.

Custom Items

Any implementation of a Custom Item concrete class has to derive from CustomItemBase (Item topic 25989) which in turn derives from the Item base class (Item topic 9110).

Any Custom Item implementation also requires an entry in config.daml, specifically it requires a registration entry under the esri_customItems category. To register a Custom Item class a new component needs to be added to the esri_customItems category. This newly added component defines the file extension which triggers a Custom Item and also provides the name of the CustomItem code-behind class that is handling the processing of this Custom Item.

File Extensions

Custom Items are normally triggered by browsing to a specific file extension which is specified in config.daml, however, there are some restrictions on the file extensions that can be used:

  • File extensions, not folder extensions, during search or browse trigger a Custom Item*. Directory names, even if they have an extension, (think file geodatabase ".gdb") do not trigger custom items.
  • File extensions specified in the config.daml are case sensitive, hence only files with file extensions that are matched using a case sensitive compare will work.
  • File extension already in use by ArcGIS Pro (including portal, on-line, geodatabases, toolboxes, etc) cannot be overridden/extended with a custom item. This includes text files, xml files, folders, database connection files, layer files, task files, mxds, sxds, etc. Custom items defined on content types in use by ArcGIS Pro will be ignored.

*Custom items can also be containers. Containers can contain content that is part of, or "a record in", a "parent" file as is the case with the quake sample. Container items are responsible for retrieval of their child items from the "contents" of their file. Retrieval of child as items is triggered by a Fetch on the parent item. Within Fetch, the parent item reads its content and provides the requisite "child" custom items to be shown in Pro.

Note: You can use a Windows link file (.xlnk) as a custom item container. This is a convenient way of grouping together a collection of files within a "container" that can be displayed in Pro. As the link file "is a" file, Pro defers to the custom item (assuming there is one) to read the link file contents via a Fetch. The ProDataReader Sample illustrates use of a link file.

esri_customItems Category

Custom Items are declared in DAML and must be registered as components in the esri_customItems category. The newly added component specifies:

  • A unique identifier. This will correspond with the item "type".
  • The class name of the custom item implementation used to implement custom behavior
  • A container type (for custom project items only) that is the name or "type" of its associated project container
  • A <content> child element containing:
    • A display name - this is the default "Display Type" of the item.
    • The associated file extension (optional). Files with this extension will be associated with your custom item.
    • Any keywords relevant for search.
    • An associated context menu (optional)
    • A <filterFlag> child element that allows you to define default behavior for the custom item. The values of the <type> child element can be any one of the values of the BrowseProjectFilter.FilterFlag Enumeration For example, if you use the AddToMap flag, the custom item will be displayed in Pro's Add To Map browse dialog.

Note: The Pro SDK custom item template will add the required daml with default values and category component registration automatically for you.

<categories>
 <updateCategory refID="esri_customItems">
   <insertComponent id="CustomItem_ProCustomItem1" className="ProCustomItem1">
     <content displayName="ProCustomItem 1" fileExtension="xyz123" isContainer="false" 
			   keywords="ProCustomItem 1" 
			   contextMenuID="CustomItem_ProCustomItem1_ContextMenu">
         <filterFlags>
            <!--Change this flag as needed. See FilterFlag enum. Example: AddToMap, DontBrowseFiles...-->
            <type id="File" />
          </filterFlags>
        </content>
   </insertComponent>
 </updateCategory>
 ...
</categories>

Implementation

Implementations of Custom Items derive from the ArcGIS.Desktop.Core.CustomItemBase class. Custom items should implement overrides of the base behavior as needed. Some overrides to consider are the default image sources associated with your Custom Item in the ArcGIS Pro Catalog.

The Pro SDK item template provides overrides for the following items "out-of-the-box":

internal class MyCustomItem : CustomItemBase {
	public override ImageSource LargeImage {
		get ...
	}

	public override Task<ImageSource> SmallImage {
		get ...
	}
	
	public override bool IsContainer => false;
}

LargeImage and SmallImage specify the icon(s) used within catalog panes and browse dialogs. They also serve as a visual cue that the file has been recognized as a Custom Item (because it is displayed with "your" icon). IsContainer is false by default meaning this Custom Item has no child Custom Items within its content.

Assuming the custom item shown in the above daml and code snippet is implemented, and proprietary file content with the extension .xyz123 (from the daml fileExtension="xyz123" attribute) is encountered, Pro would display the file:

Basic Custom Item Sample

Custom Item Containers

Custom Item file content may contain data elements that need to be displayed as children of the file in the ArcGIS Pro Catalog TreeView. For example, a file that contains "records" where each record "is an" item or a proprietary database accessed via a connection file where the database contents are shown:

Custom Item with Children

To implement container behavior in your custom item:

  1. Override IsContainer to return true.
  2. Implement the Fetch method. Fetch is executed either when your catalog node is expanded for the first time or when a "Refresh" is executed on your container or its parent container via the catalog or browse UIs*. Within Fetch, the "child" items must be added to the item child collection (typically using AddRangeToChildren(...) - see the table of child item management members below).

*To "force" a fetch on your own container you can call GetItems() on yourself (i.e. this.GetItems()). Avoid calling GetItems within Fetch itself as it will cause recursion.

An example implementation of Fetch:

 internal class MyCustomItem : CustomItemBase  {
   public override ImageSource LargeImage ...

   public override Task<ImageSource> SmallImage ...
	
   public override bool IsContainer => true;
	
   public override void Fetch() {
      //clear out any existing child items
      this.ClearChildren();

      //read child items from the item content per its format - whatever that may be
      //In this case, the content is simply a hard coded list of item names
      var child_items = new List<ChildCustomItem>();
      var itemNames = new List<string> { "Item 1", "Item 2", "Item 3" };
      foreach (var name in itemNames) {
         //catalog path must be unique for the item to be unique
	 var uniquePath = System.IO.Path.Combine(this.Path, name);
         //Create the child item. 
	 var child = new ChildCustomItem(name, uniquePath, ChildCustomItem.TypeID);
	 child_items.Add(child);
      }
      //add the collection of child items to the container "children" collection
      this.AddRangeToChildren(child_items);
   }
  }

  //This is the ~child~ custom item. Note: it does not necessarily need to be registered in the 
  //config.daml - especially if they are never browsed on disk...
  internal class ChildCustomItem : CustomItemBase {
     
     internal static readonly string TypeID= "acme_childcustomitem";
     ...
  }

The resulting ArcGIS Pro Catalog browsing experience looks like this:

Custom Item with Children Sample

Considerations
The catalog path property for a given item must be unique (otherwise it is assumed to point to the "same" item with the duplicate catalog path - similar to how the path and filename combination is used in the file system). Containers can add, remove, and retrieve child items with the following methods:

Member Description
void AddRangeToChildren(IEnumerable items, bool bBrowsingFilesMode = false)* Add the collection of items to the child collection. Keep bBrowsingFilesMode = false (default).
void ClearChildren()* Remove all children from the child collection
Item[] GetChildren() Retrieve the collection of child items (this is a copy).
IEnumerable GetItems() Browses the contents of an item to include child items - must be called on the QueuedTask
bool HasChild(Item item)* True if the item is found in the child collection
bool HasChildren { get; }* True if the item has children
void InsertChild(int index, Item item)* Insert the child item at the given index
bool RemoveChild(Item item)* Removes the specified item from the child collection
void RemoveRangeFromChildren(IEnumerable items) Remove the collection of items from the child collection
  • Known Issue: Those methods do not show in the API Reference. This will be fixed in the next release. They will still appear within Visual Studio Intellisense.

Context Menus

Actions for Custom Items are usually initiated through a context menu. You can define a context menu in the DAML by specifying a contextMenuID in the content tag of the component definition of the Custom Item as shown here:

<updateCategory refID="esri_customItems">
  <insertComponent id="CustomItem_ProCustomItem1" className="ProCustomItem1">
    <content displayName="ProCustomItem 1" fileExtension="xyz123" isContainer="false" 
			 keywords="ProCustomItem 1" 
			 contextMenuID="CustomItem_ProCustomItem1_ContextMenu" />
  </insertComponent>
</updateCategory>

You can also specify the context menu programmatically via the ContextMenuID property. In this example, ChildCustomItem sets its context menu id within its constructor:

  internal class ChildCustomItem : CustomItemBase {
     ...
     public ChildCustomItem(string name, string catalogPath, string type) 
						: base(name, catalogPath, type) {
	this.ContextMenuID = "CustomItem_ProCustomItem1_ContextMenu";
     }
     ...

It is assumed that the menu "CustomItem_ProCustomItem1_ContextMenu" is defined within the add-in config.daml.

Custom Item Context Menu

Within your context menu items (typically buttons), use the catalog pane's SelectedItems property to retrieve the current (catalog) context. The selected item(s) can then be used for the desired custom action:

 internal class CustomContextMenuOption : Button {
   ...
   protected override void OnClick() {
     var catalog = Project.GetCatalogPane();
     var items = catalog.SelectedItems;//the current context

     // for this example only look at the first selected item
     // however the Catalog Pane supports multi-selection
     var item = items.OfType<ProCustomItem1>().FirstOrDefault();
     if (item == null)
	return;
     MessageBox.Show($"Selected Custom Item: {item.Name}");
}

Renaming

To implement renaming support:

  1. Override CanRename to return true.
  2. Override OnRename(string newName) to implement your renaming logic*.

*If a file is being renamed then the item Path must also be updated (see the example below).

Note: the associated file name for a custom item is assumed to be it's "item" name. Therefore, if you are renaming an item and it is associated with a file on disk (the most common scenario) then the custom item is responsible for renaming the underlying file on the file system and updating this.Path. Said another way, you cannot rename an item without renaming its underlying file (if it has one).

For example:

  internal class MyCustomItem : CustomItemBase {
     //return true 
     protected override bool CanRename => true;

     //implement renaming logic in OnRename
     protected override bool OnRename(string newName) {

        //In this case we have to rename a file on disk
        var new_ext = System.IO.Path.GetExtension(newName);
        if (string.IsNullOrEmpty(new_ext)) {
	   // add the file extension if need be
	   new_ext = System.IO.Path.GetExtension(this.Path);
	   newName = System.IO.Path.ChangeExtension(newName, new_ext);
         }
         var new_file_path = System.IO.Path.Combine(
		System.IO.Path.GetDirectoryName(this.Path), newName);
         //move the physical file to the "new" name
         System.IO.File.Move(this.Path, new_file_path);
         this.Path = new_file_path; //we ~must~ update the item Path to keep 
                                    //name and path in-sync
         return base.OnRename(newName);
     }
}

To add rename to an item context menu, we can simply use the existing out-of-box renaming button esri_core_rename button and add it to the Custom Item's context menu in the config.daml:

 <menu id="MyCustomItem_ContextMenu" caption="Custom Item Context Menu">
   <button refID="AddToProject" />
   <button refID="RemoveFromProject" />
   <button refID="esri_core_rename" /><!-- "out-of-box" rename -->
 </menu>

Custom Item Context Menu

Custom Item Context Menu

An item that overrides CanRename to return true also "picks up" built-in support for rename. The F2 key toggles the selected item node into rename mode (if CanRename = true ) same as if a rename context menu item had been selected.

Custom Project Items

Custom Project Items are custom items that can be persisted in your ArcGIS Pro project file (same as maps, layouts, styles, toolboxes, folders, etc.). Custom Project Items can also be included in Project package files and Project Templates. To implement Custom Project Items, derive from the ArcGIS.Desktop.Core.CustomProjectItemBase base class. Custom project items also require a project item container (to hold the custom project item content). To implement a Custom Project Item you must also implement a Custom Project Item Container class. The project item container is associated with the custom project item within the config.daml. The details are provided in the following sections.

Custom Project Item Overview

Implementation

Any implementation of a Custom Project Item concrete class has to derive from CustomProjectItemBase (Item topic 26007) which in turn derives from CustomItemBase (described previously) and, ultimately, from the Item base class itself (Item topic 9110).

Custom Project Items must also be registered within the esri_customItems category (same as "regular" custom items) in the config.daml. Custom project items have an additional attribute on their daml component - "containerType" - which must be set to the content "type" attribute of the project item container with which the custom project item is associated. Custom project items must be associated with a container. When you run the custom project item template from the Pro SDK it will create a custom project item and an associated custom project item container for you.
Containers derive from CustomProjectItemContainer<T> where "T" must be the custom project item "type" the container contains. The container class must be registered in the "esri_core_projectContainers" category within the config.daml. All Custom project item containers have an associated "container type" which is an arbitrary unique string used to identify the container within the project container collection (and will be referenced by the custom project item "containerType" daml attribute).

Using the config.daml and the concrete implementations from the ProjectCustomItemEarthQuake as our example...This is the custom quake project item daml (summarized):

<updateCategory refID="esri_customItems">
  <!-- notice the containerType="QuakeContainer" attribute -->
  <insertComponent id="acme_quake_handler" className="..." containerType="QuakeContainer">
     <content displayName="..." fileExtension="quake" isContainer="true"... />
  </insertComponent>

This is the associated custom quake project item container daml (summarized):

 <updateCategory refID="esri_core_projectContainers">
    <insertComponent id="QuakeItem_FolderContainer" className="..." ...>
        <!-- notice the type="QuakeContainer" attribute matches the item's 
             containerType attribute value. This is how the two are associated -->
	<content type="QuakeContainer" displayName="..." ... />
    </insertComponent>

Custom Project Item Container

Custom project item containers are shown in the catalog panes as top-level, or "Root", nodes. By default all project item containers are not visible in catalog unless they contain project item content. The exceptions to this rule are the default Pro project item containers for Maps, Toolboxes, Databases, Layouts, Styles, Folders, and Locators which are always visible whether they contain content or not.

To add custom project items to a project, the add-in can either explicitly retrieve the instance of the (respective) custom project item container and add the item to it directly or implicitly by adding the item to the project instance which will, in turn, trigger a call to the custom container to add the content. If the container was empty previously then it will become visible in the Catalog pane once it has at least one item of content. The container must not already contain the item when the call to add the content (explicitly or implicitly) is made:

 QuakeProjectItem item = ....;

 //either explicit...
 //get the associated container for the item
 var container = Project.Current.GetProjectItemContainer(
            item.Type) as QuakeProjectItemContainer;
 //add the item
 container.AddItem(item);

 //or implicit...
 //add the item to the project and the project will add it to the container
 QueuedTask.Run(() => Project.Current.AddItem(item));

Whenever content is to be added to a custom project item container implicitly, it's corresponding CreateItem method is always called. CreateItem will always be called whenever a custom project item associated with the container is added to the project (via GetItem) or a custom project item is being "re-hydrated" from persisted data (saved in the project) when the project is opened. Custom containers are responsible for instantiating the item whose details are passed in to CreateItem. The item should be added to the container's child project item collection if it is successfully created.

Note: Custom containers can override the CreateItemPrototype method instead of CreateItem. The default behavior of the base class CreateItem is to defer creation of the item to CreateItemPrototype. Therefore, overriding either CreateItem or CreateItemPrototype is acceptable and comes down to preference. CreateItemPrototype is never called directly by Pro.

  //default behavior of CreateItem in the base class
  public override Item CreateItem(string name, string path, string containerType, 
                                  string data) {
      //default implementation defers to CreateItemPrototype
      return CreateItemPrototype(name, path, containerType, data);
  }

In the quake sample, the container overrides CreateItem but both implementations are shown here:

 internal class QuakeProjectItemContainer : CustomProjectItemContainer<QuakeProjectItem> {
   ...
 //either override CreateItem...
 public override Item CreateItem(string name, string path, string containerType, 
                                 string data) {
     var item = ItemFactory.Instance.Create(path);
     if (item is QuakeProjectItem) {
        //add the item to the container's children
	this.Add(item as QuakeProjectItem);//This will make the container visible in
                                           //Catalog if, previously, it was empty
     }
     return item;
 }

 //Or...leave CreateItem unchanged from the base class implementation and consolidate
 //logic in CreateItemPrototype...
 public override Item CreateItemPrototype(string name, string path, string containerType, 
                                          string data) {
     var item = ItemFactory.Instance.Create(path);
     if (item is QuakeProjectItem) {
        //add the item to the container's children
        this.Add(item as QuakeProjectItem);//This will make the container visible in
                                           //Catalog, if, previously, it was empty
     }
     return item;
 }

CreateItem is always passed the name of the content; path to the content; the container type (which should match the container type of your container); and a string parameter called "data". The "data" parameter can be ignored. It is for internal use only.

Persistence

The primary difference between a custom item and a custom project item beyond the container association of the custom project item is that project items can be persisted in the project aprx. Within the implementation of your concrete custom project item, you must provide an override for the OnGetInfo method to support persistence.
OnGetInfo is called when a project item has been added to a project (marking the project as "dirty") and the project aprx is saved. OnGetInfo must return the information necessary to persist the project item in the aprx. This consists of its name, path, and container type wrapped into a class called ProjectItemInfo. When the project is re-opened, that same information will be extracted from the persisted ProjectItemInfo and passed as arguments to the relevant custom project item container CreateItem method (discussed in the previous section) to rehydrate the item.

In the following example, the QuakeProjectItem provides an override of OnGetInfo():

 internal class QuakeProjectItem : CustomProjectItemBase {
   ...
  
  //called on save when the project is dirty
  public override ProjectItemInfo OnGetInfo() {
     return new ProjectItemInfo {
  	Name = this.Name,
  	Path = this.Path,
        //it is critical that the item returns the ~correct~ container 
        //type. This must match the "containerType" from the config.daml
  	Type = QuakeProjectItemContainer.ContainerName
     };
  }

OnGetInfo returns a ProjectItemInfo object which must uniquely identify the Custom Project Item. Note that the ProjectItemInfo.Type property value has to match the item's daml containerType attribute for the custom project item container.

Packages and Templates

To mark your Custom Project Item as content to be included within a project package or a project template (assuming that an aprx containing your custom project item content is being packaged or saved as a template file), custom project items or other add-in code (usually the container CreateItem implementation) must call their public void IncludeInPackages(bool includeInPackages) method with includeInPackages = true. This will flag the custom project item as both needing to be included in the package or template and will consolidate its underlying content into an internal project package/template folder to be included within the package/template archive (the default is to not include custom project items in packages or templates).
Assuming this.IncludeInPackages(true), the path to the content contained within the item will be persisted as a relative path by Pro that points to the consolidated content within the package hierarchy.

  //either in the item call IncludeInPackages
   this.IncludeInPackages(true);

  //or externally...a logical place is within the container CreateItem...
  public override Item CreateItem(string name, string path, string containerType, 
                                  string data) {
     var item = ItemFactory.Instance.Create(path);
     item.IncludeInPackages(true);
     ...
⚠️ **GitHub.com Fallback** ⚠️