ProGuide Knowledge Graph Construction Tools - Esri/arcgis-pro-sdk GitHub Wiki

Language:      C#
Subject:       Editing, Knowledge Graph
Contributor:   ArcGIS Pro SDK Team <[email protected]>
Organization:  Esri, http://www.esri.com
Date:          09/17/2024
ArcGIS Pro:    3.4
Visual Studio: 2022

This guide demonstrates how to create a construction tool for constructing Knowledge Graph entities and relates on a link chart. On a link chart, entities are represented by "nodes" with point geometries and relationships are represented by "links" which run between nodes and have line geometries.

All construction tools, not just knowledge graph link chart construction tools, are associated with a category that determines the type of geometry the tool is associated with. Feature categories include esri_editing_construction_point, esri_editing_construction_polyline, esri_editing_construction_polygon and esri_editing_construction_multipoint. Knowledge Graph categories include esri_editing_construction_knowledge_graph_entity and esri_editing_construction_knowledge_graph_relationship.

The code used in this ProGuide can be found at Knowledge Graph Construction Tools.

Prerequisites

  • For background on construction tools, review the documentation here
  • Create a new ArcGIS Pro Module Add-in, and name the project KnowledgeGraphConstructionTools. If you are not familiar with the ArcGIS Pro SDK, you can follow the steps in the ProGuide Build your first add-in to get started.

Step 1

  • Add a new ArcGIS Pro Add-ins | ArcGIS Pro Construction Tool item to the add-in project, and name the item CreateEntity.

Modify the entry for the construction tool generated in the Config.daml file as follows:

  • Change the tool caption to "Create Entity".
  • Change the tool heading to "Create Entity" and the ToolTip text to "Create a knowledge graph entity"
  • Change the categoryRefID to esri_editing_construction_knowledge_graph_entity indicating that the tool will create knowledge graph entities.
  • Your tool entry will have a content tag with a guid attribute. Don't change this attribute.
<tool id="KnowledgeGraphConstructionTools_CreateEntity" 
      categoryRefID="esri_editing_construction_knowledge_graph_entity" 
       caption="Create Entity" className="CreateEntity" 
       loadOnClick="true" 
       smallImage="GenericButtonRed16" largeImage="GenericButtonRed32">
   <tooltip heading="Create Entity">Create a knowledge graph entity
       <disabledText />
    </tooltip>
    <content guid="dcfbc202-acdc-42c9-9143-1f47981aa795" />   <!-- your guid value will be different -->
</tool>

Step 2

Open the code behind file, CreateEntity.cs, generated for the construction tool by the Visual Studio item template. It should look very similar to the code shown below:

internal class CreateEntity : MapTool
{
  public CreateEntity()
  {
    IsSketchTool = true;
    UseSnapping = true;
    // Select the type of construction tool you wish to implement.  
    // Make sure that the tool is correctly registered with the correct component category type 
    // in the daml 
    SketchType = SketchGeometryType.Point;
    // SketchType = SketchGeometryType.Line;
    // SketchType = SketchGeometryType.Polygon;
    //Gets or sets whether the sketch is for creating a feature and should use the CurrentTemplate.
    UsesCurrentTemplate = true;
    //Gets or sets whether the tool supports firing sketch events when the map sketch changes. 
    //Default value is false.
    FireSketchEvents = true;
  }

  /// <summary>
  /// Called when the sketch finishes. This is where we will create the sketch operation and then 
  /// execute it.
  /// </summary>
  /// <param name="geometry">The geometry created by the sketch.</param>
  /// <returns>A Task returning a Boolean indicating if the sketch complete event was successfully 
  /// handled.</returns>
  protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
  {
    if (CurrentTemplate == null || geometry == null)
      return Task.FromResult(false);

    // Create an edit operation
    var createOperation = new EditOperation();
    createOperation.Name = string.Format("Create {0}", CurrentTemplate.Layer.Name);
    createOperation.SelectNewFeatures = true;

    // Queue feature creation
    createOperation.Create(CurrentTemplate, geometry);

    // Execute the operation
    return createOperation.ExecuteAsync();
  }
}

Note that in the tool constructor, the SketchType is set to Point. In the OnSketchCompleteAsync method, an EditOperation is created, the Create method is called using the CurrentTemplate and sketched geometry and then the edit operation is executed. We will use this code without any changes.

  • Build the sample and verify that there are no compile errors.
  • Run the the add-in in the (visual studio) debugger.
  • In Pro, create a new, or open an existing, project. Use the "New Investigation" button in the Knowledge Graph group on the Insert tab to create an investigation within your project. Select an existing Knowledge Graph service to define the investigation. Once the investigation has been added to the project, from the knowledge graph context menu in the table of contents dockpane, add the Knowledge Graph to a new link chart.
  • Save the project.
  • With the link chart open, ensure the Create Features window is open. Verify that your construction tool appears on the palette(s) for the entity layer(s). Activate the tool and create a new entity in any of the entity layers by sketching a point.

EntityConstructionTool.png

Step 3

Next, lets create the relationship construction tool. As previously mentioned; relationships on a link chart are represented by "links" or lines. Whilst we could create a construction tool using a SketchType of Line, a link chart relationship construction tool uses a SketchType of Point. To create a link, the tool directs the user to identify the origin and destination entities with two separate clicks. We will use the SketchTip of the tool to provide information to the user and useful hints about the entities identified and/or hovered over. Once an origin entity is identified, a connection line or link from this (first) entity to the mouse location is displayed on the overlay. This connection line is updated whenever the mouse moves. When a destination entity is identified with a second click, the relationship link is created. This guide will illustrate the processes used in building a similar tool.

Add a new ArcGIS Pro Add-ins | ArcGIS Pro Construction Tool item to the add-in project, and name it CreateRelate. Modify the entry for the construction tool generated in the Config.daml file as follows:

  • Change the caption to "Create Relate".
  • Change the tool heading to "Create Relate" and the ToolTip text to "Create a knowledge graph relate"
  • Change the categoryRefID to esri_editing_construction_knowledge_graph_relationship indicating the tool will create knowledge graph relationships.
  • Your tool entry will have a content tag with a guid attribute. Don't change this attribute.
<tool id="KnowledgeGraphConstructionTools_CreateRelate" 
      categoryRefID="esri_editing_construction_knowledge_graph_relationship" 
       caption="Create Relate" className="CreateRelate" 
       loadOnClick="true" 
       smallImage="GenericButtonRed16" largeImage="GenericButtonRed32">
   <tooltip heading="Create Relate">Create a knowledge graph relate
      <disabledText />
   </tooltip>
   <content guid="858aa73d-c87b-4d37-9073-0157b03cdc2b" />   <!-- your guid value will be different -->
</tool>

Open the code behind file, CreateRelate.cs, generated for the construction tool by the Visual Studio item template. It should look very similar to the code shown below:

internal class CreateRelate : MapTool
{
  public CreateRelate()
  {
    IsSketchTool = true;
    UseSnapping = true;
    // Select the type of construction tool you wish to implement.  
    // Make sure that the tool is correctly registered with the correct component category type 
    // in the daml 
    SketchType = SketchGeometryType.Point;
    // SketchType = SketchGeometryType.Line;
    // SketchType = SketchGeometryType.Polygon;
    //Gets or sets whether the sketch is for creating a feature and should use the CurrentTemplate.
    UsesCurrentTemplate = true;
    //Gets or sets whether the tool supports firing sketch events when the map sketch changes. 
    //Default value is false.
    FireSketchEvents = true;
  }

  /// <summary>
  /// Called when the sketch finishes. This is where we will create the sketch operation and then 
  /// execute it.
  /// </summary>
  /// <param name="geometry">The geometry created by the sketch.</param>
  /// <returns>A Task returning a Boolean indicating if the sketch complete event was successfully 
  /// handled.</returns>
  protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
  {
    if (CurrentTemplate == null || geometry == null)
      return Task.FromResult(false);

    // Create an edit operation
    var createOperation = new EditOperation();
    createOperation.Name = string.Format("Create {0}", CurrentTemplate.Layer.Name);
    createOperation.SelectNewFeatures = true;

    // Queue feature creation
    createOperation.Create(CurrentTemplate, geometry);

    // Execute the operation
    return createOperation.ExecuteAsync();
  }
}

The tool defaults to a SketchType of Point and has the standard OnSketchCompleteAsync implementation. Even though we are creating links, we still want a SketchType of Point. Because our relationship construction tool will use a workflow with 2 mouse clicks (the first to identify the origin entity and the second to identify the destination entity), we will use the OnToolMouseDown method to track the mouse clicks and create the relationship link (once we have selected the required entities) rather than using the "traditional" OnSketchCompleteAsync method.

Replace the existing OnSketchCompleteAsync method with the following which ensures that OnSketchCompleteAsync contains no functionality. Note that all of the template generated code (that creates a feature with an edit operation and current template) has been deleted.

 protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
 {
    // Avoid default finish sketch handling - we are doing it instead in CreateRelationship 
    //   which we call explicitly on the second mouse click
    return Task.FromResult(true);
 }

The tool needs to track state throughout it's workflow with regards to whether it is waiting for an origin or destination entity to be identified. Add an enumeration to the code behind file to define the states of the tool throughout the relationship creation process.

  internal enum RelateToolState
  { 
    IdentifyOriginFeature = 0, 
    IdentifyDestinationFeature, 
    CreateRelationship
  }

And add a private variable into the class definition to hold the state of the tool.

internal class CreateRelate : MapTool
{
    // tracks the state of the tool
    private RelateToolState State;

    public CreateRelate()
    {
      IsSketchTool = true;
      UseSnapping = true;
      // Select the type of construction tool you wish to implement.  
      // Make sure that the tool is correctly registered with the correct component category type  
      // in the daml 
      SketchType = SketchGeometryType.Point;
      // SketchType = SketchGeometryType.Line;
      // SketchType = SketchGeometryType.Polygon;
      //Gets or sets whether the sketch is for creating a feature and should use the CurrentTemplate.
      UsesCurrentTemplate = true;
      //Gets or sets whether the tool supports firing sketch events when the map sketch changes. 
      //Default value is false.
      FireSketchEvents = true;
    }

The State needs to be initialized when the tool is activated. Override the OnToolActivateAsync method and set the State to "RelateToolState.IdentifyOriginFeature".

    protected override Task OnToolActivateAsync(bool hasMapViewChanged)
    {
      // initialize state
      State = RelateToolState.IdentifyOriginFeature;

      return base.OnToolActivateAsync(hasMapViewChanged);
    }

Track the mouse clicks by overriding the OnToolMouseDown method and updating the State. When a left mouseDown is received, the State changes to "RelateToolState.IdentifyDestinationFeature". When a second left mouseDown is received, the relationship record is created and the State changes back to "RelateToolState.IdentifyOriginFeature".

    protected override void OnToolMouseDown(MapViewMouseButtonEventArgs args)
    {
      if (args.ChangedButton == MouseButton.Left)
      {
        // progress the state when the mouse is clicked
        if (State == RelateToolState.IdentifyOriginFeature)
        {
          State = RelateToolState.IdentifyDestinationFeature;
        }
        else if (State == RelateToolState.IdentifyDestinationFeature)
        {
          // TODO - create the relationship

          State = RelateToolState.IdentifyOriginFeature;
        }
      }

      base.OnToolMouseDown(args);
    }

The method to create the relationship record will be added later in this guide. Your CreateRelate.cs code behind file should look like the following

internal enum RelateToolState
{
  IdentifyOriginFeature = 0,
  IdentifyDestinationFeature,
  CreateRelationship
}

internal class CreateRelate : MapTool
{
 // tracks the state of the tool
 private RelateToolState State;

  public CreateRelate()
  {
    IsSketchTool = true;
    UseSnapping = true;
    // Select the type of construction tool you wish to implement.  
    // Make sure that the tool is correctly registered with the correct component category type 
    // in the daml 
    SketchType = SketchGeometryType.Point;
    // SketchType = SketchGeometryType.Line;
    // SketchType = SketchGeometryType.Polygon;
    //Gets or sets whether the sketch is for creating a feature and should use the CurrentTemplate.
    UsesCurrentTemplate = true;
    //Gets or sets whether the tool supports firing sketch events when the map sketch changes. 
    //Default value is false.
    FireSketchEvents = true;
  }

  protected override Task OnToolActivateAsync(bool hasMapViewChanged)
  {
    // initialize state
    State = RelateToolState.IdentifyOriginFeature;

    return base.OnToolActivateAsync(hasMapViewChanged);
  }

  protected override void OnToolMouseDown(MapViewMouseButtonEventArgs args)
  {
    if (args.ChangedButton == MouseButton.Left)
    {
      // progress the state when the mouse is clicked
      if (State == RelateToolState.IdentifyOriginFeature)
      {
        State = RelateToolState.IdentifyDestinationFeature;
      }
      else if (State == RelateToolState.IdentifyDestinationFeature)
      {
        // TODO - create the relationship
  
        State = RelateToolState.IdentifyOriginFeature;
      }
    }

    base.OnToolMouseDown(args);
  }

  /// <summary>
  /// Called when the sketch finishes. This is where we will create the sketch operation and then 
  /// execute it.
  /// </summary>
  /// <param name="geometry">The geometry created by the sketch.</param>
  /// <returns>A Task returning a Boolean indicating if the sketch complete event was successfully 
  /// handled.</returns>
  protected override Task<bool> OnSketchCompleteAsync(Geometry geometry)
  {
    // Avoid default finish sketch handling - we are doing it instead in CreateRelationship 
    //   which we call explicitly on the second mouse click
    return Task.FromResult(true);
  }
}

Build the sample and verify that there are no compile errors. Debug the add-in. Open your Knowledge Graph aprx. With the link chart and Create Features window open, verify that your link chart (relationship) construction tool appears on the palette(s) for the relationship layer(s).

RelateConstructionTool.png

Close ArcGIS Pro.

Step 4

In this step, we will create a helper class to hold some of the lower level routines for overlay, messaging and mouse management.

Create a class file and call it RelateToolHelper.cs. Add the following private members to the RelateToolHelper class (see below). These will be used to cache the layer and oid pairs for the origin and destination entities (which the tool has selected for the purpose of constructing the new "link"). The display expression of the entities will also be cached.

internal class RelateToolHelper
{
    // cache (layer, oid) pairs for the origin and destination entities
    //  also cache the display expression 
    private string _displayExpr0 = "";
    private string _displayExpr1 = "";
    private Layer _layer0;
    private Layer _layer1;
    private long _oid0 = -1L;
    private long _oid1 = -1L;


}

Next, we are going to add a number of helper routines for determining the messages to be used for the SketchTip of the construction tool. The SketchTip message will be determined by the tool state.

    #region Sketch Tip / Messaging

    // Gets a message to use as a Sketch Tip according to the tool state
    internal string GetSketchTip(RelateToolState state, EditingTemplate template)
    {
      switch (state)
      {
        case RelateToolState.IdentifyOriginFeature: return Format0(template);
        case RelateToolState.IdentifyDestinationFeature: return Format1(template, _displayExpr0, _layer0);
        case RelateToolState.CreateRelationship:
          return Format2(template, _displayExpr0, _layer0, _displayExpr1, _layer1);
      }
      return "";
    }
    //Tool SketchTip message strings
    private static string msg0 => "Click on from entity to create {0} relationship.";
    private static string msg1 => "Create {0} relationship from '{1}' in {2}";
    private static string msg2 => "Create {0} relationship from '{1}' in {2} \n to '{3}' in {4}";
    private static string msg3 => "Create {0} relationship";

    //Formatting methods for the message strings depending on the template and tool state
    private static string Format(string s, params object[] args) =>
       string.Format(s, args).Replace("\\n", Environment.NewLine);
    private static string Format0(EditingTemplate template) => Format(msg0, template?.Name ?? "");
    private static string Format1(EditingTemplate template, string expr, MapMember member) => 
       Format(msg1, template?.Name ?? "", expr, member?.Name ?? "");
    private static string Format2(
       EditingTemplate template, string expr1, MapMember member1, string expr2, MapMember member2)
       => Format(msg2, template?.Name ?? "", expr1, member1?.Name ?? "", expr2, member2?.Name ?? "");
    private static string Format3(EditingTemplate template) => Format(msg3, template?.Name ?? "");
    #endregion

The following helper routines will be used for managing the cached origin and destination entity information.

    #region Origin / Destination Entity Management

    internal Layer Layer0 => _layer0;
    internal long OID0 => _oid0;
    internal Layer Layer1 => _layer1;
    internal long OID1 => _oid1;

    // Sets the internal display expression, layer, oid variables according to the tool state
    internal void SetFeature(RelateToolState state, string disp, Layer layer, long oid)
    {
      if (state == RelateToolState.IdentifyOriginFeature)
      {
        _displayExpr0 = disp;
        _layer0 = layer;
        _oid0 = oid;
      }
      else if (state == RelateToolState.IdentifyDestinationFeature)
      {
        _displayExpr1 = disp;
        _layer1 = layer;
        _oid1 = oid;
      }
    }

    internal bool HasOriginDestinationGeometries()
    {
      if ((_oid0 != -1) && (_oid1 != -1))
      {
        var g0 = GetGeometry(_layer0, _oid0) as MapPoint;
        var g1 = GetGeometry(_layer1, _oid1) as MapPoint;

        return (g0 != null) && (g1 != null);
      }
      return false;
    }

    private Geometry GetGeometry(Layer layer, long oid)
    {
      var insp = new Inspector();
      insp.Load(layer, oid);
      var shape = insp.Shape;

      // project to map before returning
      var projGeometry = GeometryEngine.Instance.Project(shape, MapView.Active.Map.SpatialReference);
      return projGeometry;
    }

    #endregion

The next set of code retrieves the entity (if any) at the mouse location. In the Process(...) method, the mouse location is translated from screen coordinates into map coordinates and mapView.GetFeatures(...) is called to retrieve the features around that location. As entities are displayed on link charts as point layers, we will filter the results for point layers only in order to determine if there is an entity in the results. If there is we will return the first entity in the list of results.

    #region Searching
    internal Task<(Layer, long, MapPoint)> FindEntityFeature(System.Windows.Point mouseLocation)
      => Process(mouseLocation);

    private async Task<(Layer, long, MapPoint)> Process(System.Windows.Point mouseLocation)
    {
      var mapPoint = MapView.Active.ClientToMap(mouseLocation);
      // get features around the cursor
      var selSet = MapView.Active.GetFeatures(mapPoint);  

      // filter for entities only
      // these are displayed on link charts as point layers
      Layer layer = null;
      List<long> oids = null;
      var selSetDict = selSet.ToDictionary();
      foreach (var mm in selSetDict.Keys)
      {
        if (mm is FeatureLayer fLayer && (fLayer.ShapeType == esriGeometryType.esriGeometryPoint))
        {
          layer = fLayer;
          oids = selSetDict[mm];
        }
      }
      // no information if no point layer found
      if (layer == null)
      {
        return (null as Layer, -1L, mapPoint);
      }
      // no information if more than 1 feature found
      if (oids.Count > 1)
      {
        return (null as Layer, -1L, mapPoint);
      }
      // success - return the information
      var oid = oids[0];
      return (layer, oid, mapPoint);
    }
    #endregion

Finally add methods for managing the line symbology used for drawing the relationship link on the overlay.

    #region Symbols
    private CIMSymbolReference _symbol;
    private CIMSymbolReference _disabledSymbol;

    internal static CIMColor DefaultFeedbackColor() => CIMColor.CreateRGBColor(51, 153, 255);

    internal void InitSymbols()
    {
      _symbol ??= CreateLineSymbol(DefaultFeedbackColor(), 3.0);
      _disabledSymbol ??= CreateLineSymbol(new CIMRGBColor() { 
           R = 100, G = 100, B = 100, Alpha = 10 }, 2.0);
    }
    internal void ClearSymbols()
    {
      _symbol = null;
      _disabledSymbol = null;
    }

    internal static CIMSymbolReference CreateLineSymbol(CIMColor color, double width)
    {
      var symbol = SymbolFactory.Instance.ConstructLineSymbol(color, width);
      return new CIMSymbolReference { Symbol = symbol };
    }
    #endregion

And methods for managing the overlay itself.

    #region Overlay Management
    private IDisposable _overlay = null;

    // Adds a polyline between the origin and destination features to the overlay.
    // Nothing is added if the origin and destination feature point geometries cannot be 
    // determined.
    internal void AddRelationshipLineToOverlay()
    {
      var g0 = GetGeometry(_layer0, _oid0) as MapPoint;
      if (_oid1 != -1)
      {
        var g1 = GetGeometry(_layer1, _oid1) as MapPoint;
        if (g0 != null && g1 != null)
        {
          var pts = new List<MapPoint>() { g0, g1 };
          var line = ArcGIS.Core.Geometry.PolylineBuilderEx.CreatePolyline(pts);
          var graphic = new ArcGIS.Core.CIM.CIMLineGraphic
          {
            Line = line,
            Symbol = _symbol
          };
          _overlay = MapView.Active.AddOverlay(graphic);
        }
      }
    }

    // Adds a polyline between the origin feature and the "pt" to the overlay. 
    // Nothing is added if the origin feature point geometry cannot be determined.
    internal void AddLineOverlay(MapPoint pt)
    {
      var g0 = GetGeometry(_layer0, _oid0) as MapPoint;
      if (g0 != null && pt != null)
      {
        var pts = new List<MapPoint>() { g0, pt };
        var line = ArcGIS.Core.Geometry.PolylineBuilderEx.CreatePolyline(pts);
        var graphic = new ArcGIS.Core.CIM.CIMLineGraphic
        {
          Line = line,
          Symbol = _disabledSymbol
        };
        _overlay = MapView.Active.AddOverlay(graphic);
      }
    }

    internal void ClearOverlay() => SafeDispose(System.Threading.Interlocked.Exchange(ref _overlay, null));

    internal static void SafeDispose(object o)
    {
      if (o is IDisposable disp)
        disp.Dispose();
      else if (o is IEnumerable enumerable)
      {
        foreach (object x in enumerable)
          SafeDispose(x);
      }
    }
    #endregion

Compile the solution and ensure there are no errors.

Step 5

In this step we'll return to the link chart relation tool code behind; incorporating the SketchTip, overlay, mouseMove and mouseDown functionalities we added to our helper class in step 4. Open the CreateRelate.cs file.

Add a private RelateToolHelper variable and create it in the constructor.

    // tracks the state of the tool
    private RelateToolState State;
    // the helper
    private RelateToolHelper _helper;

    public CreateRelate()
    {
      IsSketchTool = true;
      UseSnapping = true;
      // Select the type of construction tool you wish to implement.  
      // Make sure that the tool is correctly registered with the correct component category type 
      // in the daml 
      SketchType = SketchGeometryType.Point;
      // SketchType = SketchGeometryType.Line;
      // SketchType = SketchGeometryType.Polygon;
      //Gets or sets whether the sketch is for creating a feature and should use the CurrentTemplate.
      UsesCurrentTemplate = true;
      //Gets or sets whether the tool supports firing sketch events when the map sketch changes. 
      //Default value is false.
      FireSketchEvents = true;

      _helper = new RelateToolHelper();
    }

Return to the tool activation routine and add code to initialize the SketchTip and tool cursor. Use the GetSketchTip method from the RelateToolHelper class. Also add the method to initialize the drawing symbols from the RelateToolHelper class.

    protected override Task OnToolActivateAsync(bool hasMapViewChanged)
    {
      // initialize state
      State = RelateToolState.IdentifyOriginFeature;
      // initialize sketchTip and cursor
      SketchTip = _helper.GetSketchTip(RelateToolState.IdentifyOriginFeature, CurrentTemplate);
      Cursor = System.Windows.Input.Cursors.Arrow;

      // initialize symbols
      _helper.InitSymbols();

      return base.OnToolActivateAsync(hasMapViewChanged);
    }

The tool also needs some clean up code to remove all graphics from the overlay and to remove the symbols. Override the OnToolDeactivateAsync method. Use the "clear" methods on the RelateToolHelper class to ensure that the overlay and symbols are cleared.

    protected override Task OnToolDeactivateAsync(bool hasMapViewChanged)
    {
      // clear overlay and symbols
      _helper.ClearOverlay();
      _helper.ClearSymbols();

      return base.OnToolDeactivateAsync(hasMapViewChanged);
    }

Next we want to implement the tool functionality for when the mouse moves. Hovering over an entity causes the SketchTip to be updated depending on the state of the tool. The mouse cursor will also change to indicate an entity under the mouse can be picked. In addition if an origin entity has already been identified (i.e. the state of the tool is RelateToolState.IdentifyDestinationFeature), then a line graphic from the identified origin entity to the mouse cursor location is added to the overlay.

Override the OnToolMouseMove method; using a DelayedInvoker to thin out mouse move messages. The OnMouseMoveImpl and ProcessEntityFeature methods provide the processing for determining the entities under the mouse location (if any) and updating the SketchTip and overlay accordingly using the RelateToolHelper class. Add the following code to the tool:

    private DelayedInvoker _invoker;
    private System.Windows.Point _lastPoint;

    protected override void OnToolMouseMove(MapViewMouseEventArgs args)
    {
      base.OnToolMouseMove(args);
      _invoker ??= new(10);
      _lastPoint = args.ClientPoint;

      // use a delayInvoker to thin out mouseMove notifications
      // don't await
      _invoker.InvokeTask(() => OnMouseMoveImpl(_lastPoint));
    }

    private Task OnMouseMoveImpl(System.Windows.Point mouseLocation)
    {
      return QueuedTask.Run(async () =>
      {
        // try to find an entity feature under the mouse location
        var (l, oid, pt) = await _helper.FindEntityFeature(mouseLocation);
        if (l is null)
        {
          // if nothing found, update the sketch tips, state
          if (State == RelateToolState.IdentifyOriginFeature)
            SketchTip = _helper.GetSketchTip(RelateToolState.IdentifyOriginFeature, CurrentTemplate);
          else if (State == RelateToolState.IdentifyDestinationFeature)
          {
            if (_helper.Layer0 != null)
              SketchTip = _helper.GetSketchTip(
                            RelateToolState.IdentifyDestinationFeature, CurrentTemplate);
            else
              State = RelateToolState.IdentifyOriginFeature;
          }
        }
        // process results (could be nothing if cursor wasn't over an entity feature)
        await ProcessEntityFeature(l, oid, pt);
      });
    }

    // Process a layer, oid pair
    // layer could be null, oid can be -1 if nothing to process
    private async Task ProcessEntityFeature(Layer layer, long oid, MapPoint pt)
    {
      // make sure on MCT
      if (!QueuedTask.OnWorker)
        return;

      // determine the display expression
      //  (used to update the SketchTip)
      var disp = oid.ToString();
      if (layer is FeatureLayer fl)
      {
        var displayExpressions = fl.GetDisplayExpressions([oid]);
        if (displayExpressions?.Count == 1)
          disp = displayExpressions[0];
      }

      // if identifying origin
      if (State == RelateToolState.IdentifyOriginFeature)
      {
        // set it on the helper
        _helper.SetFeature(State, disp, layer, oid);
        // update cursor, sketchTIp according to whether something was found
        if (oid == -1)
        {
          SetCursorNo();
        }
        else
        {
          SetCursorYes();
          SketchTip = _helper.GetSketchTip(RelateToolState.IdentifyDestinationFeature, CurrentTemplate);
        }
      }
      // else if identifying destination
      else if (State == RelateToolState.IdentifyDestinationFeature)
      {
        // set it on the helper
        _helper.SetFeature(State, disp, layer, oid);

        // clear overlay
        _helper.ClearOverlay();

        // if I have origin and destination feature geometries
        if (_helper.HasOriginDestinationGeometries())
        {
          // add the relationship line to the overlay
          _helper.AddRelationshipLineToOverlay();
          // update cursor, sketchTip
          SetCursorYes();
          SketchTip = _helper.GetSketchTip(RelateToolState.CreateRelationship, CurrentTemplate);
        }
        else
        {
          // add a line between origin and "pt" (mouse cursor)
          _helper.AddLineOverlay(pt);
          // update cursor, sketchTIp
          SetCursorNo();
          SketchTip = _helper.GetSketchTip(RelateToolState.IdentifyDestinationFeature, CurrentTemplate);
        }
      }
    }

    #region Cursor Management

    internal void SetCursorNo() => SetCursor(true);
    internal void SetCursorYes() => SetCursor(false);

    internal bool wasNo => Cursor == System.Windows.Input.Cursors.No;
    internal void SetCursor(bool no)
    {
      if (wasNo != no)
      {
        Cursor = (no) ? System.Windows.Input.Cursors.No : System.Windows.Input.Cursors.Arrow;
      }
    }

    #endregion

Compile the solution and ensure there are no errors.

Return to the OnToolMouseDown method which was overridden earlier. Add a call to a new parameter-less CreateRelationship method (we will be implementing) followed by a call to _helper.ClearOverlay(); (i.e. our RelateToolHelper class) to clear the overlays. The CreateRelationship method is going to call an overloaded version of the CreateRelationship method with the CurrentTemplate and the cached origin and destination entity information stored on the helper.

    protected override void OnToolMouseDown(MapViewMouseButtonEventArgs args)
    {
      if (args.ChangedButton == MouseButton.Left)
      {
        // progress the state when the mouse is clicked
        if (State == RelateToolState.IdentifyOriginFeature)
        {
          State = RelateToolState.IdentifyDestinationFeature;
        }
        else if (State == RelateToolState.IdentifyDestinationFeature)
        {
          CreateRelationship();
          _helper.ClearOverlay();
          State = RelateToolState.IdentifyOriginFeature;
        }
      }

      base.OnToolMouseDown(args);
    }

    // create relationship with the cached origin and destination info
    internal Task<bool> CreateRelationship() => 
      CreateRelationship(CurrentTemplate, _helper.Layer0, _helper.OID0, _helper.Layer1, _helper.OID1);

    // create relationship with the specified entity info
    internal async Task<bool> CreateRelationship(
      EditingTemplate template, Layer fromLayer, long fromOID, Layer toLayer, long toOID)
    {
       //TODO - create the new relationship
    }

The final CreateRelationship method will use the standard editing pattern for create. Namely, executing an EditOperation to perform the create. We will use a new description class introduced at 3.4, [KnowledgeGraphRelationshipDescription], with our edit operation to create the new knowledge graph relationship record. The KnowledgeGraphRelationshipDescription object will be defined, in this case, using two [RowHandles] pointing to the origin and destination entity records respectively that are being related. We will construct the necessary rowhandles using the entity rows we previously selected and cached in our RelateToolHelper. The create of the relationship will also generate the line or link geometry for the relate record - derived from the locations of the origin and destination entities on the link chart.

    // create relationship with the specified entity info
    internal async Task<bool> CreateRelationship(
        EditingTemplate template, Layer fromLayer, long fromOID, Layer toLayer, long toOID)
    {
      if (template == null || template.MapMember == null || fromLayer == null || 
          toLayer == null || fromOID == -1L || toOID == -1L)
        return false;

      // Create an edit operation
      var createOperation = new EditOperation();
      createOperation.Name = string.Format("Create {0}", CurrentTemplate.Layer.Name);
      createOperation.SelectNewFeatures = true;

      // create the row handles
      var originRowHandle = new RowHandle(fromLayer, fromOID);//Create a row handle for the origin
      var destinationRowHandle = new RowHandle(toLayer, toOID);//Create a row handle for the dest.

      // create relationship description - KnowledgeGraphRelationshipDescription, new at 3.4
      var rd = new KnowledgeGraphRelationshipDescription(originRowHandle, destinationRowHandle);
      // queue the Create
      createOperation.Create(template.MapMember as Layer, rd);

      // Execute the operation
      await createOperation.ExecuteAsync();

      return createOperation.IsSucceeded;
    }

Build the sample and verify that there are no compile errors. Debug the add-in. Open your Knowledge Graph aprx. With the link chart and Create Features window open, activate a template on a link chart relationship layer and activate your custom construction tool.

Move the mouse around the map. Note the SketchTip and Cursor. It should update as you move over entity features and perform your selects.

RelateTool_1_ClickOnFromEntity.png

Hover over an entity and observe the SketchTip and Cursor change.

RelateTool_2_HoverOriginEntity.png

Click on the entity to identify it as the origin entity. Notice the SketchTip change again and the connecting line draw on the overlay as you move the mouse.

RelateTool_3_IdentifiedOriginEntity.png

Hover over a second entity. Notice the SketchTip, Cursor and overlay change.

RelateTool_4_HoverDestinationEntity.png

Click on the second entity to identify it as the destination entity. This will automatically trigger the creation of the relationship record. Open the attributes window of the relationship layer to see the new record.

RelateTool_5_RelationshipCreated.png

Once you have finished exploring the tools, (optionally) save your edits and close ArcGIS Pro.

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