I received a really intriguing question by email from Dave Wolfe on Friday afternoon:
Due to your post on the topic, I wanted to improve my installation method for .Net modules. While examining the topic, I ran into a few concerns that shaped my development process. The game changer is that I couldn’t find a way to use reflection to check a .net module for the CommandMethodAttributes without being in an AutoCAD session. The CommandMethodAttribute crashes on initialization outside of AutoCAD.
Dave went on to describe a couple of ways he’d found to work around this problem by driving AutoCAD, but the question of whether this was possible started nagging away at me. (Thanks for the thought-provoking question, Dave! :-)
For those unfamiliar with the issue: the previous post Dave referred to shows some code which allows modules to create their appropriate demand-loading Registry keys automatically, typically when their modules get loaded for the first time. This is a technique I’ve since used widely, particularly in the Plugins of the Month we’re posting to Autodesk Labs. The trick is that the code depends on the .NET assemblies being fully loaded – which means loaded into AutoCAD, as they depend on core AutoCAD assemblies. It would probably be possible to get at least part of the way there with using RealDWG from .NET, but it did feel like there ought to be a simpler way. Back in the day (and presumably still now), it was possible to do something similar for ObjectARX modules, providing they used RGS files for their Registry key modifications: our AutoCAD Architecture team had developed a tool, AecReg.exe, which could load the .ARX without resolving its dependencies and manually extract and create the Registry keys defined in the module’s RGS resource.
The desired outcome was something similar for .NET: it would load the assembly without fully loading its dependencies and then be able to parse the metadata attached to each of the methods to identify the commands it defines and then create the corresponding Registry keys automatically. And all from a separate executable which could be called from a batch file or an installer to do this without having to fire up AutoCAD.
On Friday evening I did some quick research and found an interesting capability of the .NET Framework: that of loading assemblies for reflection only. This looked really promising, so I ended up having to play around with this over the weekend. :-S
I’m pleased with the results, though. I managed to create a simple Console Application, modeled to some degree on RegAsm.exe (the .NET Framework utility application for registering .NET assemblies), which allows you to register and unregister AutoCAD .NET assemblies without any dependency on AutoCAD. I’ve named the application RegDL, for Register Demand-Loading, but the idea isn’t really to provide a definitive tool: people with an interest can use the code to create their own tailored application to be integrated into their deployment process. With suggestions on improving/extending this utility being most welcome, of course. :-)
The application’s project contains two C# source files.
The first is for the command-line parsing and overall console application behaviour (I used the the default “Program.cs” file for this). I found and used an interesting trick to stop dependent assemblies from being loaded fully rather than for reflection: during the current AppDomain’s ReflectionOnlyAssemblyResolve() event we load the specified assembly for reflection-only, effectively pre-empting a full assembly load.
Here’s the C# source file:
using System.Reflection;
using System.IO;
using System;
using DemandLoading;
namespace RegDL
{
class Program
{
static void Main(string[] args)
{
// We need at least one argument (the assembly name)
// or the request for help
if (args.Length <= 0 ||
args.Length == 1 &&
(args[0] == "/?" || args[0].ToLower() == "/help"))
{
PrintUsage();
return;
}
// Get the first argument and check the file exists
string asmName = args[0];
if (!File.Exists(asmName))
{
Console.WriteLine(
"RegDL : Unable to locate input assembly '{0}'.",
asmName
);
return;
}
// Now we get the optional flags
bool startup = false;
bool hklm = false;
bool unreg = false;
bool force = false;
for (int i=1; i < args.Length; i++)
{
string arg = args[i].ToLower();
startup |= (arg == "/startup");
hklm |= (arg == "/hklm");
unreg |= (arg == "/unregister");
force |= (arg == "/force");
}
// As each dependent assembly is resolved, we need to make
// sure it is loaded via Reflection-Only, not a full Load
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve +=
delegate(object sender, ResolveEventArgs rea)
{
return Assembly.ReflectionOnlyLoad(rea.Name);
};
// Let's load our assembly via Reflection-Only
Assembly assem =
Assembly.ReflectionOnlyLoadFrom(asmName);
bool res = false;
if (unreg)
{
// Unregister the assembly, if possible
try
{
res = RegistryUpdate.UnregisterForDemandLoading(assem);
}
catch(Exception ex)
{
Console.WriteLine("Exception: {0}", ex);
}
if (res)
{
Console.WriteLine(
"Removed demand-loading information for '{0}'.",
asmName
);
}
else
{
Console.WriteLine(
"Could not remove demand-loading information for '{0}'.",
asmName
);
}
}
else
{
// Register the assembly, if possible
try
{
res =
RegistryUpdate.RegisterForDemandLoading(
assem, !hklm, startup, force
);
}
catch(Exception ex)
{
if (ex is ReflectionTypeLoadException)
{
Console.WriteLine(
"Trouble loading types: do you have AcMgd.dll and " +
"AcDbMgd.dll in the same folder as RegDL.exe?"
);
}
else
{
Console.WriteLine("Exception: {0}", ex);
}
}
if (res)
{
Console.WriteLine(
"Registered assembly '{0}' for AutoCAD demand-loading.",
asmName
);
}
else
{
Console.WriteLine(
"Could not register '{0}' for AutoCAD demand-loading.",
asmName
);
}
}
}
// Print a usage message
static void PrintUsage()
{
const string indent = " ";
const string start = "Version=";
string version =
Assembly.GetExecutingAssembly().FullName;
if (version.Contains(start))
{
// Get the string starting with the version number
version =
version.Substring(
version.IndexOf(start) + start.Length
);
// Strip off anything after (and including) the comma
version =
version.Remove(version.IndexOf(','));
}
else
version = "";
Console.WriteLine(
"AutoCAD .NET Assembly Demand-Loading Registration " +
"Utility {0}", version
);
Console.WriteLine(
"Written by Kean Walmsley, Autodesk."
);
Console.WriteLine(
"http://blogs.autodesk.com/through-the-interface"
);
Console.WriteLine();
Console.WriteLine("Syntax: RegDL AssemblyName [Options]");
Console.WriteLine("Options:");
Console.WriteLine(
indent +
"/unregister Remove demand-loading keys for this assembly"
);
Console.WriteLine(
indent +
"/hklm Write keys under HKLM rather than HKCU"
);
Console.WriteLine(
indent +
"/startup Assembly to be loaded on AutoCAD startup"
);
Console.WriteLine(
indent +
"/force Overwrite keys, should they already exist"
);
Console.WriteLine(
indent +
"/? or /help Display this usage message"
);
}
}
}
The second file is a version of the code in the previous post referred to earlier, but with a few key changes: firstly, as we’re running outside of AutoCAD we don’t have access to its types, such as CommandMethodAttribute. So we have to use a somewhat more manual approach, using CustomAttributeData.GetCustomAttributes() to get all the custom attributes, and then parse the arguments provided for any attributes of the class named CommandMethodAttribute.
Another key difference: as AutoCAD isn’t running, we clearly can’t query it for the Registry location under which we should install our demand-loading keys. The approach I’ve taken in this initial version is to search for the “current” AutoCAD version (which typically means the most recently executed). It’s certainly possible to extend this application to do more analysis on the versions of AutoCAD installed before either deciding which to install the application to or providing the user with the option to select one or more of those versions via some kind of dialog.
Here’s the second source file – I’ve personally called this one “demand-loading-external.cs” (to distinguish it from “demand-loading.cs”, the file I’ve used inside AutoCAD).
using System.Collections.Generic;
using System.Reflection;
using System.Resources;
using System;
using Microsoft.Win32;
namespace DemandLoading
{
public class RegistryUpdate
{
public static bool RegisterForDemandLoading(
Assembly assem, bool currentUser, bool startup, bool force
)
{
// Get the assembly, its name and location
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 method's custom attribute(s)
IList<CustomAttributeData> attbs =
CustomAttributeData.GetCustomAttributes(meth);
foreach (CustomAttributeData attb in attbs)
{
// We only care about our specific attribute type
if (attb.Constructor.DeclaringType.Name ==
"CommandMethodAttribute")
{
// Harvest the information about each command
string grpName = "";;
string globName = "";
string locName = "";
string lid = "";
// Our processing will depend on the number of
// parameters passed into the constructor
int paramCount = attb.ConstructorArguments.Count;
if (paramCount == 1 || paramCount == 2)
{
// Constructor options here are:
// globName (1 argument)
// grpName, globName (2 args)
globName =
attb.ConstructorArguments[0].ToString();
locName = globName;
}
else if (paramCount >= 3)
{
// Constructor options here are:
// grpName, globName, flags (3 args)
// grpName, globName, locNameId, flags (4 args)
// grpName, globName, locNameId, flags,
// hlpTopic (5 args)
// grpName, globName, locNameId, flags,
// contextMenuType (5 args)
// grpName, globName, locNameId, flags,
// contextMenuType, hlpFile, helpTpic (7 args)
CustomAttributeTypedArgument arg0, arg1;
arg0 = attb.ConstructorArguments[0];
arg1 = attb.ConstructorArguments[1];
// All options start with grpName, globName
grpName = arg0.Value as string;
globName = arg1.Value as string;
locName = globName;
// If we have a localized command ID,
// let's look it up in our resources
if (paramCount >= 4)
{
// Get the localized string ID
lid = attb.ConstructorArguments[2].ToString();
// Strip off the enclosing quotation marks
if (lid != null && lid.Length > 2)
lid = lid.Substring(1, lid.Length - 2);
// Let's put a try-catch block around this
// Failure just means we use the global
// name twice (the default)
if (lid != null && lid != "")
{
try
{
locName = rm.GetString(lid);
}
catch
{ }
}
}
}
if (globName != null)
{
// Add the information to our data structures
globCmds.Add(globName);
locCmds.Add(locName);
if (grpName != null && !groups.Contains(grpName))
groups.Add(grpName);
}
}
}
}
}
}
// Let's register the application to load on AutoCAD
// startup (2) if specified or if it contains no
// commands. Otherwise we will have it load on
// command invocation (12)
int flags = (!startup && globCmds.Count > 0 ? 12 : 2);
// Now create our Registry keys
return CreateDemandLoadingEntries(
name, path, globCmds, locCmds, groups,
flags, currentUser, force
);
}
public static bool UnregisterForDemandLoading(Assembly assem)
{
// Get the name of the application to unregister
string appName = assem.GetName().Name;
// Unregister it for both HKCU and HKLM
bool res = RemoveDemandLoadingEntries(appName, true);
res &= RemoveDemandLoadingEntries(appName, false);
// If one call failed, we also fail (could change this)
return res;
}
// Helper functions
private static bool CreateDemandLoadingEntries(
string appName,
string path,
List<string> globCmds,
List<string> locCmds,
List<string> groups,
int flags,
bool currentUser,
bool force
)
{
string ackName = GetAutoCADKey();
RegistryKey hive =
(currentUser ? Registry.CurrentUser : Registry.LocalMachine);
// We may need to create the Applications key, as some AutoCAD
// verticals do not contain it under HKCU by default
// CreateSubKey just opens existing keys for write, anyway
RegistryKey appk =
hive.CreateSubKey(ackName + "\\" + "Applications");
using (appk)
{
// Already registered? Just return (unless forcing)
if (!force)
{
string[] subKeys = appk.GetSubKeyNames();
foreach (string subKey in subKeys)
{
if (subKey.Equals(appName))
{
return false;
}
}
}
// Create the our application's root key and its values
RegistryKey rk = appk.CreateSubKey(appName);
using (rk)
{
rk.SetValue(
"DESCRIPTION", appName, 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
);
}
}
}
}
return true;
}
private static bool RemoveDemandLoadingEntries(
string appName, bool currentUser
)
{
try
{
string ackName = GetAutoCADKey();
// Choose a Registry hive based on the function input
RegistryKey hive =
(currentUser ?
Registry.CurrentUser :
Registry.LocalMachine);
// Open the applications key
RegistryKey appk =
hive.OpenSubKey(ackName + "\\" + "Applications", true);
using (appk)
{
// Delete the key with the same name as this assembly
appk.DeleteSubKeyTree(appName);
}
}
catch
{
return false;
}
return true;
}
private static string GetAutoCADKey()
{
// Start by getting the CurrentUser location
RegistryKey hive = Registry.CurrentUser;
// Open the main AutoCAD key
RegistryKey ack =
hive.OpenSubKey(
"Software\\Autodesk\\AutoCAD"
);
using (ack)
{
// Get the current major version and its key
string ver = ack.GetValue("CurVer") as string;
if (ver == null)
{
return "";
}
else
{
RegistryKey verk = ack.OpenSubKey(ver);
using (verk)
{
// Get the vertical/language version and its key
string lng = verk.GetValue("CurVer") as string;
if (lng == null)
{
return "";
}
else
{
RegistryKey lngk = verk.OpenSubKey(lng);
using (lngk)
{
// And finally return the path to the key,
// without the hive prefix
return lngk.Name.Substring(hive.Name.Length + 1);
}
}
}
}
}
}
}
}
The application itself does not depend on AutoCAD, but it does need reflection-only level access to AcMgd.dll and AcDbMgd.dll. So you might place RegDL.exe into the AutoCAD program files folder, or you could copy the AcMgd.dll and AcDbMgd.dll reference assemblies from the ObjectARX SDK (not the full AutoCAD versions: the “liposuctioned” versions are adequate) into the executable’s folder.
Although we recommend placing your .NET assemblies into AutoCAD’s program files folder, the tool will work if the assemblies are located elsewhere (providing RegDL can find AcMgd.dll and AcDbMgd.dll, of course).
If we run the RegDL application without arguments (or with /? or /help), we see the usage instructions for the utility:
The usage pattern should be straightforward enough:
- /unregister to remove – rather than add – the demand-loading Registry keys for the assembly
- /hklm to create the keys per machine, under HKEY_LOCAL_MACHINE (requiring appropriate permissions)
- /startup to specify that the module should be loaded on AutoCAD startup (also the default if no commands are found in the assembly)
- /force to overwrite any existing Registry information (this just writes over – it doesn’t remove any existing commands information, for instance, although that would be trivial to add)
Now let’s use it from a batch file to register the various “Plugins of the Month” for AutoCAD.
Firstly, we can take a look at the relevant section of the Registry, to make sure there’s nothing there:
We can use a batch file (e.g. “reg.bat”) to register our various plugins one after the other:
RegDL "c:\Program Files\Autodesk\AutoCAD 2010\ADNPlugin-ClipboardManager.dll" /force
RegDL "c:\Program Files\Autodesk\AutoCAD 2010\ADNPlugin-FacetCurve.dll" /force
RegDL "c:\Program Files\Autodesk\AutoCAD 2010\ADNPlugin-LayerReporter.dll" /force
RegDL "c:\Program Files\Autodesk\AutoCAD 2010\ADNPlugin-OffsetInXref.dll" /force /startup
RegDL "c:\Program Files\Autodesk\AutoCAD 2010\ADNPlugin-Screenshot.dll" /force
We’ve specified absolute paths, but relative paths work fine, too: the information stored in the Registry is taken from the module’s location, not from what’s passed into RegDL via the command-line. We’ve specified the /force flag, but clearly that’s optional the first time run. OffsetInXref needs to be loaded on startup, as it cannot demand-load off the OFFSET command, which is why we’ve specified that additional flag.
Here’s the output we see from running our batch file:
And in the Registry we should now see our new keys and values:
And to remove our keys, it’s a simple case of executing an equivalent set of calls with the /unregister flag:
RegDL "C:\Program Files\Autodesk\AutoCAD 2010\ADNPlugin-ClipboardManager.dll" /unregister
RegDL "C:\Program Files\Autodesk\AutoCAD 2010\ADNPlugin-FacetCurve.dll" /unregister
RegDL "C:\Program Files\Autodesk\AutoCAD 2010\ADNPlugin-LayerReporter.dll" /unregister
RegDL "C:\Program Files\Autodesk\AutoCAD 2010\ADNPlugin-OffsetInXref.dll" /unregister
RegDL "C:\Program Files\Autodesk\AutoCAD 2010\ADNPlugin-Screenshot.dll" /unregister
I’ve used batch files for simplicity, but clearly the same approach also applies when implementing an installer (you should be able to fire off custom actions for the modules to be registered).
As mentioned earlier, this utility is a first step and shouldn’t be considered a definitive solution: please post a comment if you have suggestions for future directions or have enhancements you’d like to share with other readers of this blog.
Update
Dave just mentioned that certain AutoCAD verticals (AutoCAD Electrical) do not automatically have an “Applications” sub-key under HKCU – presumably any standard demand-loaded components are done so via HKLM. Which means it’s potentially unsafe to simply assume it’ll be there. The good news is that it’s a trivial code change: we just need to use CreateSubKey() rather than OpenSubKey(): this is safe, as CreateSubKey() doesn’t overwrite existing keys, it simply opens them as writeable, which is exactly what we want. I’ve modified the above code and updated the source project.
Something else has just occurred to me… I’ve realised that we don’t need to reflect on an assembly to unregister its types, as we just use its name. I could change the code to just extract the module name from the path string passed in and use that to determine what (if anything) to remove from the Registry, but I’m hesitating. Yes, the module currently needs to exist for it to be unregistered, but perhaps that’s a good thing? Comments welcome.
And one final point (for now, at least :-): I think I’ve tested the various CommandMethodAttribute syntax variations fairly thoroughly, but if you find RegDL doesn’t work properly for your application, please do let me know.
Update 2
I've updated the source and downloadable project to fix the issue reported in this comment.