Creating demand-loading entries automatically for your AutoCAD application using .NET

Here's a question I received recently by email:

How do you set up a .NET plugin for AutoCAD to install & demand load in the same way as ObjectARX plugins? The documentation is not very clear at making the distinctions visible.

In ARX terms, we currently write a set of registry entries as part of our installer, along with refreshing these via an AcadAppInfo registration during ARX load. The ARX itself can be located anywhere as long as the registry entries point to it. I'm not sure of the correct procedure for .NET plugins to duplicate this.

Augusto Gonçalves, from our DevTech Americas team, provided a solution for this which showed how to create demand-loading Registry keys programmatically based on the current assembly's name and location. It occurred to me that extending the code to make further use of reflection to query the commands defined by an assembly would make this really interesting, and could essentially create a very flexible approach for creation of a demand-loading entries as an application initializes or during execution of a custom command.

Here's the C# code:

using System;

using System.Collections.Generic;

using System.Reflection;

using System.Resources;

using Microsoft.Win32;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.Runtime;

 

namespace DemandLoading

{

  public class RegistryUpdate

  {

    public static void RegisterForDemandLoading()

    {

      // Get the assembly, its name and location

 

      Assembly assem = Assembly.GetExecutingAssembly();

      string name = assem.GetName().Name;

      string path = assem.Location;

 

      // We'll collect information on the commands

      // (we could have used a map or a more complex

      // container for the global and localized names

      // - the assumption is we will have an equal

      // number of each with possibly fewer groups)

 

      List<string> globCmds = new List<string>();

      List<string> locCmds = new List<string>();

      List<string> groups = new List<string>();

 

      // Iterate through the modules in the assembly

 

      Module[] mods = assem.GetModules(true);

      foreach (Module mod in mods)

      {

        // Within each module, iterate through the types

 

        Type[] types = mod.GetTypes();

        foreach (Type type in types)

        {

          // We may need to get a type's resources

 

          ResourceManager rm =

            new ResourceManager(type.FullName, assem);

          rm.IgnoreCase = true;

 

          // Get each method on a type

 

          MethodInfo[] meths = type.GetMethods();

          foreach (MethodInfo meth in meths)

          {

            // Get the methods custom command attribute(s)

 

            object[] attbs =

              meth.GetCustomAttributes(

                typeof(CommandMethodAttribute),

                true

              );

            foreach (object attb in attbs)

            {

              CommandMethodAttribute cma =

                attb as CommandMethodAttribute;

              if (cma != null)

              {

                // And we can finally harvest the information

                // about each command

 

                string globName = cma.GlobalName;

                string locName = globName;

                string lid = cma.LocalizedNameId;

 

                // If we have a localized command ID,

                // let's look it up in our resources

 

                if (lid != null)

                {

                  // Let's put a try-catch block around this

                  // Failure just means we use the global

                  // name twice (the default)

 

                  try

                  {

                    locName = rm.GetString(lid);

                  }

                  catch { }

                }

 

                // Add the information to our data structures

 

                globCmds.Add(globName);

                locCmds.Add(locName);

 

                if (cma.GroupName != null &&

                    !groups.Contains(cma.GroupName))

                  groups.Add(cma.GroupName);

              }

            }

          }

        }

      }

 

      // Let's register the application to load on demand (12)

      // if it contains commands, otherwise we will have it

      // load on AutoCAD startup (2)

 

      int flags = (globCmds.Count > 0 ? 12 : 2);

 

      // By default let's create the commands in HKCU

      // (pass false if we want to create in HKLM)

 

      CreateDemandLoadingEntries(

        name, path, globCmds, locCmds, groups, flags, true

      );

    }

 

    public static void UnregisterForDemandLoading()

    {

      RemoveDemandLoadingEntries(true);

    }

 

    // Helper functions

 

    private static void CreateDemandLoadingEntries(

      string name,

      string path,

      List<string> globCmds,

      List<string> locCmds,

      List<string> groups,

      int flags,

      bool currentUser

    )

    {

      // Choose a Registry hive based on the function input

 

      RegistryKey hive =

        (currentUser ? Registry.CurrentUser : Registry.LocalMachine);

 

      // Open the main AutoCAD (or vertical) and "Applications" keys

 

      RegistryKey ack =

        hive.OpenSubKey(

          HostApplicationServices.Current.RegistryProductRootKey,

          true

        );

      using (ack)

      {

        RegistryKey appk = ack.CreateSubKey("Applications");

        using (appk)

        {

          // Already registered? Just return

 

          string[] subKeys = appk.GetSubKeyNames();

          foreach (string subKey in subKeys)

          {

            if (subKey.Equals(name))

            {

              return;

            }

          }

 

          // Create the our application's root key and its values

 

          RegistryKey rk = appk.CreateSubKey(name);

          using (rk)

          {

            rk.SetValue(

              "DESCRIPTION", name, RegistryValueKind.String

            );

            rk.SetValue("LOADCTRLS", flags, RegistryValueKind.DWord);

            rk.SetValue("LOADER", path, RegistryValueKind.String);

            rk.SetValue("MANAGED", 1, RegistryValueKind.DWord);

 

            // Create a subkey if there are any commands...

 

            if ((globCmds.Count == locCmds.Count) &&

                globCmds.Count > 0)

            {

              RegistryKey ck = rk.CreateSubKey("Commands");

              using (ck)

              {

                for (int i = 0; i < globCmds.Count; i++)

                  ck.SetValue(

                    globCmds[i],

                    locCmds[i],

                    RegistryValueKind.String

                  );

              }

            }

 

            // And the command groups, if there are any

 

            if (groups.Count > 0)

            {

              RegistryKey gk = rk.CreateSubKey("Groups");

              using (gk)

              {

                foreach (string grpName in groups)

                  gk.SetValue(

                    grpName, grpName, RegistryValueKind.String

                  );

              }

            }

          }

        }

      }

    }

 

    private static void RemoveDemandLoadingEntries(bool currentUser)

    {

      try

      {

        // Choose a Registry hive based on the function input

 

        RegistryKey hive =

          (currentUser ?

            Registry.CurrentUser :

            Registry.LocalMachine);

 

        // Open the main AutoCAD (vertical) and "Applications" keys

 

        RegistryKey ack =

          hive.OpenSubKey(

            HostApplicationServices.Current.RegistryProductRootKey

          );

        using (ack)

        {

          RegistryKey appk = ack.OpenSubKey("Applications", true);

          using (appk)

          {

            // Delete the key with the same name as this assembly

 

            appk.DeleteSubKeyTree(

              Assembly.GetExecutingAssembly().GetName().Name

            );

          }

        }

      }

      catch { }

    }

  }

}

If you drop this code into an existing project, you should be able simply to add a call to DemandLoading.RegistryUpdate.RegisterForDemandLoading() during your IExtensionApplication's Initialize() method or during a custom command.

Here's an example of the Registry keys created by this code (exported from Regedit) when called from the Initialize() method of the application in this previous post:

Windows Registry Editor Version 5.00

 

[HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R18.0\ACAD-8001:409\Applications\OffsetInXref]

"DESCRIPTION"="OffsetInXref"

"LOADCTRLS"=dword:0000000c

"LOADER"="C:\\Program Files\\Autodesk\\AutoCAD 2010\\OffsetInXref.dll"

"MANAGED"=dword:00000001

 

[HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R18.0\ACAD-8001:409\Applications\OffsetInXref\Commands]

"XOFFSETLAYER"="XOFFSETLAYER"

"XOFFSETCPLAYS"="XOFFSETCPLAYS"

 

You can see that our code found the commands defined by the application and created Registry entries which tell AutoCAD to load the module when one of them is chosen by the user. You'll notice the LOADCTRLS value is c (hexadecimal for 12), which means the application will be loaded "on demand" as a specified command is invoked, but we could also adjust the code to force this to 2, which would mean the module would be loaded on AutoCAD startup (the default when no commands are found). This would actually be a good idea, in this case, as the command hooks into the OFFSET command, and we can't demand-load a module on invocation of a built-in command.

You'll also notice that the keys were created under R18.0\ACAD-8001:409 (the English version of AutoCAD 2010), but if the module was loaded in a different language version of an AutoCAD-based vertical product (French AutoCAD Architecture 2009, for instance) then the root key would be the one for that product. All you have to do is load the module once in the AutoCAD-based product of your choice, and it will be registered for automatic loading from then onwards.

This is a useful technique for people who want to deploy .NET modules without installers, or for people who wish applications to re-create their demand-loading keys on load (something this code currently does not do, by the way: if the application's key is found we do not recreate the contents for the sake of efficiency… you may want to change the code to force creation of the keys should you be adding new commands regularly to your application, for instance).

Update:

I've gone ahead and updated the above code as per the information in this more recent post.

23 responses to “Creating demand-loading entries automatically for your AutoCAD application using .NET”

  1. kean, thanks for this really cool solution.

    What it does is nearly identical to another program I found through a link in one of your older posts (see "Preventing a .NET module from being loaded by AutoCAD"), that was published by tony tanzillo

    I was using a altered version of tony's ExtensionApplicationInfo class for nearly a year now with great success. Before I dissect your version, may I ask if it improves upon Tony's version? just thought I'd ask so I can add what ever enhancement your version has into my codebase.

    thanks again,
    Frank C.

  2. Hi Frank,

    I took a quick look at Tony's solution, which (from what I can tell) has comparable functionality...

    One difference I noticed immediately is around licensing rather than the actual functionality: the code I've provided doesn't come with any restrictions around use in commercial applications.

    Regards,

    Kean

  3. hey kean, thanks. I went through your code, and yes, they seem to be similar. Oddly they are actually very similar to each other, not just in what they do, but even with coding styles. For example, the one thing that stuck out about that was the use of the C# '?' operator. Both the cadzone code and your code use it in exactly the same ways (selecting the LOADCTRLS value and the registry hive).

    What an uncannily-strange coincidence, wouldn't you say?

  4. Kean Walmsley Avatar

    Hi Frank,

    I hope you're not accusing me of plagiarism... 🙂

    I can honestly say that while I had taken a quick look at Tony's code originally when he posted the comment, I didn't look into the details, and certainly didn't remember that the code even existed when I came to write my own.

    I don't think the coding style similarities are entirely coincidental: we both have a background with C++ which is where the '?' operator comes from, originally. And while unfamiliar or strange-looking to some, it is an elegant construct for simple if-then-else clauses.

    Cheers,

    Kean

  5. Kerry L. Galloway Avatar
    Kerry L. Galloway

    Kean,
    Have discovered one thing about this routine in the VB.Net version which was distributed at AU in November or so and that is that if an Applications keys does not exist it won't make one. I ran it on a brand new instance of 2010 and it had no Applications key.
    Just a heads for folks that may not have dug into the registry when the code failed.

    Kerry

  6. Kerry,

    Yes - I heard about this just the other day (see the bottom of this post).

    I will need to update this post: it's not as easy a fix as when creating the keys from an external app (CreateSubKey() works well there but fails within AutoCAD if the Applications key already exists - most commonly the case) but it shouldn't be too hard.

    Kean

  7. Kean Walmsley Avatar

    Right - I've updated the various versions, as well as creating one for F#, and have posted them all here.

    It was as simple as using CreateSubKey(), I just had to remember to open the parent key for write (duh).

    Kean

  8. Kean Walmsley Avatar

    The original code ended up getting mangled when I tried to update this post, so I went ahead and overwrote it with the most recent version.

    Kean

  9. Need some help. i used your demaload code for .net. I'm now converting my app to 2013 and now i'm getting Error: 'CommandMethodAttribute' is ambiguous in the namespace 'Autodesk.AutoCAD.Runtime'. I'm getting this "ambiguous" error with several other command too.

    I did add the Core DLL and update other for 2013 SDK.

    Any ideal what i'm not seeing..

  10. Ok i found the issue. I had a "refence Path" set to SDK 2011, so when i loaded the 2013 dll it loaded the 2011 dlls instead. I removed the path and it corrected it self and all is well.

    Sorry for bad post.

  11. Hi Kean, I found this topic very interesting, but there are some things not so clear to me.

    Ok, I had developed my MyAcadApp.dll, my goal is to produce a set-up package that installs all the components needed by MyAcadApp.dll, e.g. partial CUI files, and as result of this, after performing installation, I will have somewhere on my AutoCAD dropdown menù the entry to recall via my custom command 'MyAcadApp' the application.

    So, briefly, what are the mainly steps to do in order to reach this?

    Thanks

  12. Hi Luigi,

    I would recommend using the more modern autoloader mechanism for this:

    keanw.com/2011/09/autodesk-exchange-preparing-your-autocad-application-for-posting.html

    Regards,

    Kean

  13. Luigi Ballotta Avatar

    Thanks Kean for the kind answer, I'm reading through and seems to be very interesting to solve my issue.

    Regards

    Luigi

  14. Luigi Ballotta Avatar

    The solution is very interesting using with AutoCAD2013, but it seems not able to work with AutoCAD2010, correct?

    So going back a little behind, what are the steps to do in order to perform this task using AutoCAD2010?

    Thank-you.

    Luigi

  15. Correct. If you need to support AutoCAD 2010, these posts should be of some help:

    Build an installer - keanw.com/2010/09/building-an-installer-part-1.html

    Load a partial CUI - keanw.com/2006/11/loading_a_parti.html

    Kean

  16. Luigi Ballotta Avatar

    Thanks for your patience. I will read the posts.

    Regards,

    Luigi

  17. Hi guys,

    the line 'prodKey = HostApplicationServices.Current.RegistryProductRootKey;' is generating an error. Here is the error:

    'Autodesk.AutoCAD.DatabaseServices.HostApplicationServices' does not contain a definition for 'RegistryProductRootKey' and no extension method 'RegistryProductRootKey' accepting a first argument of type 'Autodesk.AutoCAD.DatabaseServices.HostApplicationServices' could be found (are you missing a using directive or an assembly reference?)

    These are my references:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;

    using Microsoft.Win32;
    using System.Reflection;

    /// AutoCAD specific reference (Local Copy - False): accoremgd.dll, AcDbMgd.dll, AcMgd.dll
    using Autodesk.AutoCAD.ApplicationServices;
    using Autodesk.AutoCAD.DatabaseServices;
    using Autodesk.AutoCAD.EditorInput;
    using Autodesk.AutoCAD.Runtime;

    I did change nothing from what was available here in this forum (the code I copied); just the application name. I wonder what I'm missing? BTY, the same code in VB.NET does not generate any error and also,... when I went to 'Current' definition - the VS opened for me this (just few lines):

    ...
    namespace Autodesk.AutoCAD.DatabaseServices
    [Wrapper("AcDbHostApplicationServices")]
    public abstract class HostApplicationServices : RXObject
    {
    protected internal HostApplicationServices();
    ...
    public static HostApplicationServices Current { get; set; }
    ...

    any help?

  18. Hi Sam,

    In AutoCAD 2013 that API was replaced with two variants. The one you choose depends on the section of the Registry you're interested in:

    HostApplicationServices.Current.UserRegistryProductRootKey
    HostApplicationServices.Current.MachineRegistryProductRootKey

    I hope this helps,

    Kean

  19. Kean,
    Great stuff as always...
    I have a question regarding the Removal of Resigstry keys.
    First I really just want this loading globally on autocad launch and maintaining itself so I've put the init registry code in my App Init section - but since commands exist if found them and went the command option. Anyway I forced it to 2 and got a doc.collection changed event going to track new documents. What I find though is unless I run UnregisterForDemandLoading on my apps Terminate sub (effectively removing registry stuffs on acad close), then I get an unhandled exception when autocad shuts down... but that means I have to netload my assembly on autocad startup - rewriting reg every time and effectivley getting little benifit. Is this really just for dwg to dwg loading? Can't we leave the reg keys in there once we're happy with our app and know it's there to stay?

  20. Carl,

    There must be something else going on... you shouldn't have to remove any Registry entries on close.

    The strange thing is that the crash comes on shutdown: typically there's a problem with accessing the current document (before one is available) on startup.

    I suggest debugging this to better understand what's happening (I would start with breakpoints in Init and Terminate).

    Regards,

    Kean

  21. Thanks, that's kinda what I figured (in that they don't need removal). There are clearly other entries of the same fasion the Autocad Dev team uses and leaves in.

    When I work it out I'll let everyone know assuming its interesting 🙂 by the way, with this code is it same to run every load to make sure it's there or is this something you should set up in an installer only?

  22. hmmm.

    I think I found [this - lol] bug.

    In this case an obsessive interest in clearing things cost me.

    Dim RegUp As New RegistryUpdate
    RegUp.RegisterForDemandLoading()
    RegUp = Nothing <- this baby here, bad.

    it needs to not be cleared (apparently) in this case. fixed my issues.

  23. Great - glad you worked it out.

    Running it on load is harmless enough (it stops as soon as it finds the Registry key already exists... you might also optimise it to check that before extracting command info from the assembly, if you were really keen). But if you want to run it from an installer there is an EXE version available I called RegDL:

    keanw.com/2010/09/an-updated-version-of-regdl-available.html

    Kean

Leave a Reply to Frank C. Cancel reply

Your email address will not be published. Required fields are marked *