明经CAD社区

 找回密码
 注册

QQ登录

只需一步,快速开始

搜索
查看: 8833|回复: 9

[Kean专集] Kean专题(6)—Runtime

   关闭 [复制链接]
发表于 2009-5-21 20:09:00 | 显示全部楼层 |阅读模式
本帖最后由 作者 于 2009-5-22 21:45:11 编辑

原帖:http://through-the-interface.typepad.com/through_the_interface/runtime/
一、初始化例程
September 06, 2006
Initialization code in your AutoCAD .NET application
It's very common to need to execute some code as your application modules are loaded, and then to clean-up as they get unloaded or as AutoCAD terminates. Managed AutoCAD applications can do this by implementing the Autodesk.AutoCAD.Runtime.IExtensionApplication interface, which require Initialize() and Terminate() methods.
During the Initialize() method, you will typically want to set system variables and perhaps call commands which execute some pre-existing initialization code for your application.
Here's some code showing how to implement this interface using VB.NET:
  1. Imports Autodesk.AutoCAD.Runtime
  2. Imports Autodesk.AutoCAD.ApplicationServices
  3. Imports Autodesk.AutoCAD.EditorInput
  4. Imports System
  5. Public Class InitializationTest
  6.   Implements Autodesk.AutoCAD.Runtime.IExtensionApplication
  7.   Public Sub Initialize() Implements _
  8.   IExtensionApplication.Initialize
  9.     Dim ed As Editor = _
  10.       Application.DocumentManager.MdiActiveDocument.Editor
  11.     ed.WriteMessage("Initializing - do something useful.")
  12.   End Sub
  13.   Public Sub Terminate() Implements _
  14.   IExtensionApplication.Terminate
  15.     Console.WriteLine("Cleaning up...")
  16.   End Sub
  17.   <CommandMethod("TST")> _
  18.   Public Sub Test()
  19.     Dim ed As Editor = _
  20.       Application.DocumentManager.MdiActiveDocument.Editor
  21.     ed.WriteMessage("This is the TST command.")
  22.   End Sub
  23. End Class
And here's the equivalent code in C#:
  1. using Autodesk.AutoCAD.Runtime;
  2. using Autodesk.AutoCAD.ApplicationServices;
  3. using Autodesk.AutoCAD.EditorInput;
  4. using System;
  5. public class InitializationTest
  6.   : Autodesk.AutoCAD.Runtime.IExtensionApplication
  7. {
  8.   public void Initialize()
  9.   {
  10.     Editor ed =
  11.       Application.DocumentManager.MdiActiveDocument.Editor;
  12.     ed.WriteMessage("Initializing - do something useful.");
  13.   }
  14.   public void Terminate()
  15.   {
  16.     Console.WriteLine("Cleaning up...");
  17.   }
  18.   [CommandMethod("TST")]
  19.   public void Test()
  20.   {
  21.     Editor ed =
  22.       Application.DocumentManager.MdiActiveDocument.Editor;
  23.     ed.WriteMessage("This is the TST command.");
  24.   }
  25. }
A few notes about this code:
.NET modules are not currently unloaded until AutoCAD terminates. While this is a popular request from developers (as it would make debugging much simpler), my understanding is that this is an issue that is inherent to the implementation of .NET - see this MSDN blog post for more information.
What this means is that by the time the Terminate() method is called, AutoCAD is already in the process of closing. This is why I've used Console.Write() rather than ed.WriteMessage(), as by this point there's no command-line to write to.
That said, you can and should use the Terminate() callback to close any open files, database connections etc.
Something else you might come across when implementing this... I've implemented a single command in this application for a couple of reasons: in my next post I'm going to segregate the command into a different class, to show how you can tweak your application architecture both to follow a more logical structure and to optimize load performance.
The second reason I added the command was to raise a subtle you might well hit while coding: you might see the initialization string sent to the command-line as the application loads, but then the command is not found when you enter "TST" at the command-line. If you experience this behaviour, you're probably hitting an issue that can come up when coding managed applications against AutoCAD 2007: there's been a change in this version so that acmgd.dll is loaded on startup, and under certain circumstances this assembly might end up getting loaded again if found on a different path, causing your commands not to work.
The issue can be tricky to identify but is one that can be resolved in a number of ways:
Edit the Registry to disable the demand-loading "load on startup" of acmgd.dll (a bad idea, in my opinion - it's safer not to second guess what assumptions might have been made about the availability of core modules)
Make sure AutoCAD is launched from its own working directory - commonly this issue is hit while debugging because Visual Studio doesn't automatically pick up the debugging application's working directory
Set the "Copy Local" flag for acmgd.dll to "False". "Copy Local" tells Visual Studio whether the build process should make copies of the assemblies referenced by the project in its output folder
My preference is for the third approach, as on a number of occasions I've overwritten acmgd.dll and acdbmgd.dll with those from another AutoCAD version (inadvertently trashing my AutoCAD installation). This usually happens when testing projects across versions (projects that's I've set to output directly to AutoCAD's program folder, for convenience), and I've forgotten to change the assemblies references before building.
So I would actually set both your references to acdbmgd.dll and acmgd.dll to "Copy Local = False". You can do this either by selecting the Reference(s) via the Solution Explorer (in C#) or via the References tab in your Project Properties (in VB.NET) and then editing the reference's Properties.

 楼主| 发表于 2009-5-21 20:16:00 | 显示全部楼层
二、优化.Net模块的载入
September 08, 2006
Optimizing the loading of AutoCAD .NET applications
In my previous post I described how you could use the Autodesk.AutoCAD.Runtime.IExtensionApplication interface to implement initialization code in your .NET module. Building on this, we're now going to look at how use of the Autodesk.AutoCAD.Runtime.IExtensionApplication interface can also allow you - with very little effort - to optimize the architecture of your managed modules for faster loading into AutoCAD.
First some information from the "Using .NET for AutoCAD documentation" (which is available in the ObjectARX Developer's Guide on the ObjectARX SDK):
When AutoCAD loads a managed application, it queries the application's assembly for an ExtensionApplication custom attribute. If this attribute is found, AutoCAD sets the attribute's associated type as the application's entry point. If no such attribute is found, AutoCAD searches all exported types for an IExtensionApplication implementation. If no implementation is found, AutoCAD simply skips the application-specific initialization step.
...
In addition to searching for an IExtensionApplication implementation, AutoCAD queries the application's assembly for one or more CommandClass attributes. If instances of this attribute are found, AutoCAD searches only their associated types for command methods. Otherwise, it searches all exported types.
The samples that I've shown in this blog - and most of those on the ObjectARX SDK - do not show how you can use the ExtensionApplication or CommandClass attribute in your code, as it's not essential to implement them for your application to work. But if you have a large .NET module to be loaded into AutoCAD, it might take some time for AutoCAD to check the various objects in the assembly, to find out which is the ExtensionApplication and which are the various CommandClasses.
The attributes you need to implement are very straightforward:
C#:
  1. [assembly: ExtensionApplication(typeof(InitClass))]
  2. [assembly: CommandClass(typeof(CmdClass))]
复制代码
VB.NET:
  1. <Assembly: ExtensionApplication(GetType(InitClass))>
  2. <Assembly: CommandClass(GetType(CmdClass))>
复制代码
These assembly-level attributes simply tell AutoCAD where to look for the various objects it will otherwise need to identify by searching. Here's some more information from the documentation on the use of these attributes:
The ExtensionApplication attribute can be attached to only one type. The type to which it is attached must implement the IExtensionApplication interface.
...
A CommandClass attribute may be declared for any type that defines AutoCAD command handlers. If an application uses the CommandClass attribute, it must declare an instance of this attribute for every type that contains an AutoCAD command handler method.
While optimizing yesterday's code to reduce load-time, I also changed the structure slightly to be more logical. The above attributes also take classes within a namespace, so I decided to split the initialization code (the "Initialization" class) away from the command implementations (the "Commands" class), but keeping them both in the same ("ManagedApplication") namespace.
And here's the code...
C#:
  1. using Autodesk.AutoCAD.Runtime;
  2. using Autodesk.AutoCAD.ApplicationServices;
  3. using Autodesk.AutoCAD.EditorInput;
  4. using System;
  5. [assembly:
  6.   ExtensionApplication(
  7.     typeof(ManagedApplication.Initialization)
  8.   )
  9. ]
  10. [assembly:
  11.   CommandClass(
  12.     typeof(ManagedApplication.Commands)
  13.   )
  14. ]
  15. namespace ManagedApplication
  16. {
  17.   public class Initialization
  18.     : Autodesk.AutoCAD.Runtime.IExtensionApplication
  19.   {
  20.     public void Initialize()
  21.     {
  22.       Editor ed =
  23.         Application.DocumentManager.MdiActiveDocument.Editor;
  24.       ed.WriteMessage("Initializing - do something useful.");
  25.     }
  26.     public void Terminate()
  27.     {
  28.       Console.WriteLine("Cleaning up...");
  29.     }
  30.   }
  31.   public class Commands
  32.   {
  33.     [CommandMethod("TST")]
  34.     public void Test()
  35.     {
  36.       Editor ed =
  37.         Application.DocumentManager.MdiActiveDocument.Editor;
  38.       ed.WriteMessage("This is the TST command.");
  39.     }
  40.   }
  41. }
VB.NET:
  1. Imports Autodesk.AutoCAD.Runtime
  2. Imports Autodesk.AutoCAD.ApplicationServices
  3. Imports Autodesk.AutoCAD.EditorInput
  4. Imports System
  5. <Assembly: _
  6.   ExtensionApplication( _
  7.     GetType(ManagedApplication.Initialization))>
  8. <Assembly: _
  9.   CommandClass( _
  10.     GetType(ManagedApplication.Commands))>
  11. Namespace ManagedApplication
  12.   Public Class Initialization
  13.     Implements Autodesk.AutoCAD.Runtime.IExtensionApplication
  14.     Public Sub Initialize() Implements _
  15.     IExtensionApplication.Initialize
  16.       Dim ed As Editor = _
  17.         Application.DocumentManager.MdiActiveDocument.Editor
  18.       ed.WriteMessage("Initializing - do something useful.")
  19.     End Sub
  20.     Public Sub Terminate() Implements _
  21.     IExtensionApplication.Terminate
  22.       Console.WriteLine("Cleaning up...")
  23.     End Sub
  24.   End Class
  25.   Public Class Commands
  26.     <CommandMethod("TST")> _
  27.     Public Sub Test()
  28.       Dim ed As Editor = _
  29.         Application.DocumentManager.MdiActiveDocument.Editor
  30.       ed.WriteMessage("This is the TST command.")
  31.     End Sub
  32.   End Class
  33. End Namespace
 楼主| 发表于 2009-5-21 20:19:00 | 显示全部楼层
三、自动加载
September 11, 2006
Automatic loading of .NET modules
Clearly it’s far from ideal to ask your users to load your application modules manually into AutoCAD whenever they need them, so over the years a variety of mechanisms have been put in place to enable automatic loading of applications – acad.lsp, acad.ads, acad.rx, the Startup Suite, to name but a few.
The most elegant way to auto-load both ObjectARX and .NET applications is the demand-loading mechanism. This mechanism is based on information stored in the Registry describing the conditions under which modules should be loaded and how to load them.
Demand loading is fairly straightforward and well documented in the ObjectARX Developer’s Guide (look under “demand loading applications”).
Essentially the information can be stored in one of two places: under HKEY_LOCAL_MACHINE or under HKEY_CURRENT_USER. The decision on where to place the information will depend on a few things – mainly whether the application is to be shared across all users but also whether your application installer has the privileges to write to HKEY_LOCAL_MACHINE or not.
It’s not really the place to talk about the pros and cons of these two locations – for the sake of simplicity the following examples show writing the information to HKEY_CURRENT_USER. Let’s start by looking at the root location for the demand-loading information:
HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R17.0\ACAD-5001:409\Applications
Most of this location is logical enough (to humans), although the “ACAD-5001:409” piece needs a bit of explanation. This number has evolved over the releases, but right now 5001 means AutoCAD 2007 (it was 4001 for AutoCAD 2006, 301 for AutoCAD 2005 and 201 for AutoCAD 2004), and 409 is the “locale” corresponding to English.
A more complete description of the meaning of this key is available to ADN members at:
Registry values for ProductID and LocaleID for AutoCAD and the vertical products
There are two common times to load a .NET application: on AutoCAD startup and on command invocation. ObjectARX applications might also be loaded on object detection, but as described in a previous post this is not something that is currently available to .NET applications.
Let’s take the two common scenarios and show typical settings for the test application shown in this previous post.
Loading modules on AutoCAD startup
Under the root key (HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R17.0\ACAD-5001:409\Applications) there is some basic information needed for our application. Firstly, you need to create a key just for our application: in this case I’ve used “MyTestApp” (as a rule it is recommended that professional software vendors prefix this string with their Registered Developer Symbol (RDS), which can be logged here, but for in-house development this is not necessary – just avoid beginning the key with “Acad” :-).
Under our application key (HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R17.0\ACAD-5001:409\Applications\MyTestApp), we then create a number of values:
  1. DESCRIPTION    A string value describing the purpose of the module
  2. LOADCTRLS      A DWORD (numeric) value describing the reasons for loading the app
  3. MANAGED         Another DWORD that should be set to "1" for .NET modules
  4. LOADER            A string value containing the path to the module
复制代码
The interesting piece is the LOADCTRLS value – the way to encode this is described in detail in the ObjectARX Developer’s Guide, but to save time I’ll cut to the chase: this needs to have a value of "2" for AutoCAD to load the module on startup.
Here's a sample .reg file:
  1. Windows Registry Editor Version 5.00
  2. [HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R17.0\ACAD-5001:409\Applications\MyTestApplication]
  3. "DESCRIPTION"="Kean's test application"
  4. "LOADCTRLS"=dword:00000002
  5. "MANAGED"=dword:00000001
  6. "LOADER"="C:\\My Path\\MyTestApp.dll"
复制代码

After merging it into the Registry, here's what happens when you launch AutoCAD:
Regenerating model.
  1. Initializing - do something useful.
  2. AutoCAD menu utilities loaded.
  3. Command: tst
  4. This is the TST command.
  5. Command:
复制代码
Loading modules on command invocation
To do this we need to add a little more information into the mix.
Firstly we need to change the value of LOADCTRLS to "12" (or "c" in hexadecimal), which is actually a combination of 4 (which means "on command invocation") and 8 (which means "on load request"). For people that want to know the other flags that can be used, check out rxdlinkr.h in the inc folder of the ObjectARX SDK.
Secondly we need to add a couple more keys, to contain information about our commands and command-groups.
Beneath a "Commands" key (HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R17.0\ACAD-5001:409\Applications\MyTestApp\Commands), we'll create as many string values as we have commands, each with the name of the "global" command name, and the value of the "local" command name. As well as the "TST" command, I've added one more called "ANOTHER".
Beneath a "Groups" key (HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R17.0\ACAD-5001:409\Applications\MyTestApp\Groups), we'll do the same for the command-groups we've registered our commands under (I used the default CommandMethod attribute that doesn't mention a group name, so this is somewhat irrelevant for our needs - I'll use "ASDK_CMDS" as an example, though).
Here's the the updated .reg file:
  1. Windows Registry Editor Version 5.00
  2. [HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R17.0\ACAD-5001:409\Applications\MyTestApplication]
  3. "DESCRIPTION"="Kean's test application"
  4. "LOADCTRLS"=dword:0000000c
  5. "MANAGED"=dword:00000001
  6. "LOADER"="C:\\My Path\\MyTestApp.dll"
  7. [HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R17.0\ACAD-5001:409\Applications\MyTestApp\Commands]
  8. "TST"="TST"
  9. "ANOTHER"="ANOTHER"
  10. [HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R17.0\ACAD-5001:409\Applications\MyTestApp\Groups]
  11. "ASDK_CMDS"="ASDK_CMDS"
复制代码
And here's what happens when we launch AutoCAD this time, and run the tst command. You see the module is only loaded once the command is invoked:
  1. Regenerating model.
  2. AutoCAD menu utilities loaded.
  3. Command: tst
  4. Initializing - do something useful.This is the TST command.
  5. Command:
复制代码
 楼主| 发表于 2009-5-21 20:29:00 | 显示全部楼层
四、程序集的路径
November 22, 2006
Finding the location of a .NET module
A quick pre-Thanksgiving tip that came from an internal discussion today: how to find the location of a .NET module (meaning the currently executing assembly).
Two techniques were identified:
Identify the current assembly by asking where one of its types is defined
Use the Assembly.GetExecutingAssembly() to get the assembly from where the current code is executing
Here's the C# code showing the two techniques:
  1. using System;
  2. using System.Reflection;
  3. using Autodesk.AutoCAD.ApplicationServices;
  4. using Autodesk.AutoCAD.EditorInput;
  5. using Autodesk.AutoCAD.Runtime;
  6. namespace AssemblyLocationTest
  7. {
  8.   public class AssemblyCmds
  9.   {
  10.     [CommandMethod("LOC")]
  11.     public void GetModuleLocation()
  12.     {
  13.       Editor ed =
  14.         Application.DocumentManager.MdiActiveDocument.Editor;
  15.       // First technique
  16.       Type type = typeof(AssemblyCmds);
  17.       Assembly asm1 = type.Assembly;
  18.       ed.WriteMessage(
  19.         "\nAssembly location (1) is: "
  20.         + asm1.Location
  21.       );
  22.       // Second technique
  23.       Assembly asm2 = Assembly.GetExecutingAssembly();
  24.       ed.WriteMessage(
  25.         "\nAssembly location (2) is: "
  26.         + asm2.Location
  27.       );
  28.     }
  29.   }
  30. }
And here are the results, just to show they return the same thing:
  1. Command: loc
  2. Assembly location (1) is: C:\temp\MyAssembly.dll
  3. Assembly location (2) is: C:\temp\MyAssembly.dll
复制代码
 楼主| 发表于 2009-5-21 20:34:00 | 显示全部楼层
五、在注册表保存设置
May 05, 2008
Storing custom AutoCAD application settings in the Registry using .NET
Thanks to Sreekar Devatha, Gopinath Taget & Jeremy Tammik (from DevTech India, Americas and Europe, respectively) for contributing to my knowledge in this area over the last few months (whether they knew they were doing so, or not :-).
This post shows how to make use of a handy interface inside AutoCAD to place custom settings in the Registry and how to then read them back. The code is very simple: you simply open up the current profile and then access/modify your hierarchy of setting beneath it. I've used a Registered Developer Symbol (RDS) to prefix the section of the Registry directly beneath the profile, to avoid conflicts with other applications.
There are other ways of saving more complex settings to the Registry: in a future post I'll go more in-depth with the System.Configuration namespace (especially how to implement your own System.Configuration.ConfigurationSettings class and save it to the Registry).
Here's the C# code:
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.Runtime;
  3. using Autodesk.AutoCAD.EditorInput;
  4. namespace ApplicationSettings
  5. {
  6.   public class Commands
  7.   {
  8.     // We're using our Registered Developer Symbol (RDS)
  9.     //  TTIF == Through the Interface
  10.     // for the section name.
  11.     // The entries beneath don't need this.
  12.     const string sectionName = "TTIFSettings";
  13.     const string intProperty = "TestInteger";
  14.     const string doubleProperty = "TestDouble";
  15.     const string stringProperty = "TestString";
  16.     [CommandMethod("ATR")]
  17.     static public void AddToRegistry()
  18.     {
  19.       IConfigurationSection con =
  20.         Application.UserConfigurationManager.OpenCurrentProfile();
  21.       using (con)
  22.       {
  23.         IConfigurationSection sec =
  24.           con.CreateSubsection(sectionName);
  25.         using (sec)
  26.         {
  27.           sec.WriteProperty(intProperty, 1);
  28.           sec.WriteProperty(doubleProperty, 2.0);
  29.           sec.WriteProperty(stringProperty, "Hello");
  30.         }
  31.       }
  32.     }
  33.     [CommandMethod("RFR")]
  34.     static public void RetrieveFromRegistry()
  35.     {
  36.       Document doc =
  37.         Application.DocumentManager.MdiActiveDocument;
  38.       Editor ed = doc.Editor;
  39.       IConfigurationSection prf =
  40.         Application.UserConfigurationManager.OpenCurrentProfile();
  41.       using (prf)
  42.       {
  43.         if (prf.ContainsSubsection(sectionName))
  44.         {
  45.           IConfigurationSection sec =
  46.             prf.OpenSubsection(sectionName);
  47.           using (sec)
  48.           {
  49.             double doubleValue =
  50.               (double)sec.ReadProperty(doubleProperty, 0.0);
  51.             string stringValue =
  52.               (string)sec.ReadProperty(stringProperty, "");
  53.             int intValue =
  54.               (int)sec.ReadProperty(intProperty, 0);
  55.             object defValue =
  56.               sec.ReadProperty("NotThere", 3.142);
  57.             ed.WriteMessage("\nInt value: " + intValue);
  58.             ed.WriteMessage("\nDouble value: " + doubleValue);
  59.             ed.WriteMessage("\nString value: " + stringValue);
  60.             ed.WriteMessage("\nNon-existent value: " + defValue);
  61.           }
  62.         }
  63.       }
  64.     }
  65.   }
  66. }
Here's what we see when we run the RFR command (after having run the ATR beforehand, whether in the same session or a previous one):
  1. Command: RFR
  2. Int value: 1
  3. Double value: 2
  4. String value: Hello
  5. Non-existent value: 3.142
复制代码
And here are the contents of our new section of the Registry:

The observant among you will notice I've switched across to Vista (having just received a new machine). So far I've actually enjoyed using it, having disabled UAC within the first few minutes of getting it. :-)

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

x
 楼主| 发表于 2009-5-21 20:51:00 | 显示全部楼层

六、程序集无法卸载的挽救方法

September 29, 2008
Tired of not being able to NETUNLOAD from AutoCAD? "Edit and Continue" to the rescue!
A question came in on a previous post:

Hello, I write applications for Autocad Map and Civil3d platforms, mostly with ObjectARX. I would like to do more with .NET but so far the main reason preventing this is not having the NETUNLOAD command.. With arx I can just arxunload and arxload the application for modifications in a second. But with .NET I have to restart the heavy environment and do all kinds of lengthy initializations before being able to try even small changes in code, this can take a minute or more.. Maybe it is possible to create an utility, for development purposes, to unload a .net assembly from Autocad ?

NETUNLOAD has been requested many times by developers, but unfortunately would require a significant change for the .NET API - one it's unclear there'd be significant benefit in making. The root of the situation is that .NET assemblies cannot be unloaded from an AppDomain.

To implement a NETUNLOAD command, we would have to host each assembly in a separate AppDomain and then destroy the AppDomain to unload it. It's altogether possible to implement your own bootstrapper assembly that does just this: I'm going to give that a try, to see how it works, but in the meantime I thought I'd point out (or remind you of) a capability that to greatly reduces the need to continually unload modules from AutoCAD: Visual Studio's Edit and Continue.

Edit and Continue has been around since VB6, although you may not have looked at it in recent versions of Visual Studio. I personally didn't start finding it usable again until Visual Studio 2005 (and its Express Editions). If you'd previously turned your back on it, I suggest taking another look.

To enable Edit and Continue, start by editing the Debugging options (accessed via Tools -> Options in the Visual Studio editor):

If you attempt to edit the code before the module has been loaded, you'll get this error:

And if you try to edit the code before a breakpoint has hit, you'll get this altogether more obscure error:

This message wasn't very helpful - at least not to me. My code wasn't running, as such (although I agree that someone's code was :-) and the setting mentioned was set correctly. Anyway - breaking into your code allows you to then edit the code, which should be obvious by a message on the application status-bar that tells you when the "continue" piece is successful:

I'd be curious to hear about others opinions of Edit and Continue: does it meet your debugging needs, or do you still see NETUNLOAD as important functionality? (There are clearly still areas - unrelated to debugging - where it might be useful to unload .NET assemblies. To be clear, I'd be happy to receive input on those areas, also.)

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

x
 楼主| 发表于 2009-5-21 20:56:00 | 显示全部楼层
本帖最后由 作者 于 2009-5-26 16:06:58 编辑

七、实现Com接口
May 20, 2009
Interfacing an external COM application with a .NET module in-process to AutoCAD
This question came in recently by email from Michael Fichter of Superstructures Engineers and Architects:
Could you suggest an approach that would enable me to drive a .NET function (via COM) that could return a value from .NET back to COM? I have used SendCommand in certain instances where return values were not needed.
Michael’s referring to a technique used in this previous post, which shows how to launch AutoCAD from a .NET executable via COM and then launch a command which can then safely interface with AutoCAD in-process via its managed API.
And yes, this technique is fine if you don’t want to return results, but has limitations if you do. You could populate AutoCAD user variables or create a file for the calling application to read but such approaches are cumbersome.
So… in spite of my initial doubtful reaction I decided to give it a try. Here are the steps I used to get this working…
First we create a Class Library for our in-process component with references to the usual acmgd.dll and acdbmgd.dll assemblies, adding the following C# code:
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.EditorInput;
  3. using Autodesk.AutoCAD.DatabaseServices;
  4. using Autodesk.AutoCAD.Runtime;
  5. using Autodesk.AutoCAD.Geometry;
  6. using System.Runtime.InteropServices;
  7. namespace LoadableComponent
  8. {
  9.   [ProgId("LoadableComponent.Commands")]
  10.   public class Commands
  11.   {
  12.     // A simple test command, just to see that commands
  13.     // are loaded properly from the assembly
  14.     [CommandMethod("MYCOMMAND")]
  15.     public void MyCommand()
  16.     {
  17.       Document doc =
  18.         Application.DocumentManager.MdiActiveDocument;
  19.       Editor ed = doc.Editor;
  20.       ed.WriteMessage("\nTest command executed.");
  21.     }
  22.     // A function to add two numbers and create a
  23.     // circle of that radius. It returns a string
  24.     // withthe result of the addition, just to use
  25.     // a different return type
  26.     public string AddNumbers(int arg1, double arg2)
  27.     {
  28.       // During tests it proved unreliable to rely
  29.       // on DocumentManager.MdiActiveDocument
  30.       // (which was null) so we will go from the
  31.       // HostApplicationServices' WorkingDatabase
  32.       Database db =
  33.         HostApplicationServices.WorkingDatabase;
  34.       Document doc =
  35.         Application.DocumentManager.GetDocument(db);
  36.       // Perform our addition
  37.       double res = arg1 + arg2;
  38.       // Lock the document before we access it
  39.       DocumentLock loc = doc.LockDocument();
  40.       using (loc)
  41.       {
  42.         Transaction tr =
  43.           db.TransactionManager.StartTransaction();
  44.         using (tr)
  45.         {
  46.           // Create our circle
  47.           Circle cir =
  48.             new Circle(
  49.               new Point3d(0, 0, 0),
  50.               new Vector3d(0, 0, 1),
  51.               res
  52.             );
  53.           cir.SetDatabaseDefaults(db);
  54.           // Add it to the current space
  55.           BlockTableRecord btr =
  56.             (BlockTableRecord)tr.GetObject(
  57.               db.CurrentSpaceId,
  58.               OpenMode.ForWrite
  59.             );
  60.           btr.AppendEntity(cir);
  61.           tr.AddNewlyCreatedDBObject(cir, true);
  62.           // Commit the transaction
  63.           tr.Commit();
  64.         }
  65.       }
  66.       // Return our string result
  67.       return res.ToString();
  68.     }
  69.   }
  70. }
You’ll see we mark our Commands class as having the ProgId of “LoadableComponent.Commands” (this doesn’t have to follow the namespace.class-name convention, if you’d rather use something else). Be sure to edit the AssemblyInfo.cs file to make sure the ComVisible assembly attribute is set to true (the default is false), otherwise no classes will be exposed via COM.
The code is mostly pretty simple… it includes a command just to make sure commands are registered when the assembly loads. The AddNumbers() function uses a slightly different technique to get the working database and its document, mainly because I found MdiActiveDocument to be null when I needed it. I suspect this is simply a timing issue, and that if AutoCAD had the time to fully initialize we wouldn’t have to code this defensively. There may well be a clean way to wait for this to happen (comments, anyone?).
Once we’ve built the assembly it needs to be registered via COM. The way I tend to do this is via a “Visual Studio Command Prompt” (which has the path set nicely to call the VS development tools). I browse to the location of my assembly and then run “regasm LoadableComponent.dll” (you can specify the optional /reg parameter if you’d rather create a .reg file rather than modifying the Registry directly).
Now we can create an executable project to drive this component with COM references to the AutoCAD Type Library (I’m using the one for AutoCAD 2010) and the AutoCAD/ObjectDBX Common Type Library (AutoCAD 2010’s is version 18.0), as well as a reference to our .NET assembly (which I have called LoadableComponent.dll).
Inside the default form created with the executable project we can add a button behind which we copy the code in the post referred to earlier, adding some logic to load our component and dynamically execute its AddNumbers() function:
  1. using Autodesk.AutoCAD.Interop;
  2. using System.Windows.Forms;
  3. using System.Runtime.InteropServices;
  4. using System.Reflection;
  5. using System;
  6. using LoadableComponent;
  7. namespace DrivingAutoCAD
  8. {
  9.   public partial class Form1 : Form
  10.   {
  11.     public Form1()
  12.     {
  13.       InitializeComponent();
  14.     }
  15.     private void button1_Click(object sender, EventArgs e)
  16.     {
  17.       const string progID = "AutoCAD.Application.18";
  18.       AcadApplication acApp = null;
  19.       try
  20.       {
  21.         acApp =
  22.           (AcadApplication)Marshal.GetActiveObject(progID);
  23.       }
  24.       catch
  25.       {
  26.         try
  27.         {
  28.           Type acType =
  29.             Type.GetTypeFromProgID(progID);
  30.           acApp =
  31.             (AcadApplication)Activator.CreateInstance(
  32.               acType,
  33.               true
  34.             );
  35.         }
  36.         catch
  37.         {
  38.           MessageBox.Show(
  39.             "Cannot create object of type "" +
  40.             progID + """
  41.           );
  42.         }
  43.       }
  44.       if (acApp != null)
  45.       {
  46.         try
  47.         {
  48.           // By the time this is reached AutoCAD is fully
  49.           // functional and can be interacted with through code
  50.           acApp.Visible = true;
  51.           object app =
  52.             acApp.GetInterfaceObject("LoadableComponent.Commands");
  53.           if (app != null)
  54.           {
  55.             // Let's generate the arguments to pass in:
  56.             // an integer and a double
  57.             object[] args = { 5, 6.3 };
  58.             // Now let's call our method dynamically
  59.             object res =
  60.               app.GetType().InvokeMember(
  61.                 "AddNumbers",
  62.                 BindingFlags.InvokeMethod,
  63.                 null,
  64.                 app,
  65.                 args
  66.               );
  67.             acApp.ZoomAll();
  68.             MessageBox.Show(
  69.               this,
  70.               "AddNumbers returned: " + res.ToString()
  71.             );
  72.           }
  73.         }
  74.         catch (Exception ex)
  75.         {
  76.           MessageBox.Show(
  77.             this,
  78.             "Problem executing component: " +
  79.             ex.Message
  80.           );
  81.         }
  82.       }
  83.     }
  84.   }
  85. }
I decided to try Application.GetInterfaceObject() – the classic way to load an old VB6 ActiveX DLL into AutoCAD from VBA or Visual LISP – to see whether it worked for .NET assemblies that have a ProgId assigned. It not only worked, but the commands contained within the module were registered properly. A nice surprise! :-)
I started by defining an interface in the Class Library to be used in the Executable, but ended up going with a more dynamic approach, using InvokeMember() on the class of the object returned by GetInterfaceObject(). This avoids having to define and cast to the interface but adds a little uncertainty to the operation (as I’ve mentioned a number of times in recent weeks when we start getting dynamic we lose most of the compiler crutches we’ve all become used to :-). I also hit a problem if the command function was declared as static, but presumably that can be resolved with the right arguments to InvokeMember().
When we run this code and select the button, we see a circle get created with the radius of 11.3, the result of adding the integer (5) and double (6.3) we passed to the AddNumbers() function:


The combination of COM for out-of-process control with .NET for in-process power and performance will hopefully be a useful technique for many of you needing to automate AutoCAD from an external executable. Be sure to post comments if any of you have things to share on this topic. Thanks for the question, Michael! :-)

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

x
 楼主| 发表于 2009-5-26 16:09:00 | 显示全部楼层
May 25, 2009
Interfacing an external COM application with a .NET module in-process to AutoCAD (redux)
Thanks to all of your interest in this recent post, which looked at a way to interface an out-of-process .NET application with an assembly running in-process to AutoCAD. After some obvious functionality gaps were raised, Renze de Waal, one of our ADN members, pointed out a DevNote on the ADN website covering – and more completely addressing – this topic. Shame on me for not checking there before writing the post. Anyway, onwards and upwards…
The information in the DevNote highlights some of the problems I and other people had hit with my previous code, mostly related to the fact it wasn’t executed on the main AutoCAD thread (which meant we were effectively limited in the interactions we had with the AutoCAD application).
To fix this we can derive our application from System.EnterpriseServices.ServicedComponent (also adding an additional project reference to the System.EnterpriseServices .NET assembly). Here is the updated C# code for the LoadableComponent:
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.EditorInput;
  3. using Autodesk.AutoCAD.DatabaseServices;
  4. using Autodesk.AutoCAD.Runtime;
  5. using Autodesk.AutoCAD.Geometry;
  6. using System.Runtime.InteropServices;
  7. using System.EnterpriseServices;
  8. namespace LoadableComponent
  9. {
  10.   [Guid("5B5B731C-B37A-4aa2-8E50-42192BD51B17")]
  11.   public interface INumberAddition
  12.   {
  13.     [DispId(1)]
  14.     string AddNumbers(int arg1, double arg2);
  15.   }
  16.   [ProgId("LoadableComponent.Commands"),
  17.    Guid("44D8782B-3F60-4cae-B14D-FA060E8A4D01"),
  18.    ClassInterface(ClassInterfaceType.None)]
  19.   public class Commands : ServicedComponent, INumberAddition
  20.   {
  21.     // A simple test command, just to see that commands
  22.     // are loaded properly from the assembly
  23.     [CommandMethod("MYCOMMAND")]
  24.     static public void MyCommand()
  25.     {
  26.       Document doc =
  27.         Application.DocumentManager.MdiActiveDocument;
  28.       Editor ed = doc.Editor;
  29.       ed.WriteMessage("\nTest command executed.");
  30.     }
  31.     // A function to add two numbers and create a
  32.     // circle of that radius. It returns a string
  33.     // withthe result of the addition, just to use
  34.     // a different return type
  35.     public string AddNumbers(int arg1, double arg2)
  36.     {      
  37.       // During tests it proved unreliable to rely
  38.       // on DocumentManager.MdiActiveDocument
  39.       // (which was null) so we will go from the
  40.       // HostApplicationServices' WorkingDatabase
  41.       Document doc =
  42.         Application.DocumentManager.MdiActiveDocument;
  43.       Database db = doc.Database;
  44.       Editor ed = doc.Editor;
  45.       ed.WriteMessage(
  46.         "\nAdd numbers called with {0} and {1}.",
  47.         arg1, arg2
  48.       );
  49.       // Perform our addition
  50.       double res = arg1 + arg2;
  51.       // Lock the document before we access it
  52.       DocumentLock loc = doc.LockDocument();
  53.       using (loc)
  54.       {
  55.         Transaction tr =
  56.           db.TransactionManager.StartTransaction();
  57.         using (tr)
  58.         {
  59.           // Create our circle
  60.           Circle cir =
  61.             new Circle(
  62.               new Point3d(0, 0, 0),
  63.               new Vector3d(0, 0, 1),
  64.               res
  65.             );
  66.           cir.SetDatabaseDefaults(db);
  67.           // Add it to the current space
  68.           BlockTableRecord btr =
  69.             (BlockTableRecord)tr.GetObject(
  70.               db.CurrentSpaceId,
  71.               OpenMode.ForWrite
  72.             );
  73.           btr.AppendEntity(cir);
  74.           tr.AddNewlyCreatedDBObject(cir, true);
  75.           // Commit the transaction
  76.           tr.Commit();
  77.         }
  78.       }
  79.       // Return our string result
  80.       return res.ToString();
  81.     }
  82.   }
  83. }
Some points to note...
We now use an interface to expose functionality from our component, which allows us more flexibility in the way we return data to the calling application.
We're labeling our interface and component with specific GUIDs - generated by guidgen.exe - although we could probably skip this step.
We're now able to use the MdiActiveDocument property safely, as well as being able to write messages via the editor.
When we build the component we can - as before - register it via the regasm.exe tool. Here's the .reg output if you specify the /regfile option:
  1. REGEDIT4
  2. [HKEY_CLASSES_ROOT\LoadableComponent.Commands]
  3. @="LoadableComponent.Commands"
  4. [HKEY_CLASSES_ROOT\LoadableComponent.Commands\CLSID]
  5. @="{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}"
  6. [HKEY_CLASSES_ROOT\CLSID\{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}]
  7. @="LoadableComponent.Commands"
  8. [HKEY_CLASSES_ROOT\CLSID\{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}\InprocServer32]
  9. @="mscoree.dll"
  10. "ThreadingModel"="Both"
  11. "Class"="LoadableComponent.Commands"
  12. "Assembly"="LoadableComponent, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
  13. "RuntimeVersion"="v2.0.50727"
  14. [HKEY_CLASSES_ROOT\CLSID\{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}\InprocServer32\1.0.0.0]
  15. "Class"="LoadableComponent.Commands"
  16. "Assembly"="LoadableComponent, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
  17. "RuntimeVersion"="v2.0.50727"
  18. [HKEY_CLASSES_ROOT\CLSID\{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}\ProgId]
  19. @="LoadableComponent.Commands"
  20. [HKEY_CLASSES_ROOT\CLSID\{44D8782B-3F60-4CAE-B14D-FA060E8A4D01}\Implemented Categories\{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}]
复制代码
One thing to mention - I found that the calling application was not able to to cast the returned System.__COMObject to LoadableComponent.INumberAddition unless I updated the project settings to "Register from COM Interop" (near the bottom of the Build tab).
Now for our calling application… here’s the updated C# code:
  1. using Autodesk.AutoCAD.Interop;
  2. using System.Windows.Forms;
  3. using System.Runtime.InteropServices;
  4. using System.Reflection;
  5. using System;
  6. using LoadableComponent;
  7. namespace DrivingAutoCAD
  8. {
  9.   public partial class Form1 : Form
  10.   {
  11.     public Form1()
  12.     {
  13.       InitializeComponent();
  14.     }
  15.     private void button1_Click(object sender, EventArgs e)
  16.     {
  17.       const string progID = "AutoCAD.Application.18";
  18.       AcadApplication acApp = null;
  19.       try
  20.       {
  21.         acApp =
  22.           (AcadApplication)Marshal.GetActiveObject(progID);
  23.       }
  24.       catch
  25.       {
  26.         try
  27.         {
  28.           Type acType =
  29.             Type.GetTypeFromProgID(progID);
  30.           acApp =
  31.             (AcadApplication)Activator.CreateInstance(
  32.               acType,
  33.               true
  34.             );
  35.         }
  36.         catch
  37.         {
  38.           MessageBox.Show(
  39.             "Cannot create object of type "" +
  40.             progID + """
  41.           );
  42.         }
  43.       }
  44.       if (acApp != null)
  45.       {
  46.         try
  47.         {
  48.           // By the time this is reached AutoCAD is fully
  49.           // functional and can be interacted with through code
  50.           acApp.Visible = true;
  51.           INumberAddition app =
  52.             (INumberAddition)acApp.GetInterfaceObject(
  53.               "LoadableComponent.Commands"
  54.             );
  55.           // Now let's call our method
  56.           string res = app.AddNumbers(5, 6.3);
  57.           acApp.ZoomAll();
  58.           MessageBox.Show(
  59.             this,
  60.             "AddNumbers returned: " + res
  61.           );
  62.         }
  63.         catch (Exception ex)
  64.         {
  65.           MessageBox.Show(
  66.             this,
  67.             "Problem executing component: " +
  68.             ex.Message
  69.           );
  70.         }
  71.       }
  72.     }
  73.   }
  74. }
You should be able to see straightaway that it’s simpler – we cast the results of the GetInterfaceObject call to our interface and call the AddNumbers method on it.
And when we execute the code, we can see we’re now able to write to the command-line, as well as getting better results from our ZoomAll():

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

x
 楼主| 发表于 2009-5-29 14:54:00 | 显示全部楼层
May 28, 2009
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&ccedil;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:
  1. using System.Collections.Generic;
  2. using System.Reflection;
  3. using System.Resources;
  4. using System;
  5. using Microsoft.Win32;
  6. using Autodesk.AutoCAD.DatabaseServices;
  7. using Autodesk.AutoCAD.Runtime;
  8. namespace DemandLoading
  9. {
  10.   public class RegistryUpdate
  11.   {
  12.     public static void RegisterForDemandLoading()
  13.     {
  14.       // Get the assembly, its name and location
  15.       Assembly assem = Assembly.GetExecutingAssembly();
  16.       string name = assem.GetName().Name;
  17.       string path = assem.Location;
  18.       // We'll collect information on the commands
  19.       // (we could have used a map or a more complex
  20.       // container for the global and localized names
  21.       // - the assumption is we will have an equal
  22.       // number of each with possibly fewer groups)
  23.       List<string> globCmds = new List<string>();
  24.       List<string> locCmds = new List<string>();
  25.       List<string> groups = new List<string>();
  26.       // Iterate through the modules in the assembly
  27.       Module[] mods = assem.GetModules(true);
  28.       foreach(Module mod in mods)
  29.       {
  30.         // Within each module, iterate through the types
  31.         Type[] types = mod.GetTypes();
  32.         foreach (Type type in types)
  33.         {
  34.           // We may need to get a type's resources
  35.           ResourceManager rm =
  36.             new ResourceManager(type.FullName, assem);
  37.           rm.IgnoreCase = true;
  38.           // Get each method on a type
  39.           MethodInfo[] meths = type.GetMethods();
  40.           foreach (MethodInfo meth in meths)
  41.           {
  42.             // Get the methods custom command attribute(s)
  43.             object[] attbs =
  44.               meth.GetCustomAttributes(
  45.                 typeof(CommandMethodAttribute),
  46.                 true
  47.               );
  48.             foreach (object attb in attbs)
  49.             {
  50.               CommandMethodAttribute cma =
  51.                 attb as CommandMethodAttribute;
  52.               if (cma != null)
  53.               {
  54.                 // And we can finally harvest the information
  55.                 // about each command
  56.                 string globName = cma.GlobalName;
  57.                 string locName = globName;
  58.                 string lid = cma.LocalizedNameId;
  59.                 // If we have a localized command ID,
  60.                 // let's look it up in our resources
  61.                 if (lid != null)
  62.                 {
  63.                   // Let's put a try-catch block around this
  64.                   // Failure just means we use the global
  65.                   // name twice (the default)
  66.                   try
  67.                   {
  68.                     locName = rm.GetString(lid);
  69.                   }
  70.                   catch
  71.                   {}
  72.                 }
  73.                 // Add the information to our data structures
  74.                 globCmds.Add(globName);
  75.                 locCmds.Add(locName);
  76.                 if (cma.GroupName != null &&
  77.                     !groups.Contains(cma.GroupName))
  78.                   groups.Add(cma.GroupName);
  79.               }
  80.             }
  81.           }
  82.         }
  83.       }
  84.       // Let's register the application to load on demand (12)
  85.       // if it contains commands, otherwise we will have it
  86.       // load on AutoCAD startup (2)
  87.       int flags = (globCmds.Count > 0 ? 12 : 2);
  88.       // By default let's create the commands in HKCU
  89.       // (pass false if we want to create in HKLM)
  90.       CreateDemandLoadingEntries(
  91.         name, path, globCmds, locCmds, groups, flags, true
  92.       );
  93.     }
  94.     public static void UnregisterForDemandLoading()
  95.     {
  96.       RemoveDemandLoadingEntries(true);
  97.     }
  98.     // Helper functions
  99.     private static void CreateDemandLoadingEntries(
  100.       string name,
  101.       string path,
  102.       List<string> globCmds,
  103.       List<string> locCmds,
  104.       List<string> groups,
  105.       int flags,
  106.       bool currentUser
  107.     )
  108.     {
  109.       // Choose a Registry hive based on the function input
  110.       RegistryKey hive =
  111.         (currentUser ? Registry.CurrentUser : Registry.LocalMachine);
  112.       // Open the main AutoCAD (or vertical) and "Applications" keys
  113.       RegistryKey ack =
  114.         hive.OpenSubKey(
  115.           HostApplicationServices.Current.RegistryProductRootKey
  116.         );
  117.       RegistryKey appk =
  118.         ack.OpenSubKey("Applications", true);
  119.       // Already registered? Just return
  120.       string[] subKeys = appk.GetSubKeyNames();
  121.       foreach (string subKey in subKeys)
  122.       {
  123.         if (subKey.Equals(name))
  124.         {
  125.           appk.Close();
  126.           return;
  127.         }
  128.       }
  129.       // Create the our application's root key and its values
  130.       RegistryKey rk =
  131.         appk.CreateSubKey(name);
  132.       rk.SetValue("DESCRIPTION", name, RegistryValueKind.String);
  133.       rk.SetValue("LOADCTRLS", flags, RegistryValueKind.DWord);
  134.       rk.SetValue("LOADER", path, RegistryValueKind.String);
  135.       rk.SetValue("MANAGED", 1, RegistryValueKind.DWord);
  136.       // Create a subkey if there are any commands...
  137.       if ((globCmds.Count == locCmds.Count) &&
  138.           globCmds.Count > 0)
  139.       {
  140.         RegistryKey ck =
  141.           rk.CreateSubKey("Commands");
  142.         for (int i=0; i < globCmds.Count; i++)
  143.           ck.SetValue(
  144.             globCmds[i],
  145.             locCmds[i],
  146.             RegistryValueKind.String
  147.           );
  148.       }
  149.       // And the command groups, if there are any
  150.       if (groups.Count > 0)
  151.       {
  152.         RegistryKey gk =
  153.           rk.CreateSubKey("Groups");
  154.         foreach (string grpName in groups)
  155.           gk.SetValue(grpName, grpName, RegistryValueKind.String);
  156.       }
  157.       appk.Close();
  158.     }
  159.     private static void RemoveDemandLoadingEntries(bool currentUser)
  160.     {
  161.       // Choose a Registry hive based on the function input
  162.       RegistryKey hive =
  163.         (currentUser ? Registry.CurrentUser : Registry.LocalMachine);
  164.       // Open the main AutoCAD (or vertical) and "Applications" keys
  165.       RegistryKey ack =
  166.         hive.OpenSubKey(
  167.           HostApplicationServices.Current.RegistryProductRootKey
  168.         );
  169.       RegistryKey appk =
  170.         ack.OpenSubKey("Applications", true);
  171.       // Delete the key with the same name as this assembly
  172.       appk.DeleteSubKeyTree(
  173.         Assembly.GetExecutingAssembly().GetName().Name
  174.       );
  175.       appk.Close();
  176.     }
  177.   }
  178. }
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:
  1. Windows Registry Editor Version 5.00
  2. [HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R18.0\ACAD-8001:409\Applications\OffsetInXref]
  3. "DESCRIPTION"="OffsetInXref"
  4. "LOADCTRLS"=dword:0000000c
  5. "LOADER"="C:\\Program Files\\Autodesk\\AutoCAD 2010\\OffsetInXref.dll"
  6. "MANAGED"=dword:00000001
  7. [HKEY_CURRENT_USER\Software\Autodesk\AutoCAD\R18.0\ACAD-8001:409\Applications\OffsetInXref\Commands]
  8. "XOFFSETLAYER"="XOFFSETLAYER"
  9. "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).

 楼主| 发表于 2009-6-12 08:31:00 | 显示全部楼层
使用本地化的名字注册命令(多语言支持)
June 08, 2009
Registering AutoCAD commands with localized names using .NET
While I was preparing this recent post I put together a simple project which registered localized names for commands, to make sure they were picked up and used to create the appropriate demand-loading Registry keys. It was a little tricky to do this from the current documentation, so thankfully I had access to this DevNote on the ADN site which helped a great deal (this is only available to ADN members but don’t worry if you’re not one: this post should provide equivalent information and in certain ways goes beyond the original example).
Anyway, it seemed a relevant topic to cover in its own post, so here we are today looking at this question: how to register localized command-names – possibly for multiple target languages – for your AutoCAD commands.
First of all we need to register some commands, the code for which proves to be pretty straightforward. Here’s the C# code defining our commands, which we will save in a file named Commands.cs:
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.Runtime;
  3. namespace LocalizedCommands
  4. {
  5.   public class LocCmds
  6.   {
  7.     [CommandMethod(
  8.       "LOCCMDS", "HELLO", "helloCmdId", CommandFlags.Modal
  9.     )]
  10.     public void HelloCommand()
  11.     {
  12.       Application.DocumentManager.MdiActiveDocument.Editor.
  13.         WriteMessage("\nHello!");
  14.     }
  15.     [CommandMethod(
  16.       "LOCCMDS", "GOODBYE", "goodbyeCmdId", CommandFlags.Modal
  17.     )]
  18.     public void GoodbyeCommand()
  19.     {
  20.       Application.DocumentManager.MdiActiveDocument.Editor.
  21.         WriteMessage("\nGoodbye!");
  22.     }
  23.   }
  24. }
You’ll notice straight away that we’re using a special version of the CommandMethod attribute which specifies more than the usual items (command name and flags are the most common ones). We start with the command group (“LOCCMDS”), the global names (“HELLO” and “GOODBYE”), the localized resource IDs (“helloCmdId” and “goodbyeCmdId”) and then come the flags (CommandFlags.Modal).
The localized command name is provided as a resource ID to aid localization. Let’s see how this helps… we’ll add some resources to the project for our neutral culture (US English) and for two additional cultures (French and German).
We can add resource files by right-clicking the project and selecting Add –> New Item:

From here we can use the Add New Item dialog to add new resource files with the names “Commands.resx” (which is for the neutral culture, US English), “Commands.fr-FR.resx” and “Commands.de-DE.resx” (these three files will need to be added one-by-one). It’s important that the resource files use the same base name as the .cs file (they do not have to use the class name – LocCmds – and, in fact, you may run into name collisions if you attempt to do so).

So far, so good. We should now see the resultant files in our solution explorer, and be able to open them by double-clicking them.

The files will be blank initially, and we will want to add two string resources to each, one for each of our commands, using the IDs “helloCmdId” and “goodbyeCmdId”.
When we come to edit the individual string resources, we may be presented with a warning dialog (it is safe to select Yes):

Now we can go ahead and add localized string resources for our two commands in each of the three cultures:

It should now be possible to build our application. If we take a look at the output folder, we should see sub-folders for each of our cultures:

The en-US folder will be empty, as we’ve included that as our base culture, but the other two will contain resource DLLs (also known as satellite assemblies) containing our localized strings for that target culture.
Now to see these in action. In order to fake the loading of our assembly into different language versions of AutoCAD, I put together a simple application to change the UI culture (the one used by the resource manager to choose the resources to load) of the current thread. This clearly needs to be in a separate assembly, as we want to change the culture before we use NETLOAD to load the assembly containing our localized commands.
Here’s the C# code:
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.Runtime;
  3. using System.Threading;
  4. using System.Globalization;
  5. namespace CultureShift
  6. {
  7.   public class Commands
  8.   {
  9.     private void setCulture(string culture)
  10.     {
  11.       Thread.CurrentThread.CurrentUICulture =
  12.         new CultureInfo(culture);
  13.     }
  14.     private void setNeutralCulture(string neutCult)
  15.     {
  16.       Thread.CurrentThread.CurrentUICulture =
  17.         CultureInfo.CreateSpecificCulture(neutCult);
  18.     }
  19.     [CommandMethod("SETFR")]
  20.     public void SetFrenchCulture()
  21.     {
  22.       setNeutralCulture("fr"); // or setCulture("fr-FR");
  23.       GetCulture();
  24.     }
  25.     [CommandMethod("SETDE")]
  26.     public void SetGermanCulture()
  27.     {
  28.       setNeutralCulture("de"); // or setCulture("de-DE");
  29.       GetCulture();
  30.     }
  31.     [CommandMethod("SETEN")]
  32.     public void SetEnglishCulture()
  33.     {
  34.       setNeutralCulture("en"); // or setCulture("en-US");
  35.       GetCulture();
  36.     }
  37.     [CommandMethod("GETCUL")]
  38.     public void GetCulture()
  39.     {
  40.       Application.DocumentManager.MdiActiveDocument.Editor.
  41.         WriteMessage(
  42.           "\nCurrent UI culture is {0}.",
  43.           Thread.CurrentThread.CurrentUICulture.Name
  44.         );
  45.     }
  46.   }
  47. }
This code implements a number of commands to set the UI culture to US English (SETEN), French (SETFR) and German (SETDE) as well as to check the current UI culture (GETCUL). The code chooses to set the culture neutrally – which sets the language but not the location – but the end result is the same for the cultures we’ve chosen. One thing to bear in mind: this code only actually works if running from the debugger. It doesn’t crash, otherwise, but the current UI culture always ends up as “en-US”. I assume this is a threading issue, but as this is really only for testing purposes it’s a tolerable requirement to run everything from the debugger.
Here’s what happens when (launching AutoCAD from the Visual Studio 2009 debugger) we load our test code, set the current UI culture to French and then load the main application, checking which commands work and which do not:
  1. Command: NETLOAD
  2. Command: GETCUL
  3. Current UI culture is en-US.
  4. Command: SETFR
  5. Current UI culture is fr-FR.
  6. Command: GETCUL
  7. Current UI culture is fr-FR.
  8. Command: NETLOAD
  9. Command: HELLO
  10. Hello!
  11. Command: GOODBYE
  12. Goodbye!
  13. Command: BONJOUR
  14. Hello!
  15. Command: AUREVOIR
  16. Goodbye!
  17. Command: GUTENTAG Unknown command "GUTENTAG".  Press F1 for help.
  18. Command: BYE Unknown command "BYE".  Press F1 for help.
复制代码
We can see that the global and the French-localized commands have indeed worked, while the German and US ones have not.
Let’s do the same for German:
  1. Command: NETLOAD
  2. Command: GETCUL
  3. Current UI culture is en-US.
  4. Command: SETDE
  5. Current UI culture is de-DE.
  6. Command: GETCUL
  7. Current UI culture is de-DE.
  8. Command: NETLOAD
  9. Command: HELLO
  10. Hello!
  11. Command: GOODBYE
  12. Goodbye!
  13. Command: BONJOUR Unknown command "BONJOUR".  Press F1 for help.
  14. Command: GUTENTAG
  15. Hello!
  16. Command: AUFWIEDERSEHEN
  17. Goodbye!
  18. Command: BYE Unknown command "BYE".  Press F1 for help.
复制代码
And sure enough, while the German and global commands work, the others do not.
For completeness, if we just load our application – without either launching from the debugger or loading our test application to change the current UI culture – and run the commands, here’s what we see:
  1. Command: NETLOAD
  2. Command: HELLO
  3. Hello!
  4. Command: GOODBYE
  5. Goodbye!
  6. Command: HI
  7. Hello!
  8. Command: BYE
  9. Goodbye!
  10. Command: BONJOUR Unknown command "BONJOUR".  Press F1 for help.
  11. Command: AUREVOIR Unknown command "AUREVOIR".  Press F1 for help.
  12. Command: GUTENTAG Unknown command "GUTENTAG".  Press F1 for help.
  13. Command: AUFWIEDERSEHEN Unknown command "AUFWIEDERSEHEN".  Press F1 for help.
复制代码

My assumption is that the current UI culture will be set appropriately in the different language versions of AutoCAD, and so the localized resources will be chosen correctly. If someone trying the technique for real were able to confirm, I'd certainly appreciate it. :-)

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

x
您需要登录后才可以回帖 登录 | 注册

本版积分规则

小黑屋|手机版|CAD论坛|CAD教程|CAD下载|联系我们|关于明经|明经通道 ( 粤ICP备05003914号 )  
©2000-2023 明经通道 版权所有 本站代码,在未取得本站及作者授权的情况下,不得用于商业用途

GMT+8, 2024-11-25 16:25 , Processed in 0.209925 second(s), 24 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表