How to write an Inventor Add-In using F#


1. Preface
2. Basic steps
3. The Add-In F# project
4. The Add-in minimum code
5. Register the Add-In
6. Debug the Add-In
7. Behind the scenes
8. Summary
9. References

1. Preface

Last summer I was wandering around in a Cambridge bookshop, when I came across at Robert Pickering's book: "Foundations of F#". I spent a couple of hours reading about the F# language (of course after I bought the book), an interesting mix of functional and imperative programming style.

I started programming in AutoCAD several years ago using AutoLisp first and ADS/ARX C++ environment later. Even though AutoLisp has very little to do with the Lisp language I had the chance to see some aspects of functional languages. Now is the time of .NET languages but I have to say sometime I miss the neat style that just a functional language provides. This is the reason I decided to invest a bit of time learning F#.

In this article I will show you how to write an Autodesk Inventor add-in using F#.

2. Basic steps

Of course you need Autodesk Inventor (if you are reading this article probably it is already installed on your PC). Download the F# package from the Microsoft Research website: (http://research.microsoft.com/fsharp/fsharp.aspx) . The release I used to write the add-in is FSharp-1.9.2.9.

3. The Add-In F# project

It's possible to write software for Autodesk Inventor in several ways: I decided to write an Add-in because is the most powerful way you can have to communicate with the internal API. Inventor was designed to support clients that support COM technologies, this make the task of writing an Add-in very easy using VB6, but you will face more complexities using a .NET language.

F# language is very well designed to work in Visual Studio 2005. The setup automatically add an Add-in for Visual Studio 2005. Even though the Add-in is not really stable yet, you will be surprised of the good integration with the IDE. I will use this VS2005 for the job though this in not mandatory. Start Visual Studio and create a new F# project.

An Inventor Add-In is basically a dll so we have to force the F# compiler to make it instead of the default executable. Go to the project properties and set the project type to DLL. This will add the -a parameter to the compiler command line.

To have a working Add-in we need a strong-named add-in assembly and a strong-named Inventor Interop assembly. This is basically to avoid namespace conflicts once you add the assembly to the GAC. First you need to create a strong-name key file. This is quite easy, go to Start->Programs->Microsoft Visual Studio 2005 Tools->Microsoft Visual Studio 2005 Command Prompt and once in the command prompt move to the folder of your project. The reason we use the VS2005 command prompt is the PATH environment is correct in order to use the VS tools. Here you can type:

sn -k keyfile.snk

Visual Studio now creates a signature file for you. Go back to the project properties and set the Strong Naming to "Public Private Key File" and Strong name key file to your keyfile.snk. Now you need to reference the Inventor Interop Assembly. Unfortunately Autodesk does not provide this assembly yet so you have to create your own. Go to your Inventor folder and copy the "RxInventor.tlb" file from the "bin" folder to your project folder (where your keyfile.snk is located). Again, from the command prompt type:

tlbimp RxInventor.tlb /out:Inventor.dll /keyfile:keyfile.snk /primary /namespace:Inventor

this will create a strong-named Inventor interop assembly (Inventor.dll) avoiding conflicts. You will see lots of messages during the creation of the dll. You can basically ignore all of them. Go back to your project and reference the newly created dll. For F# this means adding a -r (we do not need to copy locally the dll, so we use the lowercase) before the name of the Inventor interop dll

4. The Add-in minimum code

Few lines of code are necessary to Inventor to connect to your add-in. Create a new source file "Inventor.fs" for your Visual Studio 2005 project.

Open the file and add the standard #light preprocessor definition to tell F# we require the light syntax. After we add few lines describing our assembly

[<System.Runtime.InteropServices.ComVisible(true);
  System.Runtime.InteropServices.ClassInterface(System.Runtime.InteropServices.ClassInterfaceType.AutoDual);
  System.Runtime.InteropServices.ProgIdAttribute("FSInventor.AddIn");
  System.Runtime.InteropServices.GuidAttribute("DD9293F8-6881-3614-9FB4-11AAB3DCCBA1");
  AssemblyVersion("1.0.*");
  AssemblyKeyFile("keyfile.snk");
  AssemblyCompany("QS Informatica");
  AssemblyProduct("F# Sample Add-In");>]

Not all the declarations are actually necessary, but few of them are required to have a correct assembly. Then we declare a local namespace and reference some assemblies we are going to use in our add-in.

namespace FSInventorAddIn

open Inventor
open System
open System.Reflection //.Assembly
open System.Runtime.InteropServices
open System.Windows.Forms
open Microsoft.Win32

Now we can start writing the required code. We create a class exposing few methods that Inventor requires to be defined. To make the Add-In just a bit more interesting we will create a toolbar with a single button showing a welcome message. First we create a Button Class as a wrapper for the Inventor Button this will make the code clear (of course you need to know how Inventor works). A couple of points I want to show you: first, you can define F# comments in several ways:

  • // this is a single line comment

  • (* this is a for a multiline comment *)

  • /// this is a DOC comment.

You can tell the F# compiler to produce the source documentation at compiler time adding the -doc mydocfile.xml option to the command line:

fsc --fullpaths --progress -a -O3 --keyfile "E:\Src\F#\Inventor\InventorAddIn\keyfile.snk" -r Inventor.dll -doc InventtorAddIn.xml

Second, F# can infers type automatically, so it is not necessary to declare types. This is required only in few situations: just append a colon after the parameter declaration (inventor : Inventor.Application)

/// Wrap the Inventor Button with my button class
type Button = class
    val mutable button : ButtonDefinition
    val mutable buttonHandler : ButtonDefinitionSink_OnExecuteEventHandler    
    new () = {button = null; buttonHandler = null}
    
    // add new button
    member x.Add(inventor:Inventor.Application, dispname, intname, classification, clientid, descr, tooltip, stdicon, lrgicon, btndisplay) =
        x.button <- inventor.CommandManager.ControlDefinitions.AddButtonDefinition(dispname, intname, classification, clientid, descr, tooltip, stdicon, lrgicon, btndisplay)
        
    // set the button handler                                                                                      
    member x.SetHandler(btndelegate) =
        x.buttonHandler <- ButtonDefinitionSink_OnExecuteEventHandler(btndelegate)                                                                             
        x.button.add_OnExecute(x.buttonHandler)                
    
    // release the button
    member x.Detach () =
        x.button.remove_OnExecute(x.buttonHandler)
        Marshal.ReleaseComObject(x.button) |> ignore
    
    // the 'get' property
    member x.InvButton with get() = x.button    
end

The following code creates the basic Add-in server class. I attempted to create a complete Add-In but, in fact, just the "Activate" and "Deactivate" methods seem to be required for the Add-In to be valid. Let's have a look at the code. We explicitly declare the GUID for the Add-In: this make the Windows registry inspection easier. You can generate your own valid Add-In using lots of different tools. VS 2005 provide a simple utility to generate a valid GUID. If you come from VB6 then this is probably new: VB6 automatically generates a new GUID every time you recompile the project (you have to force the binary compatibility to avoid this behaviour).

// explicit the add-in registration GUID
[<GuidAttribute("5E707342-8849-11DC-8314-0800200C9A66")>]
type AddInServer = class
    
    val mutable inventor : Inventor.Application    
    val buttonHandler : ButtonDefinitionSink_OnExecuteEventHandler
    val button : Button    
    new () = { 
        inventor = null; 
        buttonHandler = null;
        button = new Button()  // add the new button here
    }  
    
    // button callback
    member x.OnButtonExecute(context : NameValueMap) =
        System.Windows.Forms.MessageBox.Show("Hello from F#!") |> ignore        
        
    interface Inventor.ApplicationAddInServer with     
                
        member x.Activate(addInSiteObject : Inventor.ApplicationAddInSite, firstTime : bool) = 
                        
            x.inventor <- addInSiteObject.Application 
            
            // create the button
            x.button.Add(x.inventor, "F# Button",  
                                     "F#_Button_Internal", 
                                     Inventor.CommandTypesEnum.kShapeEditCmdType, 
                                     "{982C624F-77F7-42AA-8477-C86D027AFB46}", 
                                     "Description", 
                                     "Tooltip", 
                                     System.Type.Missing, 
                                     System.Type.Missing, 
                                     Inventor.ButtonDisplayEnum.kAlwaysDisplayText)
                                      
            x.button.SetHandler(x.OnButtonExecute)            
        
            // the first time create the toolbar
            if firstTime = true then
                let commandbar = x.inventor.UserInterfaceManager.CommandBars.Add("F# Toolbar",
                                                                                 "F#_Toolbar_Internal",
                                                                                 CommandBarTypeEnum.kRegularCommandBar,
                                                                                 "{963308E2-D850-466D-A1C5-503A2E171552}")
                                                                             
                // add the button to the toolbar
                commandbar.Controls.AddButton(x.button.InvButton, 0) |> ignore
                commandbar.Visible <- true
        
        member x.Deactivate() =
            // detach button
            x.button.Detach()
            
            Marshal.ReleaseComObject(x.inventor) |> ignore
            x.inventor <- null
            GC.WaitForPendingFinalizers()
            GC.Collect() |> ignore       
            
        member x.ExecuteCommand(cmdID:int) =
            ignore()            
       
        member x.Automation with get() = null       
    end
        
    

I think the code is self explanatory: you have to define the Activate/Deactivate methods the Inventor.ApplicationAddInServer Interface exposes. F# syntax is clear and the #light preprocessor definition makes sure you can use indentation (like Python) to correctly formatting your code.

Probably you have noticed some weird constructs like |> ignore:

commandbar.Controls.AddButton(x.button.InvButton, 0) |> ignore 

do not forget F# is a functional language (even though in this example we are using the imperative programming style). The AddButton method returns a value (CommandBarControl) and you can't just get rid of it. You have to turn this function into a function of type unit (a kind of void for C++ people). You can do this in several ways: bind the object to the _ identifier or just using the pass-forward operator to pass the returned value to the ignore function. This is a very powerful construct of F#, you will save hundreds of lines of code once you master the pass-forward operator.

let _ = commandbar.Controls.AddButton(x.button.InvButton, 0)
ignore(commandbar.Controls.AddButton(x.button.InvButton, 0))

As you can see from the picture, F# has a really good integration with VS2005. Even during the typing F# complains for type consistency, providing warnings and highlighting the wrong code.

In the same class we define the registration functions. They are actually unnecessary since we miss the "register for com interop" project flag available in C#. Supposed that we have such flag the code would be more or less like the following snippet. Anyway, I will do the registration manually later.

    (* Add-in Registration functions *)    

    static member GetSubKeyName (inType :Type ) =
        @"CLSID\{" + (inType.GUID.ToString()).ToUpper() + @"}"   
        
    [<ComRegisterFunctionAttribute>]
    static member RegisterFunction (inType :Type ) =
        let clsid = Registry.ClassesRoot.CreateSubKey(AddInServer.GetSubKeyName inType)
        clsid.SetValue(null, "F# InventorAddIn")
        let mutable subkey = clsid.CreateSubKey(@"Implemented Categories\{39AD2B5C-7A29-11D6-8E0A-0010B541CAA8}")
        subkey.Close()
        
        subkey <- clsid.CreateSubKey("Settings")
        subkey.SetValue("AddInType", "Standard")
        subkey.SetValue("LoadOnStartUp", "1")
        subkey.SetValue("SupportedSoftwareVersionGreaterThan", "11..")
        subkey.SetValue("Version", 0)
        subkey.Close()
        
        subkey <- clsid.CreateSubKey("Description")
        subkey.SetValue(null, "InventorAddIn1") |> ignore
      

    [<ComUnregisterFunctionAttribute>]
    static member UnregisterFunction (inType :Type ) =
              
        let clssRoot = Microsoft.Win32.Registry.ClassesRoot
        let clsid = clssRoot.OpenSubKey(AddInServer.GetSubKeyName inType, true)
        clsid.SetValue(null, "")
        clsid.DeleteSubKeyTree(@"Implemented Categories\{39AD2B5C-7A29-11D6-8E0A-0010B541CAA8}") // required by Inventor
        clsid.DeleteSubKeyTree("Settings")
        clsid.DeleteSubKeyTree("Description")
        clsid.Close() |> ignore          
    
end // close the class

Compile your project: you should find the InventorAddIn.dll in your project folder.

5. Register the Add-In

As I mentioned before, I am going to manually register the Add-In. Go back to the Visual Studio 2005 command prompt and move to your project folder. We use the RegAsm tool to create the required REG file.

RegAsm InventorAddIn.dll /codebase /regfile:InventorAddIn.reg

Unfortunately this is not enough. Inventor does not recognize this dll as an Add-in unless you create some additional entries. Open the InventorAddIn.reg file with your favourite editor and add the following lines (bold lines only):

REGEDIT4

[HKEY_CLASSES_ROOT\FSInventorAddIn.AddInServer]
@="FSInventorAddIn.AddInServer"

[HKEY_CLASSES_ROOT\FSInventorAddIn.AddInServer\CLSID]
@="{5E707342-8849-11DC-8314-0800200C9A66}"

[HKEY_CLASSES_ROOT\CLSID\{5E707342-8849-11DC-8314-0800200C9A66}]
@="FSInventorAddIn.AddInServer"

[HKEY_CLASSES_ROOT\CLSID\{5E707342-8849-11DC-8314-0800200C9A66}\InprocServer32]
@="mscoree.dll"
"ThreadingModel"="Both"
"Class"="FSInventorAddIn.AddInServer"
"Assembly"="inventoraddin, Version=0.0.0.0, Culture=neutral, PublicKeyToken=1dfdf97c76eb5dcd"
"RuntimeVersion"="v2.0.50727"
"CodeBase"="file:///E:/Src/F#/Inventor/InventorAddIn/InventorAddIn.dll"

[HKEY_CLASSES_ROOT\CLSID\{5E707342-8849-11DC-8314-0800200C9A66}\InprocServer32\0.0.0.0]
"Class"="FSInventorAddIn.AddInServer"
"Assembly"="inventoraddin, Version=0.0.0.0, Culture=neutral, PublicKeyToken=1dfdf97c76eb5dcd"
"RuntimeVersion"="v2.0.50727"
"CodeBase"="file:///E:/Src/F#/Inventor/InventorAddIn/InventorAddIn.dll"

[HKEY_CLASSES_ROOT\CLSID\{5E707342-8849-11DC-8314-0800200C9A66}\ProgId]
@="FSInventorAddIn.AddInServer"

[HKEY_CLASSES_ROOT\CLSID\{5E707342-8849-11DC-8314-0800200C9A66}\Implemented Categories\{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}]

[HKEY_CLASSES_ROOT\CLSID\{5E707342-8849-11DC-8314-0800200C9A66}\Implemented Categories\{39AD2B5C-7A29-11D6-8E0A-0010B541CAA8}]

[HKEY_CLASSES_ROOT\CLSID\{5E707342-8849-11DC-8314-0800200C9A66}\Settings]
"LoadOnStartUp"="1"
"Type"="Standard"

[HKEY_CLASSES_ROOT\CLSID\{5E707342-8849-11DC-8314-0800200C9A66}\Description]
@="F# Simple Add-In: QS Informatica"

[HKEY_CLASSES_ROOT\CLSID\{5E707342-8849-11DC-8314-0800200C9A66}\Required Categories\{39AD2B5C-7A29-11D6-8E0A-0010B541CAA8}]

Probably you will also see the entries created for the Button class. The only remaining thing to do is registering the .reg file. You can do that from the contextual menu or from the command prompt:

regedit /s InventorAddIn.reg

this will add the required keys to the Windows registry..

6. Debug the Add-In

As I said, F# provides a smooth integration with Visual Studio 2005. This means you can actually debug your F# code in VS2005. Open the project properties and add the required debug information.

Now you are free to place a couple of breakpoints in your code and debug as you normally would do in your C++/CLI, VB.NET or C# programs.

Run the debug session. As soon as Inventor starts you should see the new F# toolbar with the F# button. Press the button and the welcome message should appear.

In case something goes wrong, make sure your Add-in has been loaded. Go to the tools menu and check whether your Add-in is in the list.

7. Behind the scenes

You may be curious to know how your F# Add-In looks like. You can use the excellent Lutz Roeder's .NET reflector (http://www.aisto.com/roeder/dotnet/) to dig into your add-in assembly. The structure of the AddIn assembly looks like this:

The required AddIn server class look like this:

I compared the signature of this Add-In with a standard C# Add-In. They look pretty similar but in our Add-in the Activate/Deactivate methods are private whereas in the C# Add-In they are public. I attempted to expose the two methods out of the Inventor.ApplicationAddInServer Interface declaration. Doing this the two methods change to public and the assemblies looks absolutely identical. Despite this Inventor does not like the Add-In and refuses to load it.

The following structure represents our Button class.

8. Summary

In this tutorial I explained the basic steps required to create a .NET Add-In for Autodesk Inventor, using F#. I think this article can be useful to programmers that want to investigate the Inventor Add-In mechanism and to everyone interested in learning F#.

9. References

Here are few references on the argument

This tutorial has been created using DOCBOOK.