Custom Commands

Shortening CustomCommand development time by using a proxy

One of the most time-consuming processes when developing with Hexagon G/Technology is creating custom commands. The usual development approach consists of the following iterative process:

  • Compile custom command
  • Launch G/Technology
  • Test custom command
  • Exit G/Technology
  • Modify custom command
During testing, only limited changes to the source code are allowed by the Visual Studio Edit and Continue function, effectively requiring multiple iterations of this sequence.

The process of starting and closing G/Technology can easiliy takeup multiple minutes and you need to close it because G/Technology will lock any assemblies containing your custom commands.

Customcommands are written in DotNet and therefore run in something called an “Assembly Domain”, in this case the G/Technology Assembly Domain. When you create a custom command loading other custom commands in its own assembly domain, dotnet will not load the same assembly again in fact creating a proxy. The following code snippet demonstrates this:

string assembly = @"c:\Program Files (86)\Intergraph\GTechnology\YourCustomCommand.dll";
byte[] assemblyBytes = File.ReadAllBytes(assembly);
System.Reflection.Assembly assemblyToLoad = Assembly.Load(assemblyBytes);

Type entryClass = assemblyToLoad.GetTypes().FirstOrDefault(t ⇒ typeof(IGTCustomCommandModal).IsAssignableFrom(t));
if( entryClass != null)
{
    IGTCustomCommandModal CCModal = (IGTCustomCommandModal)assemblyToLoad.CreateInstance(entryClass.FullName);
    CCModal.Activate();
    return;
}

entryClass = assemblyToLoad.GetTypes().FirstOrDefault(t ⇒ typeof(IGTCustomCommandModeless).IsAssignableFrom(t));
if( entryClass != null)
{
    IGTCustomCommandModeless CCModeless = (IGTCustomCommandModeless)assemblyToLoad.CreateInstance(entryClass.FullName);
    CCModeless.Activate(_customCommandHelper);
    return;
}

This snippet scans an assembly for a type implementing either interface ‘IGTCustomCommandModal‘ or ‘IGTCustomCommandModeless‘, which both can be found in namespace ‘Intergraph.GTechnology.Interfaces‘ and are needed to implement customcommands. If a type implements one of these interfaces, the proxy customcommand creates an instance of it loading it in the assembly domain.

When this approach is used to load custom commands, G/Technology will not lock the containing assemblies after closing the custom command, enabling much shorter development cycles.

The technique constists of 2 or more custom commands, the first one being the proxy, the second one being the custom command to be developed. Once the first customcommand is started, it will show a dialog where the assembly containing the custom command to be developed should be entered :

Proxy dialog

Some extra querying to the G3E_CUSTOMCOMMAND table allows to provide useful metadata as shown in the pop-up window.

Then attach the Visual Studio debugger and press ‘Launch’ and any breakpoint in your customcommand should be hit and it can be tested. Once done, detach the debugger, modify code, compile and attach again, etc., etc. :

Debugging session active

An example of a session using this technique this can be seen in this this youtube video, showing a debug session of a custom command called “Swap Inner Ducts” which will just show a dialog and a messagebox, but the important part is that the customcommand is changed, recompiled and executed again using the Visual Studio debugger without leaving G/Technology.

Youtube video

Sources can be downloaded from here.

Credits go to Jan Stuckens for initially coming up with this idea.

Thanks to Michaël Demanet and Didier de Bisschop from Proximus for use of their environment to test this technique.

Notes :

  • the proxy needs to be built with debugging information, else breakpoints in the target customcommand won’t be hit
  • All referenced assemblies need to be loaded
  • This technique has been used with assemblies containing a single custom command, assemblies with multiple custom commands where not tested
  • G/Technology version 10.04.2002.00035 was used to test this approach

Invoking Oracle Spatial from a Custom Command

Consider a requirement where the centroid of a set of points needs to be retrieved within a custom command. This can be achieved using the ‘GTDataProvider.DataContext.Execute‘-method:

IGTApplication GTApp = GTClassFactory.Create<IGTApplication>( );

string coors = "0, 0, 0, 10, 0, 0, 10, 10, 0, 0, 10, 0, 0, 0, 0"; // change to actual coordinates
string sql = $@"WITH shape AS
  ( 
  	select 1 id, sdo_geometry( 3003, NULL, NULL
         , sdo_elem_info_array( 1, 1003, 1)
         , sdo_ordinate_array({coors} )) G FROM dual
  )
SELECT s.ID, t.x, t.y FROM shape S, sdo_util.getvertices( sdo_geom.sdo_centroid( S.G)) t";

ADODB.Recordset rs = GTApp.DataContext.Execute( sql, out _, (int)ADODB.CommandTypeEnum.adCmdText);

if( rs == null) return;
if( rs.RecordCount == 0 ) return;
rs.MoveFirst( );

string x = System.Convert.ToString( rs.Fields[ "X"].Value);
string y = System.Convert.ToString( rs.Fields[ "Y"].Value);

System.Diagnostics.Debug.WriteLine( string.Format( "x={0}, y={1}", x, y)); // x=5, y=5

rs.Close();
rs = null;

Rubberbanding in G/Technology

Whenever a user is digitizing a polygon, he expects feedback when moving the cursor before actually adding a point, the system should ‘rubberband’ the position under the cursor and only add it to a collection of points after the user left-mouse clicks in the Window. Key to rubberbending in G/Technology is responding to three events :

  1. CustomCommandHelper_Click
  2. CustomCommandHelper_MouseMove
  3. CustomCommandHelper_DblClick

CustomCommandHelper_Click

private void CustomCommandHelper_Click( object sender, GTMouseEventArgs e )
{
  if( this.PntCnt == 0)
  {
    this.PntCnt++;

    this.Idx = this.GeometryCreationService.AddGeometry( GTClassFactory.Create<IGTPolygonGeometry>(), StyleId);
    this.GeometryCreationService.AppendPoint( this.Idx, e.WorldPoint);
    return;
  }

  this.GeometryCreationService.AppendPoint( this.Idx, e.WorldPoint);
}

CustomCommandHelper_MouseMove

private void CustomCommandHelper_MouseMove( object sender, GTMouseEventArgs e )
{
  if( this.PntCnt == 0)
  {
    this.setGtStatusBar( "Click to start digitizing");
    return;
  }

  if( this.PntCnt == 1)
  {
    this.setGtStatusBar( "Click to add point");
    this.GeometryCreationService.SetDynamicPoint( this.Idx, e.WorldPoint);
    return;
  }
				
  this.setGtStatusBar( "Click to add point, Doubleclick to end");
  this.GeometryCreationService.SetDynamicPoint( this.Idx, e.WorldPoint);
}

CustomCommandHelper_DblClick

private void CustomCommandHelper_DblClick( object sender, GTMouseEventArgs e )
{
  this.PntCnt = 0;
}

Besides these 3 events, the following general code is used:

general code

private IGTApplication _GTApp = null;
private IGTApplication GTApp
{
  get
  {
    if( this._GTApp == null)
    {
      this._GTApp = GTClassFactory.Create<IGTApplication>();
    }

    return( this._GTApp);
   }
}

private IGTGeometryCreationService GeometryCreationService { get; set; } = GTClassFactory.Create<IGTGeometryCreationService>();

private void setGtStatusBar( string message)
{
  this.GTApp.SetStatusBarText( GTStatusPanelConstants.gtaspcMessage, message);
}

private int Idx { get; set; }
private const int StyleId = 5010;

The complete code can be downloaded from here.