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.
Prerequisite
- Part 1. Basic Pattern
- Part 2. Manual Context Menu Popup
-
Part 3. Manual Dynamic Menu Popup
- Step 9. Dynamic Menu Workflow
- Step 10. Define a Dynamic Menu
- Step 11. Add a Dynamic Menu Declaration to the Config.daml
- Step 12. Change Map Tool SketchType to Line
- Step 13. Get Line Features For Use With the Dynamic Menu
- Step 14. Pass the Line Features To The Dynamic Menu
- Step 15. Add Menu Items to the Dynamic Menu
- Step 16. Handle a Menu Item Click
The code examples used within this guide are available from the context menu sample.
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".
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.
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
.
Create the map tool context menu. Use the ArcGIS Pro Menu item template. Add it to the "Menus" project folder. Name the menu "ToolContextMenu".
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() {
}
}
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);
}
}
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.
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".
Add the new map tool to the "Ribbon" project folder using the map tool item template. Name the new tool "ContextMenuTool2".
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.
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.
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).
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.
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:
- 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.
- 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.
- 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.
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 {
}
}
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>
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;
}
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;
}
}
});
}
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.
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);
}
}
}
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).
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.