Need a final mouse-up validation hook before link creation in GoXam

I need to perform validation when the user releases the mouse to finish drawing a link (after the final endpoint is coerced). The issue is that:

  1. IsValidTo() and IsValidLink() are called during link drawing, not at the final commit moment.
  2. If I try to cancel in DoMouseUp(), the link has already been drawn or committed in some cases, and DoCancel() / StopTool() do not reliably remove it.
  3. There seems to be no override or event that runs after mouse-up but before the link is created.

Because I cannot validate at the final coerced endpoint, one of my validation errors is thrown early, when the link momentarily passes near a port that is not intended as the target.

“In GoXam, how can I validate and cancel a link exactly at mouse-up, after the final endpoint is coerced but before the link is created or drawn—so that I don’t trigger validation early during link drawing when the cursor briefly passes over unintended ports?”

IsValidLink is the primary validation predicate, although it calls more specific predicates and depends on more specific properties for common customization convenience. However, as its documentation states, it is checking for logical validity of link relationships, not relationships based on physical or temporal conditions. GoXam for WPF v3

I think you want to override FindTargetPort, which is called on each call to DoMouseMove and DoMouseUp, and which calls IsValidLink only when it does not already know whether a particular pair of ports is valid or not. GoXam for WPF v3

When FindTargetPort returns null, the LinkingTool won’t create a new link. (Unless you are allowing disconnected links.)

What is the correct way to use FindTargetPort purely for port selection when all validation is handled in IsValidLink/IsValidFrom/IsValidTo?
(i.e., how does the selected port flow into the validation methods?)
How do I override FindTargetPort(bool) safely?
I’m seeing an issue while drawing links:
during the drag operation, IsValidLink is receiving an incorrect or premature toPortvalue, even though myPortGravity` is set to 0. This causes my validation logic in IsValidLink, IsValidFrom, and IsValidTo to fail because the toPort passed in is not the actual intended port — it’s a provisional/early value.

Call the base.FindTargetPort first, then decide whether you like that target port.

Yes, possible pairs of ports will be checked for link relationship validity, and that boolean value is cached so that the validity of that pair of ports is not checked again. But FindTargetPort is called continually during mouse moves and on mouse up.

I don’t know if giving you the implementations will help you, but let’s try. In LinkingBaseTool:

    /// <summary>
    /// Find a port with which the user could complete a valid link.
    /// </summary>
    /// <param name="toend">true if looking for a "to" port</param>
    /// <returns>
    /// a <c>FrameworkElement</c> representing a valid port,
    /// or null if no such port is near the current mouse point (within <see cref="PortGravity"/> distance)
    /// </returns>
    /// <remarks>
    /// <para>
    /// This finds elements near to the current mouse point for which a valid link connection is possible.
    /// For example, when <paramref name="toend"/> is true, this looks for elements (i.e. "ports") in nodes that
    /// have <see cref="Node.GetLinkableTo"/> return true and for which <see cref="IsValidTo"/> is true.
    /// </para>
    /// <para>
    /// For each port element found, this calls <see cref="IsValidLink"/> to find out if a link between
    /// the original node/port and the found node/port would be valid.
    /// The result is saved in the <see cref="ValidPortsCache"/> for faster decisions later during
    /// the operation of this tool.
    /// The closest valid port is returned.
    /// </para>
    /// </remarks>
    protected virtual FrameworkElement FindTargetPort(bool toend) {
      Diagram diagram = this.Diagram;
      Point p = diagram.LastMousePointInModel;
      double gravity = this.PortGravity;
      if (gravity <= 0) gravity = 0.1;
      IEnumerable<FrameworkElement> nearports;
      if (toend)
        nearports = diagram.Panel.FindElementsNear<FrameworkElement>(p, gravity, FindValidToPort, x => true, SearchLayers.Nodes);
      else
        nearports = diagram.Panel.FindElementsNear<FrameworkElement>(p, gravity, FindValidFromPort, x => true, SearchLayers.Nodes);
      double bestDist = Double.PositiveInfinity;
      FrameworkElement bestPort = null;
      foreach (FrameworkElement port in nearports) {
        if (port == null) continue;
        Node node = Diagram.FindAncestor<Node>(port);
        if (node == null) continue;
        Point toPoint = node.GetElementPoint(port, Spot.Center);  //?? assumes center point of port
        double dx = p.X - toPoint.X;
        double dy = p.Y - toPoint.Y;
        double dist = dx*dx + dy*dy;  // don't bother taking sqrt
        if (dist < bestDist) {  // closest so far
          bool valid = true;
          // check cache of IsValidLink calls
          if (this.ValidPortsCache.TryGetValue(port, out valid)) {
            // known to be either valid or invalid
            if (valid) { // known to be a valid port for a link
              bestPort = port;
              bestDist = dist;
            } // else known not valid: don't need to call IsValidLink again
          } else {  // but if not cached, try IsValidLink in the appropriate direction
            if ((toend && IsValidLink(this.OriginalFromNode, this.OriginalFromPort, node, port)) ||
                (!toend && IsValidLink(node, port, this.OriginalToNode, this.OriginalToPort))) {
              // now known valid, remember in cache
              this.ValidPortsCache[port] = true;
              bestPort = port;
              bestDist = dist;
            } else {
              // now known not valid, remember in cache
              this.ValidPortsCache[port] = false;
            }
          }
        }
      }
      if (bestPort != null) {
        Node targetnode = Diagram.FindAncestor<Node>(bestPort);
        if (targetnode != null && (targetnode.Layer == null || targetnode.Layer.AllowLink)) {
          return bestPort;
        }
      }
      return null;
    }

    /// <summary>
    /// Mouse movement results in the temporary node moving to where the valid <see cref="LinkingBaseTool.TargetPort"/> is located,
    /// or to where the mouse is if there is no valid target port nearby.
    /// </summary>
    /// <remarks>
    /// This calls <see cref="LinkingBaseTool.FindTargetPort"/> to update the <see cref="LinkingBaseTool.TargetPort"/>
    /// given the new mouse point.
    /// If a valid target port is found, this calls <see cref="LinkingBaseTool.CopyPortProperties"/> to move the
    /// temporary node/port and make them appear like the target node/port.
    /// If no valid target port is found, this calls <set cref="LinkingBaseTool.SetNoTargetPortProperties"/>
    /// to move the temporary node to where the mouse currently is and to remove any node/port appearance.
    /// </remarks>
    public override void DoMouseMove() {
      if (this.Active) {
        Diagram diagram = this.Diagram;
        this.TargetPort = FindTargetPort(this.Forwards);
        if (this.TargetPort != null) {
          Node targetnode = Diagram.FindAncestor<Node>(this.TargetPort);
          if (targetnode != null) {
            if (this.Forwards) {
              CopyPortProperties(targetnode, this.TargetPort, this.TemporaryToNode, this.TemporaryToPort, true);
              return;
            } else {
              CopyPortProperties(targetnode, this.TargetPort, this.TemporaryFromNode, this.TemporaryFromPort, false);
              return;
            }
          }
        }
        // found no potential port
        if (this.Forwards) {
          SetNoTargetPortProperties(this.TemporaryToNode, this.TemporaryToPort);
        } else {
          SetNoTargetPortProperties(this.TemporaryFromNode, this.TemporaryFromPort);
        }
      }
    }

In LinkingTool:

    /// <summary>
    /// A mouse-up ends the linking operation; if there is a valid <see cref="LinkingBaseTool.TargetPort"/> nearby,
    /// this calls <see cref="IDiagramModel.AddLink"/> to create a new link.
    /// </summary>
    /// <remarks>
    /// If a new link is created in the model, the corresponding <see cref="Link"/> is selected in the diagram
    /// and the "link drawn" event is raised.
    /// In any case this stops the tool.
    /// </remarks>
    public override void DoMouseUp() {
      if (this.Active) {
        Diagram diagram = this.Diagram;
        if (diagram == null) return;
        PartManager mgr = diagram.PartManager;
        if (mgr == null) return;
        this.TransactionResult = null;

        Object fromdata = null;
        Object fromid = null;
        Object todata = null;
        Object toid = null;

        IDiagramModel model = null;
        this.TargetPort = FindTargetPort(this.Forwards);
        Node targetnode = null;
        if (this.TargetPort != null) {
          targetnode = Diagram.FindAncestor<Node>(this.TargetPort);
          if (targetnode != null) {
            if (this.Forwards) {
              if (this.OriginalFromNode != null) {
                fromdata = this.OriginalFromNode.Data;
                fromid = this.OriginalFromNode.GetPortName(this.OriginalFromPort);
              }
              todata = targetnode.Data;
              toid = targetnode.GetPortName(this.TargetPort);
            } else {
              fromdata = targetnode.Data;
              fromid = targetnode.GetPortName(this.TargetPort);
              if (this.OriginalToNode != null) {
                todata = this.OriginalToNode.Data;
                toid = this.OriginalToNode.GetPortName(this.OriginalToPort);
              }
            }
            model = mgr.FindCommonDataModel(fromdata, todata);
          }
        } else {  // not connecting to a port; set FROMDATA or TODATA, but not both
          if (this.Forwards) {
            if (this.OriginalFromNode != null) {
              ILinksModel lmodel = this.OriginalFromNode.Model as ILinksModel;
              if (lmodel != null && lmodel.ValidUnconnectedLinks == ValidUnconnectedLinks.Allowed) {
                fromdata = this.OriginalFromNode.Data;
                fromid = this.OriginalFromNode.GetPortName(this.OriginalFromPort);
                model = lmodel;
              }
            }
          } else {
            if (this.OriginalToNode != null) {
              ILinksModel lmodel = this.OriginalToNode.Model as ILinksModel;
              if (lmodel != null && lmodel.ValidUnconnectedLinks == ValidUnconnectedLinks.Allowed) {
                todata = this.OriginalToNode.Data;
                toid = this.OriginalToNode.GetPortName(this.OriginalToPort);
                model = lmodel;
              }
            }
          }
        }
        if (model != null) {
          Object linkdata = model.AddLink(fromdata, fromid, todata, toid);
          Link link = null;
          if (linkdata != null) link = mgr.FindLinkForData(linkdata, model);
          if (link == null) link = mgr.FindLinkForData(fromdata, todata, model);
          if (link != null) {
            if (this.TargetPort == null) {
              // need to tell Route that it ought to end at Diagram.LastMousePointInModel
              if (this.Forwards) {
                link.Route.DefaultToPoint = this.Diagram.LastMousePointInModel;
              } else {
                link.Route.DefaultFromPoint = this.Diagram.LastMousePointInModel;
              }
            }
            if (diagram.AllowSelect) {
              diagram.Select(link);
            }
            // set the TransactionResult before raising event, in case it changes the result or cancels the tool
            this.TransactionResult = "NewLink";
            RaiseEvent(Diagram.LinkDrawnEvent, new DiagramEventArgs(link));
          } else {
            model.ClearUnresolvedReferences();
          }
        }
      }
      StopTool();
    }

could you please share the logic for FindValidFromPort and FindValidToPort Func delegates used for getting nearbyports

    // search up the chain of parent elements to find one that has Node.GetLinkableTo == true and IsValidTo
    internal FrameworkElement FindValidToPort(DependencyObject d) {
      FrameworkElement elt = Diagram.FindAncestorOrSelf<FrameworkElement>(d);
      if (elt == null) return null;
      Node node = Diagram.FindAncestor<Node>(elt);
      if (node == null) return null;
      return FindElementUpFrom(elt, Node.GetLinkableTo, x => IsValidTo(node, x));
    }

    // search up the chain of parent elements to find one that has Node.GetLinkableFrom == true and IsValidFrom
    internal FrameworkElement FindValidFromPort(DependencyObject d) {
      FrameworkElement elt = Diagram.FindAncestorOrSelf<FrameworkElement>(d);
      if (elt == null) return null;
      Node node = Diagram.FindAncestor<Node>(elt);
      if (node == null) return null;
      return FindElementUpFrom(elt, Node.GetLinkableFrom, x => IsValidFrom(node, x));
    }
    static T FindAncestor<T>(DependencyObject v) where T : DependencyObject {
      if (v == null) return null;
      DependencyObject p = VisualTreeHelper.GetParent(v);
      if (p == null) return null;
      T t = p as T;
      if (t != null) return t;
      return FindAncestor<T>(p);
    }
    static T FindAncestorOrSelf<T>(DependencyObject v) where T : DependencyObject {
      T t = v as T;
      if (t != null) return t;
      return FindAncestor<T>(v);
    }