Stephan Deckers

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

Workaround for the Dotnet Maui Tabs ‘OnAppearing’ event

Tabs in dotnet maui have an ‘OnAppearing’ event which you would expect to be called each time a tab is activated. This however is not the case and a possible workaround is to initialize data in a static manner and pass arguments using the ‘Arguments’-construct like shown in this post :

Tab1:

<!--?xml version="1.0" encoding="utf-8" ?-->
<contentpage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:class="MauiApp1.Pages.Tab1" xmlns:local="clr-namespace:MauiApp1.Data" title="Tab1">

	<contentpage.bindingcontext>
		<local:itemviewmodel>
			<x:arguments>
				<x:int32>10</x:int32>
			</x:arguments>
		</local:itemviewmodel>
	</contentpage.bindingcontext>

	<verticalstacklayout>
		<label text="Integers as argument">
		<collectionview x:name="collectionView" margin="5" itemtemplate="{StaticResource DataTemplate1}" itemssource="{Binding ItemCollection}" selectionmode="Single">
	</collectionview></label></verticalstacklayout>

</contentpage>

Tab2:

<!--?xml version="1.0" encoding="utf-8" ?-->
<contentpage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:class="MauiApp1.Pages.Tab2" xmlns:local="clr-namespace:MauiApp1.Data" title="Tab2">

	<contentpage.bindingcontext>
		<local:itemviewmodel>
			<x:arguments>
				<x:int32>20</x:int32>
			</x:arguments>
		</local:itemviewmodel>
	</contentpage.bindingcontext>

	<verticalstacklayout>
		<label text="Integers as argument">
		<collectionview x:name="collectionView" margin="5" itemtemplate="{StaticResource DataTemplate1}" itemssource="{Binding ItemCollection}" selectionmode="Single">
	</collectionview></label></verticalstacklayout>

</contentpage>

Tab3:

<!--?xml version="1.0" encoding="utf-8" ?-->
<contentpage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:class="MauiApp1.Pages.Tab3" xmlns:local="clr-namespace:MauiApp1.Data" title="Array of integers">

	<contentpage.bindingcontext>
		<local:itemviewmodel>
			<x:arguments>
				<x:array type="{x:Type x:Int32}">
					<x:int32>10</x:int32>
					<x:int32>11</x:int32>
					<x:int32>12</x:int32>
					<x:int32>13</x:int32>
					<x:int32>14</x:int32>
					<x:int32>15</x:int32>
				</x:array>
			</x:arguments>
		</local:itemviewmodel>
	</contentpage.bindingcontext>

	<verticalstacklayout>
		<label text="Array of integers as arguments">
		<collectionview x:name="collectionView" margin="5" itemtemplate="{StaticResource DataTemplate1}" itemssource="{Binding ItemCollection}" selectionmode="Single">
	</collectionview></label></verticalstacklayout>

</contentpage>

Where the Itemcollection looks like this:

public class ItemViewModel : INotifyPropertyChanged
{
	public ItemViewModel( )	{}

	public ItemViewModel( int categoryId)
	{
		this.ItemCollection = new ObservableCollection<item>( );

		for( int i = 0; i ‹ 4; i++)
		{
			this.ItemCollection.Add( new Item( ) { Name = $"V1-{categoryId}", Description = $"c{categoryId}/T1 rocks"});
		}
	}

	public ItemViewModel( int[] categoryIds)
	{
		this.ItemCollection = new ObservableCollection<item>( );

		for( int i = 0; i ‹ categoryIds.Length; i++ )
		{
			int categoryId = categoryIds[i];
			this.ItemCollection.Add( new Item( ) { Name = $"V1-{categoryId}", Description = $"c{categoryId}/T1 rocks"});
		}
	}

	public event PropertyChangedEventHandler PropertyChanged;

	protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
	{		
		ArgumentNullException.ThrowIfNull(e);
		PropertyChanged?.Invoke(this, e);
	}

	public ObservableCollection<item> ItemCollection	{ get; set; }
}

This will result in a nice flexible way of initialisation for tabs:

Integer argument
Integer argument
Array of integers as argument

An example project can be loaded from here.