ProGuide Context Menus - kataya/arcgis-pro-sdk GitHub Wiki

Language:      C#
Subject:       Framework
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

This ProGuide explains the step by step process on how to provide a context menu for a map tool. Context menus come in two flavors: A predefined menu, declared in the config.menu and a dynamic menu, where the menu options are configured "on-the-fly". Both types of context menu are covered in this guide.

In this topic

Prerequisite

The code examples used within this guide are available from the context menu sample.

Prerequisite

Create an add-in that will be used to contain your map tool(s) and context menu(s). In this ProGuide, the name of the addin project being used is "ContextMenu". In the add-in project, add a folder called "Menus" and a folder called "Ribbon".

Part 1. Basic Pattern

This is the basic, and most straightforward, pattern for displaying a context menu on a tool in Pro. It is ideal for those tools that do not need any special "right-click" handling logic.

Step 1. Create a Map Tool

Use the ArcGIS Pro Map Tool item template to add a map tool to your addin. Add it to the "Ribbon" project folder. Call the map tool ContextMenuTool1.

Step 2. Create a Menu

Create the map tool context menu. Use the ArcGIS Pro Menu item template. Add it to the "Menus" project folder. Name the menu "ToolContextMenu".

Menu Item Template

Check your addin project. Open the Config.daml. Verify the menu Item template added a definition for a menu containing (arbitrarily) three menu items - each of which is a button:

   <menus>
        <menu id="ContextMenu_Menus_ToolContextMenu" caption="ToolContextMenu" ...>
          <button refID="ContextMenu_Menus_ToolContextMenu_Items_Button1" />
          <button refID="ContextMenu_Menus_ToolContextMenu_Items_Button2" />
          <button refID="ContextMenu_Menus_ToolContextMenu_Items_Button3" />
        </menu>
      </menus>

...and, a definition for each of the buttons referenced on the menu:

  <button id="ContextMenu_Menus_ToolContextMenu_Items_Button1" caption="Menu Button 1" 
          className="ContextMenu.Menus.ToolContextMenu_button1" loadOnClick="true" 
          smallImage="pack://application:,,,/ArcGIS.Desktop.Resources;component/Images/GenericButtonGreen16.png" 
          largeImage="pack://application:,,,/ArcGIS.Desktop.Resources;component/Images/GenericButtonGreen32.png">
      <tooltip heading="Menu Button 1">ToolTip<disabledText /></tooltip>
  </button>
  ...
  <!-- Menu Button 2, Menu Button 3 -->
 ...

...and, a reference to the tool and menu in "Group 1" (which will be shown on the addin tab).

  <group id="ContextMenu_Group1" caption="Group 1" appearsOnAddInTab="true">
    <!-- host controls within groups -->
    <tool refID="ContextMenu_Ribbon_ContextMenuTool1" size="large" />
    <menu refID="ContextMenu_Menus_ToolContextMenu" size="middle" />
 </group>

Delete the context menu reference from "Group 1". We intend to show our menu as a context menu and not as a menu activated from the ribbon. Your edited Config.daml should now look like this (with the menu reference deleted):

  <group id="ContextMenu_Group1" caption="Group 1" appearsOnAddInTab="true">
    <!-- host controls within groups -->
    <tool refID="ContextMenu_Ribbon_ContextMenuTool1" size="large" />
 </group>

In the Menus folder, verify that a source file "ContextMenuTool.cs" was created containing the implementation of the 3 buttons added to the context menu (by the item template):

  internal class ToolContextMenu_button1 : Button {
     protected override void OnClick() {
     }
  }

  internal class ToolContextMenu_button2 : Button {
     protected override void OnClick() {
     }
  }

  internal class ToolContextMenu_button3 : Button {
     protected override void OnClick() {
     }
  }

Step 3. Edit Menu Button OnClick

Let's edit the button "OnClick" handlers for each of the menu item buttons. Add a message box to each click handler that shows the name of the button and the name of the currently active tool as the message box title: MessageBox.Show(this.Caption, FrameworkApplication.ActiveTool.Caption);.

The completed code for ToolContextMenu.cs should look like this:

  internal class ToolContextMenu_button1 : Button {
     protected override void OnClick() {
       MessageBox.Show(this.Caption, FrameworkApplication.ActiveTool.Caption);
     }
  }

  internal class ToolContextMenu_button2 : Button {
     protected override void OnClick() {
       MessageBox.Show(this.Caption, FrameworkApplication.ActiveTool.Caption);
     }
  }

  internal class ToolContextMenu_button3 : Button {
     protected override void OnClick() {
       MessageBox.Show(this.Caption, FrameworkApplication.ActiveTool.Caption);
     }
  }

Step 4. Associate the Menu with the Map Tool

Assign the daml id of the menu to the ContextMenuID property of the map tool in its constructor. The daml id can be found on the menu declaration in the Config.daml:

  <menus>
    <menu id="ContextMenu_Menus_ToolContextMenu" caption="ToolContextMenu" ...>

Open the source file containing the definition of your map tool (ContextMenuTool1.cs if you are following the naming convention of this guide). Add the assignment:

   public ContextMenuTool1() {
     IsSketchTool = true;
     SketchType = SketchGeometryType.Rectangle;
     SketchOutputMode = SketchOutputMode.Map;
     //Add this line here - associating the menu id with the tool's ContextMenuID property
     this.ContextMenuID = "ContextMenu_Menus_ToolContextMenu";
  }

Compile and run your add-in in the debugger. Open any project in Pro. If you do not have any projects previously opened, create a new one using the "Map" template. On the "Add-in" tab, activate your tool. Right click anywhere on the map. Observe that your context menu, daml id "ContextMenu_Menus_ToolContextMenu", is shown.

Context Menu

Part 2. Manual Context Menu Popup

There may be occasions where your tool needs to capture the right-click event before showing the context menu - perhaps to perform some custom processing with the right-click location. The automatic association via the tool ContextMenuID property does not work when tools perform their own right-click handling. Therefore, in this scenario, we must show the context menu "by hand".

Step 5. Popup the Context Menu Manually

Add the new map tool to the "Ribbon" project folder using the map tool item template. Name the new tool "ContextMenuTool2".

Step 6. Add the Right Click Handler Logic

We will handle the right-click using a pair of overrides: OnToolMouseDown and HandleMouseDownAsync. Open your map tool (ContextMenuTool2.cs in this case) and add the following code:

  protected override void OnToolMouseDown(MapViewMouseButtonEventArgs e) {
    if (e.ChangedButton == System.Windows.Input.MouseButton.Right)
      e.Handled = true;
  }

  protected override Task HandleMouseDownAsync(MapViewMouseButtonEventArgs e) {
    return Task.CompletedTask;
 }

Notice how we detect if the mouse click is a right click in OnToolMouseDown and, if so, we set e.Handled = true on the passed in MapViewMouseButtonEventArgs parameter. This signals to the Framework that the HandleMouseDownAsync method overload should be called. Currently it is simply returning a completed Task to satisfy the requirements of the method's return type.

Step 7. Show the Context Menu in Our Handler

To show our context menu "manually", we must capture the clicked location of the mouse to use it as the location for the context menu.

First, add a get/set property called "ClientPoint" to the map tool. Declare it as type System.Windows.Point(and not ArcGIS.Core.Geometry.MapPoint):

  internal class ContextMenuTool2 : MapTool {
    public System.Windows.Point ClientPoint { get; set; }

Second, within the HandleMouseDownAsync overload, assign the (right) clicked location of the mouse to the ClientPoint property. The clicked location is available from the MapViewMouseButtonEventArgs parameter:

  protected override Task HandleMouseDownAsync(MapViewMouseButtonEventArgs e) {
    this.ClientPoint = e.ClientPoint;
    //TODO - use the right click location as needed in the tool click handler
    return Task.CompletedTask;
  }

Third, add code to show the context menu in a custom method called "ShowContextMenu". Call the "ShowContextMenu" method from within the HandleMouseDownAsync handler. To show the context menu, we must instantiate a context menu instance via the static api method FrameworkApplication.CreateContextMenu(...). It takes as its first parameter the id of the menu we wish to create. To its second parameter, we pass in a delegate that returns the client location at which the menu is to be shown (ClientPoint in this case). The context menu's IsOpen property must also be set to true. When it is closed, the Framework will change it back to false.

  protected override Task HandleMouseDownAsync(MapViewMouseButtonEventArgs e) {
    this.ClientPoint = e.ClientPoint;
    //TODO - use the right click location as needed in the tool click handler
    ShowContextMenu();
    return Task.CompletedTask;
  }

  private void ShowContextMenu() {
     //Create a context menu instance - this must be called on the UI thread!
     //Reference the "ClientPoint" property in a delegate passed as the 2nd parameter
     var contextMenu = FrameworkApplication.CreateContextMenu("ContextMenu_Menus_ToolContextMenu", 
                                                                               () => ClientPoint);

     //Add an (optional) handler for when the menu is closed
     contextMenu.Closed += (o, e) => {
        //TODO, any clean up associated with your context menu closing
     };
    //Mark the context menu as "open"
    contextMenu.IsOpen = true;
  }

Note: Context menus must be created and shown on the UI thread or an exception will be thrown.

Step 8. Compile and Run the Addin

Compile and run your add-in. Open a project with a map. Activate "ContextMenuTool 2" (or whichever tool contains the "manual" popup code). On a right-click the context menu should show. Click on any menu option.

If the menu does not show, ensure you are passing the correct menu id to FrameworkApplication.CreateContextMenu. Ensure that you are correctly assigning the mouse location to your map tool's custom ClientPoint property (step 7).

Part 3. Manual Dynamic Menu Popup

In the final scenario, the map tool intends to define its context menu item content on the fly by adding menu item options to its context menu programmatically. To add menu item content programmatically, the addin must use a "dynamic menu".

Dynamic menus _can either be shown using the ContextMenuID map tool property, as described in Part 1., or, more typically, with a custom right-click handler, as described in Part 2. This is the scenario that is described here.

Step 9. Dynamic Menu Workflow

The map tool will be using a line sketch geometry. The map tool will allow existing line features to have their feature geometry appended into the current active sketch. The procedure will be as follows:

  1. When the tool is activated, all line features intersecting the current map extent, from the first visible line feature layer in the TOC, will be retrieved and their object ids stored.
  2. When the tool is sketching and the user right-clicks, a dynamic context menu will show a menu item for each feature (line) retrieved when the tool was activated.
  3. The user can click on any menu item to append that feature's geometry to the existing sketch.

The map tool needs to define a dynamic menu with logic to retrieve the relevant feature geometries and append them (as needed) to the current sketch.

Step 10. Define a Dynamic Menu

There is no item template for a dynamic menu so it must be added manually. First, add the code behind file for the dynamic menu class implementation to your addin in the Menus folder. Use the "standard" Visual C# Class item template. Call the class "LineFeaturesDynamicMenu".

namespace ContextMenu.Menus {
  class LineFeaturesDynamicMenu {
  }
}

Modify the class declaration to derive from the DynamicMenu contract. You will need to add a using ArcGIS.Desktop.Framework.Contracts; statement to the class file to resolve the DynamicMenu reference.

 using ArcGIS.Desktop.Framework.Contracts;
 ...
 namespace ContextMenu.Menus {
   class LineFeaturesDynamicMenu {
   }
 }

Step 11. Add a Dynamic Menu Declaration to the Config.daml

In the Config.daml add a <dynamicMenu .../> tag to the <controls></controls> section. Assign it the daml id "ContextMenu_Menus_UpdateSketch". In the className attribute, be sure to reference the dynamic menu class added previously in Step 10, i.e. ContextMenu.Menus.LineFeaturesDynamicMenu if you are explicitly following this guide:

<controls>
  ...
<dynamicMenu id="ContextMenu_Menus_UpdateSketch"
              className="ContextMenu.Menus.LineFeaturesDynamicMenu"
              caption="Update Sketch" />

</controls>

Step 12. Change Map Tool SketchType to Line

Change the sketch type of the map tool to SketchGeometryType.Line (it is probably Rectangle):

 public ShowContextMenuTool2() {
   IsSketchTool = true;
   SketchType = SketchGeometryType.Line;
   SketchOutputMode = SketchOutputMode.Map;
 }

Step 13. Get Line Features For Use With the Dynamic Menu

Declare a class variable to hold the retrieved line feature information in your map tool class. The class variable will be of type Tuple<string, List<long>>. Name it _lineFeatureInfo:

  internal class ContextMenuTool2 : MapTool {

     private Tuple<string, List<long>> _lineFeatureInfo = null;

In the OnToolActivateAsync override, populate the _lineFeatureInfo with the layer URI and OIDs of features retrieved from the/a line feature layer in the TOC. The tool uses MapView.Active.GetFeatures and the extent of the current map view is used as the intersecting geometry:

  protected override Task OnToolActivateAsync(bool active) {
    return QueuedTask.Run(() => {
      _lineFeatureInfo = null;
       //Get all (visible) features within the extent of the active 2D map
       var select = MapView.Active.GetFeatures(MapView.Active.Extent);
       foreach(var kvp in select)  {
          //Look for the first line feature class in the TOC
          if (kvp.Key.ShapeType == esriGeometryType.esriGeometryPolyline) {
             //Get the intersecting line features
	     _lineFeatureInfo = new Tuple<string, List<long>>(kvp.Key.URI, kvp.Value);
             break;
	  }
       }
     });
  }

Step 14. Pass the Line Features To The Dynamic Menu

First, we add a static method and class instance variables to the dynamic menu that the tool can use to pass it the features. Add the following code to the LineFeatureDynamicMenu class. Add any required using statements to satisfy the class references:

 class LineFeaturesDynamicMenu : DynamicMenu {

  private static Tuple<string, List<long>> _featureInfo = null;
  private static MapPoint _insertPoint = null;
  private static Geometry _sketch = null;

  //The map tool will use this method to pass in line feature info to the menu. We also pass in the current
  //sketch so that it can be modified
  public static void SetFeaturesForMenu(Geometry sketch, Tuple<string, List<long>> featureInfo, 
                                                                           MapPoint insertPoint) {
     _featureInfo = featureInfo;
     _insertPoint = insertPoint;
     _sketch = null;
     _sketch = sketch;
  }

Second, in the right-click handler, in the map tool, add the following code to convert the right-click point to map coordinates and to retrieve the current sketch geometry:

  protected async override Task HandleMouseDownAsync(MapViewMouseButtonEventArgs e) {
    MapPoint clickedPoint = await QueuedTask.Run(() => {
      //This is the right-click location
      this.ClientPoint = e.ClientPoint;
      //Convert to map coordinates
      return MapView.Active.ClientToMap(this.ClientPoint);
    });
    //retrieve the current sketch
    var sketch = await MapView.Active.GetCurrentSketchAsync();
    ShowContextMenu();
 }

Third, in the custom ShowContextMenu member function within the map tool (added previously in step 7), the signature needs to be changed to accept the sketch geometry from the HandleMouseDownAsync callback along with the clicked point, converted into map coordinates.

Modify the HandleMouseDownAsync callback to call ShowContextMenu with the modified parameters:

  protected async override Task HandleMouseDownAsync(MapViewMouseButtonEventArgs e) {
    ...
    ShowContextMenu(sketch, clickedPoint);
  }

  //Add parameters for the sketch and clicked point
  private void ShowContextMenu(Geometry sketch, MapPoint clickedPoint) {
   ...		
  }

Fourth, modify the ShowContextMenu member function in the map tool to instantiate the dynamic menu (instead of the context menu from step 7) and to pass the retrieved line feature info (collected when the tool was activated) to the dynamic menu via its static method "SetFeaturesForMenu" (added previously in step 14):

  private void ShowContextMenu(Geometry sketch, MapPoint clickedPoint) {
    //Use the daml id of the _dynamic menu_ here. Replace the id of the context menu - 
    //"ContextMenu_Menus_ToolContextMenu"  that was being previously used with the dynamic menu 
    //id "ContextMenu_Menus_UpdateSketch"...
    var contextMenu = FrameworkApplication.CreateContextMenu("ContextMenu_Menus_UpdateSketch", () => ClientPoint);

    //Pass the line feature info to the dynamic menu class
    LineFeaturesDynamicMenu.SetFeaturesForMenu(sketch, _lineFeatureInfo, clickedPoint);

    contextMenu.Closed += (o, e) => {
      //TODO, any clean up associated with your dynamic menu closing
    };
    contextMenu.IsOpen = true;
  }

*Notice that ShowContextMenu is being called from HandleMouseDownAsync on the UI thread and not from within the QueuedTask.

Step 15. Add Menu Items to the Dynamic Menu

To add menu items to a dynamic menu, a number of "Add" method overloads are provided on the Dynamic Menu base class. Calling "Add", with the respective parameters, creates a menu item in the dynamic menu's collection of menu items, each of which is shown when the dynamic menu is displayed. Menu items should be added in the dynamic menu's OnPopup callback - which is called each time the menu is about to be displayed.

When the menu is closed, the menu items are cleared from the dynamic menu's collection. Refer to ProConcepts Framework, Dynamic Menu for more details.

In our dynamic menu, we will add one menu item per line feature passed in to the static method SetFeaturesForMenu by the map tool. Implement the OnPopup callback in the LineFeaturesDynamicMenu class as follows:

 protected override void OnPopup() {
   //No line features were retrieved
   if (_featureInfo == null) {
     //Place holder
     this.Add("No features found");
   }
   else {
     //For each feature oid, add a menu item to the dynamic menu item collection
     foreach(var oid in _featureInfo.Item2) {
       var caption = $"Update sketch with feature {oid}";
       //call the add overload with the menu item caption
       this.Add(caption);
     }
   }
 }

Step 16. Handle a Menu Item Click

Whichever dynamic menu item the user clicks on, we need to add the relevant line feature geometry (for that item) to the sketch. The shape will be appended to the existing sketch geometry at the specified insertion point (defined by the right click location on the map).

Dynamic menus can handle the menu item click event either with a delegate defined for each menu item (defined when the menu item is created) or via the dynamic menu OnClick override - which provides the index of the menu item that was clicked as its input parameter. We will use the OnClick method override.

In the implementation, the relevant line feature shape (for the clicked menu item) is retrieved from the feature layer and "moved" to the sketch insertion point (i.e. the right-click location on the map). Next, it is appended to the sketch geometry and the modified sketch is applied back to the mapview. The code comments explain the purpose of each section:

 protected override void OnClick(int index) {
   if (_featureInfo == null)
     return;
   var fl = MapView.Active.Map.FindLayer(_featureInfo.Item1) as FeatureLayer;

   QueuedTask.Run(() => {
      //Use inspector to retrieve the feature shape
      var insp = new Inspector(false);
      insp.Load(fl, _featureInfo.Item2[index]);
      //Project
      var temp_line = GeometryEngine.Instance.Project(insp["SHAPE"] as Polyline, 
			  MapView.Active.Map.SpatialReference) as Polyline;

      //Move the beginning of the shape to the right-click
      //location...
      var first_point = temp_line.Points[0];
      var dx = _insertPoint.X - first_point.X;
      var dy = _insertPoint.Y - first_point.Y;
      var mv_line = GeometryEngine.Instance.Move(temp_line, dx, dy);

      //match the geometry sr with the sketch sr
      Polyline finalLine =  GeometryEngine.Instance.Project(mv_line,
                           _sketch.SpatialReference) as Polyline;

      //Sketch might be empty but it is never null...
      //assumes single part polyline here...append it to the sketch
      var points = ((Polyline)_sketch).Points.ToList();
      points.AddRange(finalLine.Points);
      var bldr = new PolylineBuilder(points.Select(p => p.Coordinate3D).ToList(), 
					   _sketch.SpatialReference);

      //ensure the geometry is Z enabled to be used with the sketch
      bldr.HasZ = true;
      _sketch = bldr.ToGeometry();

      //Update the sketch
      MapView.Active.SetCurrentSketchAsync(_sketch);
   });
 }

Compile and run the code. Right click and select a menu item. The feature geometry associated with the clicked item will be appended to the sketch at the right-click location. Pan the map to show different content. Deactivate/reactivate the tool (click any other tool to deactivate) to refresh the retrieved line features. On the right click, notice how the list of menu items will be different when the menu is constructed using different line feature content (retrieved from the view).

Context Menu 2

If there are no line features visible in the map view when the tool is activated then a "No Features Found" menu item is displayed.

Refer to the MapToolWithDynamicMenu community sample for an example of a dynamic menu that is using a delegate on each dynamic menu item to handle a menu click rather than the OnClick override implemented in this guide.

⚠️ **GitHub.com Fallback** ⚠️