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.