.NET
cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

Canceling Command Externally with COM

9 REPLIES 9
Reply
Message 1 of 10
Anonymous
1002 Views, 9 Replies

Canceling Command Externally with COM

So I'm working on a program that runs independent of AutoCAD in its own process space.  I have an AutoCAD dll that is netloaded to receive commands sent to it to perform the necessary function.  Basically, from the external exe, I'm just capturingthe COM Application object and using SendCommand().  That has worked great for all my needs.  However, we have recently discovered that if a command is in session, the COM object enters a state of exception and can't even tell us that a command is in session.  Nor can I send a command.  For example, if the user had started a line command, then used our external tool, an exception will be raised simply by accessing the COM object's properties.  I haven't provided a code sample here, because it's a bit involved and would take some time to generalize. But if anyone can point me in the right direction, that would be so helpful.

9 REPLIES 9
Message 2 of 10
norman.yuan
in reply to: Anonymous

If your solution is to let user run an EXE application while he/she is working with AutoCAD and the EXE application need to interact with AutoCAD regardless what the user is doing in AutoCAD, I'd say the solution is a bad one: the user already runs AutoCAD there, why does he/she need to use another app to do things in AutoCAD and be forced to switch in between back and forth?

 

If there are good reasons that to run an EXE to control AutoCAD doing something, it would be better to let the EXE app have an exclusively accessible AutoCAD session. For example, once the app starts (or stats to connect to AutoCAD session), you can always start a new AutoCAD session. If you want to use existing AutoCAD session when there is one (you can searcn acad.exe process), you may want to warn user to finish/cancell existing command...

Norman Yuan

Drive CAD With Code

EESignature

Message 3 of 10
Anonymous
in reply to: norman.yuan

Yeah, I'm considering verifying the state of the application and using the UI binding to disable the interaction buttons when this occurs.  However, that requires checking the state of the COM object which uses reflection to obtain, so I'm not super thrilled about the potential performance impact.  If all else fails, then I'm going to start looking to see if it can bypass the enabled binding and be more strategic.  The warning would be a nice last resort though. 

 

It is a stand-alone application for a good reason.  It reads the data from our server-side databases and gives users the ability to read, create, modify, and associate the data to projects.  Over 90% of its functionality is stand-alone and by keeping it a separate exe, users without AutoCAD will still be able to use it.  We will have plenty of cases like this.  However, it also has to give the user who does have AutoCAD the option to select data and insert into the drawing (it uses basic blocks defined internal to the netloaded dll).  It also has to extract the data from the drawings to process within the exe so that it can be staged for one of our enterprise systems.  With the netloaded dll, we've got the extraction working and inserting of selected items, but ran into an issue where the command caused it to raise exceptions due to the state of the COM AcadApplication object while a command is in progress.  This is literally our last hoop to go through on this application.  If there's a way to cancel commands from the exe before we send a command, then I feel we can have a more solid system.  If there is no way, then like I said, we'll first look at a strategic disabling of the exe controls, and if that doesn't work a message that warns completing commands while absorbing the exception.

Message 4 of 10
ActivistInvestor
in reply to: Anonymous


@Anonymous wrote:

So I'm working on a program that runs independent of AutoCAD in its own process space.  I have an AutoCAD dll that is netloaded to receive commands sent to it to perform the necessary function.  Basically, from the external exe, I'm just capturingthe COM Application object and using SendCommand().  That has worked great for all my needs.  However, we have recently discovered that if a command is in session, the COM object enters a state of exception and can't even tell us that a command is in session.  Nor can I send a command.  For example, if the user had started a line command, then used our external tool, an exception will be raised simply by accessing the COM object's properties.  I haven't provided a code sample here, because it's a bit involved and would take some time to generalize. But if anyone can point me in the right direction, that would be so helpful.


The best way to solve the problem is to expose your NETLOADed DLL to your standalone process via COM, and use the AcadApplication.GetInterfaceObject() method to access it, rather than sending commands to the command line. Driving in process code by sending commands to AutoCAD is not robust, because you can't easily determine what may have gone wrong with the in-process component, or wait until whatever operation you started by sending a command has completed, in order to take further actions.

 

Regardless of that, AutoCAD is in a busy state when a command is running, and you can't execute any in-process code in that state, so the only solution to cancelling the current command is by sending keyboard messages to the AutoCAD window via the Windows API (e.g., SendMessage, PostMessage, etc).

 

You should be able to poll the AcadApplication.IsQuiescent property without triggering an exception. If you can do that, you can use an Application.Idle event handler in your standalone application to regularly poll AutoCAD to see if it's busy or not, and enable your UI when it is. You can also wrap the handler for the Application.Idle event into an object that exposes a property (e.g., AcadIsReady, or some such) that you can data bind to, to enable/disable UI elements. To do that you Implement INotifyPropertyChanged on the object that exposes that property, and raise it's PropertyChanged event when you detect that AutoCAD has entered/left a quiescent state, or has started or stopped running. Then, you bind the Enabled property of your UI elements to your 'AcadIsReady' property and that should do it.

Message 5 of 10
Anonymous
in reply to: ActivistInvestor

Sorry for the late response, I've spent a couple days with my family while the kids are on Fall break.  This is an interesting approach and one that I didn't even realize existed.  Let me play around with this and see where it goes.  Thanks for the direction!

Message 6 of 10
ActivistInvestor
in reply to: Anonymous


@Anonymous wrote:

Sorry for the late response, I've spent a couple days with my family while the kids are on Fall break.  This is an interesting approach and one that I didn't even realize existed.  Let me play around with this and see where it goes.  Thanks for the direction!


Just the other day, I came across this somewhat old code that does almost exactly what I described, except that it doesn't use an Idle handler, it uses a Timer instead. It isn't the optimal approach because of its reliance on a timer, but it does work.

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.Runtime.InteropServices;

namespace Namespace1
{

   /// <summary>
   /// A class that monitors the state of AutoCAD from another
   /// process, signals when that state changes, and provides 
   /// a means to get and use the AcadApplication object.
   /// 
   /// The IsAcadReady property can be bound to the Enabled 
   /// property of user-interface elements to automatically 
   /// control their Enabled state based on AutoCAD's state.
   /// 
   /// When the Enabled property of a Control is bound to the
   /// IsAcadReady property of this class, the bound Control
   /// will be enabled only when AutoCAD is running, and in a
   /// state that allows automation calls to be serviced.
   /// 
   /// See the usage example below for more details.
   /// </summary>

   [DefaultBindingProperty("IsAcadReady")]
   public class AcadConnector : IDisposable, INotifyPropertyChanged
   {
      bool disposed;
      dynamic acadObject = null;
      bool ready = false;
      System.Windows.Forms.Timer timer;
      string progId = "AutoCAD.Application";
      Type acadType;

      public AcadConnector(string progId = "AutoCAD.Application", int pollingInterval = 500)
      {
         if(string.IsNullOrEmpty(progId))
            throw new ArgumentException("progId cannot be null or empty");
         this.progId = progId;
         acadType = Type.GetTypeFromProgID(progId);
         if(acadType == null)
            throw new ArgumentException(string.Format("Type for {0} not found", progId));
         timer = new System.Windows.Forms.Timer();
         timer.Interval = Math.Max(pollingInterval, 100);
         timer.Enabled = false;
         timer.Tick += timerTick;
      }

      void timerTick(object sender, EventArgs e)
      {
         /// Disable the timer if nobody is listening:
         UpdateTimerState();
         /// update the current AutoCAD state:
         queryAcadState();
      }

      /// <summary>
      /// Determines if AutoCAD is running, and in a state 
      /// that allows automation calls to be serviced.
      /// </summary>

      void queryAcadState()
      {
         if(acadObject == null)
         {
            try
            {
               acadObject = Marshal.GetActiveObject(progId);
            }
            catch(COMException ex)
            {
               LastError = ex;
               setIsReady(false);
               if(ex.ErrorCode != -2147221021)
                  throw;
               return;
            }
         }
         try
         {
            setIsReady((bool) acadObject.GetAcadState().IsQuiescent);
         }
         catch(System.Exception ex)
         {
            LastError = ex;
            acadObject = null;
            setIsReady(false);
         }
      }

      /// <summary>
      /// This method updates the IsAcadReady property and 
      /// fires both events when the property's value changes.
      /// </summary>

      void setIsReady(bool value)
      {
         if(ready ^ value)
         {
            ready = value;
            OnIsReadyChanged(value);
            if(propertyChanged != null)
               propertyChanged(this, new PropertyChangedEventArgs("IsAcadReady"));
         }
      }

      /// <summary>
      /// A virtual method that can be overridden by a
      /// derived type for more specialized purposes.
      /// 
      /// This method also fires the IsReadyChanged event.
      /// </summary>
      
      protected virtual void OnIsReadyChanged(bool value)
      {
         if(isReadyChanged != null)
            isReadyChanged(this, new IsReadyChangedEventArgs(value));
      }

      /// <summary>
      /// The property that indicates if an instance of
      /// AutoCAD is running and is currently not busy.
      /// 
      /// This property can be bound to the Enabled 
      /// property of UI elements to synchronize their
      /// enabled state with AutoCAD's quiescent state.
      /// </summary>

      public bool IsAcadReady
      {
         get
         {
            if(!timer.Enabled)
               queryAcadState();
            return ready;
         }
      }

      /// <summary>
      /// The AcadApplication instance as a dynamic object
      /// </summary>

      public dynamic Application
      {
         get
         {
            return acadObject;
         }
      }

      /// <summary>
      /// Indicates if there is a running instance of AutoCAD,
      /// regardless of whether it is in a quiescent state, or
      /// not.  
      /// 
      /// Note: this property is not elegible for data binding.
      /// </summary>
      
      public bool IsAcadActive
      {
         get
         {
            if(!timer.Enabled)
               queryAcadState();
            if(acadObject != null)
            {
               try
               {
                  bool value = (bool) acadObject.GetAcadState().IsQuiescent;
                  return true;
               }
               catch
               {
                  acadObject = null;
               }
            }
            return false;
         }
      }

      /// <summary>
      /// The last exception that was caught while polling.
      /// </summary>
      
      public System.Exception LastError
      {
         get;
         private set;
      }

      /// <summary>
      /// Starts a new instance of AutoCAD. After calling
      /// this, use the Application property to get the
      /// AcadApplication object of the instance that was 
      /// started.
      /// </summary>

      public bool Start()
      {
         try
         {
            acadObject = Activator.CreateInstance(acadType);
            if(acadObject != null)
            {
               int i = 0;
               while(true)
               {
                  try
                  {
                     acadObject.Visible = true;
                     setIsReady(true);
                     return true;
                  }
                  catch
                  {
                     Thread.Sleep(100);
                     if(++i > 1000)
                        return false;
                  }
               }
            }
         }
         catch(System.Exception ex)
         {
            LastError = ex;
            acadObject = null;
         }
         return false;
      }

      public void Dispose()
      {
         if(!disposed)
         {
            disposed = true;
            this.timer.Dispose();
         }
      }

      /// <summary>
      /// The design calls for the Timer to be enabled only when
      /// necessary, which is when there is at least one handler 
      /// for either the PropertyChanged or IsReadyChanged events. 
      /// Otherwise, there is no purpose to having a timer running
      /// or routinely polling AutoCAD, so the timer is disabled if
      /// there are no handlers for either of the two events. 
      /// 
      /// When the timer's tick event fires, the Tick handler checks
      /// to see if either of the aforementioned events has at least
      /// one handler, and if not, it disables the timer.
      /// 
      /// When a handler is added to either of the two events, the 
      /// timer is enabled.
      /// </summary>

      /// <summary>
      /// Disable the timer when there are no handlers for 
      /// either of the two events exposed by this class.
      /// This method is called from the timer's Tick handler.
      /// </summary>

      void UpdateTimerState()
      {
         timer.Enabled = isReadyChanged != null || propertyChanged != null;
      }

      /// <summary>
      /// Enables the timer when a handler is added to the event.
      /// </summary>
      public event PropertyChangedEventHandler PropertyChanged
      {
         add
         {
            propertyChanged += value;
            timer.Enabled = true;
         }
         remove
         {
            propertyChanged -= value;
         }
      }

      /// <summary>
      /// Enables the timer when a handler is added to the event.
      /// </summary>
      public event EventHandler<IsReadyChangedEventArgs> IsReadyChanged
      {
         add
         {
            isReadyChanged += value;
            timer.Enabled = true;
         }
         remove
         {
            isReadyChanged -= value;
         }
      }

      event EventHandler<IsReadyChangedEventArgs> isReadyChanged = null;
      event PropertyChangedEventHandler propertyChanged = null;
   }

   public class IsReadyChangedEventArgs : System.EventArgs
   {
      public IsReadyChangedEventArgs(bool isReady)
      {
         this.IsReady = isReady;
      }
      public bool IsReady
      {
         get;
         private set;
      }
   }

   /// Basic Usage Example:
   /// 
   /// <summary>
   /// An example WinForm that connects to AutoCAD via COM,
   /// and uses data binding to automatically enable/disable 
   /// relevant UI elements when AutoCAD is not available or
   /// is busy.
   /// 
   /// The form contains two buttons. One draws a Circle in 
   /// the model space of the active document, and the other
   /// starts an instance of AutoCAD.
   /// 
   /// The code uses the AcadConnector helper class to 
   /// monitor the state of AutoCAD and signal when it has 
   /// changed, and automatically update the enabled state
   /// of the 'Draw a Circle' button.
   /// 
   /// The 'Draw a Circle' button's Enabled property is bound 
   /// to the AcadConnector's 'IsAcadReady' property, which 
   /// returns true if AutoCAD is running and able to service 
   /// automation calls.
   /// 
   /// Whenever AutoCAD's state changes from 'ready' to
   /// 'not ready' the 'Draw A Circle' Button's Enabled
   /// state changes accordingly.
   /// 
   /// </summary>
   
   public class Form1 : System.Windows.Forms.Form
   {
      Button button1;
      Button button2;
      AcadConnector connector;

      public Form1()
      {
         this.button1 = new Button();
         this.SuspendLayout();
         this.button1.AutoSize = true;
         this.button1.Text = "Draw a Circle";
         this.button1.Click += new EventHandler(this.button1_Click);
         button2 = new Button();
         button2.AutoSize = true;
         button2.Location = new Point(12, 12);
         this.button1.Location = new Point(12, button2.Top + button2.Height + 10);
         button2.Text = "Start AutoCAD";
         button2.Click += new EventHandler(button2_Click);
         this.ClientSize = new Size(325, 122);
         this.Controls.AddRange(new[] { button1, button2 });
         this.Text = "Form1";

         /// Create the AcadConnector instance:

         connector = new AcadConnector();

         /// Bind the 'Draw a Circle' Button's Enabled property 
         /// to the AcadConnector's IsAcadReady property:
         
         this.button1.DataBindings.Add("Enabled", connector, "IsAcadReady",
            false, DataSourceUpdateMode.OnPropertyChanged);

         this.ResumeLayout(false);
         this.PerformLayout();
      }

      private void button1_Click(object sender, EventArgs e)
      {
         connector.Application.ActiveDocument.ModelSpace.AddCircle(new[] { 10.0, 8.0, 0.0 }, 5.0);
      }

      void button2_Click(object sender, EventArgs e)
      {
         connector.Start();
      }

      /// <summary>
      /// The instance of the connector should be Disposed with the Form:
      /// </summary>
      
      protected override void Dispose(bool disposing)
      {
         if(disposing && connector != null)
            connector.Dispose();
         connector = null;
         base.Dispose(disposing);
      }
   }





}

 

 

Message 7 of 10
Anonymous
in reply to: ActivistInvestor

I'm actually fairly familiar with waiting on commands to finish, so I'm not to worried here.  I've played around with exposing my own COM-wrapped classes and that seems to be a fairly neat way into the AutoCAD process space.  However, it requires that the classes defined be registered.  I'm hoping to simply create the autoloader bundle folder with instructions, etc, without requiring the need for an installer if possible.  Can these COM components be retrieved registry-free?  I've seen an example regarding Inventor Fusion, but haven't seen anything with regards to AutoCAD. I've also posted a question regarding Regstration-free COM through ADN as I figure this may get deeper than what is commonly used fairly quickly.

Message 8 of 10
ActivistInvestor
in reply to: Anonymous


@Anonymous wrote:

I'm actually fairly familiar with waiting on commands to finish, so I'm not to worried here.  I've played around with exposing my own COM-wrapped classes and that seems to be a fairly neat way into the AutoCAD process space.  However, it requires that the classes defined be registered.  I'm hoping to simply create the autoloader bundle folder with instructions, etc, without requiring the need for an installer if possible.  Can these COM components be retrieved registry-free?  I've seen an example regarding Inventor Fusion, but haven't seen anything with regards to AutoCAD. I've also posted a question regarding Regstration-free COM through ADN as I figure this may get deeper than what is commonly used fairly quickly.


Because GetInterfaceObject() requires a ProgId, there is no way I'm aware of to do zero-registration COM in this case.

 

Also, Visual Studio can't register the components for you during the build process because it requires them to be loaded, and they can't because they have a dependence on AutoCAD DLLs that cannot be loaded into any process other than AutoCAD. RegSvr32.exe has the same problem, because it can't load an assembly that has a dependence on AutoCAD DLLs.

 

So, the way to register a COM server is to load it once using the NETLOAD command, and in the IExtensionApplication.Initialize() method, you check to see if NETLOAD is running in the active document and if it is, you register the assembly using the System.Runtime.InteropServices.RegistrationServices class. The only other way to do it that I'm aware of is to write the registry entries from an installer, and remove in from the uninstall process.

Message 9 of 10
Anonymous
in reply to: ActivistInvestor


Because GetInterfaceObject() requires a ProgId, there is no way I'm aware of to do zero-registration COM in this case.

I'm actually afraid of that, but also hopeful as that would be sweet.


Also, Visual Studio can't register the components for you during the build process because it requires them to be loaded, and they can't because they have a dependence on AutoCAD DLLs that cannot be loaded into any process other than AutoCAD. RegSvr32.exe has the same problem, because it can't load an assembly that has a dependence on AutoCAD DLLs.


Yeah, I figured this out playing with sample code.  However, I did figure out how to make it work through Visual Studio registration with a little nudge:

 

  1. On the project properties Build tab, set 'Regiser for COM interop'
  2. On same tab, set the target platform.  The COM library evidently has to be compiled to its specific platform.  In my case, x64.
  3. When it registers, it needs to know the AutoCAD assemblies exist.  Since our typical references are shell objects anyways with the actual assemblies in the install directory, I set the Copy Local to true.  That's not typically recommended since you would netload into the AutoCAD process space via the Debug properties, but it worked so I'm not complaining.

And, believe it or not, if you attach the AutoCAD process just prior to calling the COM method, you can step through the COM server methods as well.

 


So, the way to register a COM server is to load it once using the NETLOAD command, and in the IExtensionApplication.Initialize() method, you check to see if NETLOAD is running in the active document and if it is, you register the assembly using the System.Runtime.InteropServices.RegistrationServices class. The only other way to do it that I'm aware of is to write the registry entries from an installer, and remove in from the uninstall process.

This is interesting and I may give it a try, but does it require that the application be ran as an administrator?  Our permissions are locked down pretty tight around here.  If it does, which I suspect, then the only other way around would be to create an installer.

Message 10 of 10
ActivistInvestor
in reply to: Anonymous


@Anonymous wrote:

Because GetInterfaceObject() requires a ProgId, there is no way I'm aware of to do zero-registration COM in this case.

I'm actually afraid of that, but also hopeful as that would be sweet.


Also, Visual Studio can't register the components for you during the build process because it requires them to be loaded, and they can't because they have a dependence on AutoCAD DLLs that cannot be loaded into any process other than AutoCAD. RegSvr32.exe has the same problem, because it can't load an assembly that has a dependence on AutoCAD DLLs.


Yeah, I figured this out playing with sample code.  However, I did figure out how to make it work through Visual Studio registration with a little nudge:

 

  1. On the project properties Build tab, set 'Regiser for COM interop'
  2. On same tab, set the target platform.  The COM library evidently has to be compiled to its specific platform.  In my case, x64.
  3. When it registers, it needs to know the AutoCAD assemblies exist.  Since our typical references are shell objects anyways with the actual assemblies in the install directory, I set the Copy Local to true.  That's not typically recommended since you would netload into the AutoCAD process space via the Debug properties, but it worked so I'm not complaining.

And, believe it or not, if you attach the AutoCAD process just prior to calling the COM method, you can step through the COM server methods as well.

 

That's because the COM server is running in AutoCAD's process when you create it via GetInterfaceObject().

 

This is interesting and I may give it a try, but does it require that the application be ran as an administrator?  Our permissions are locked down pretty tight around here.  If it does, which I suspect, then the only other way around would be to create an installer.

Unfortunately, yes it does require admin.

 

As far as Register For COM Interop goes, I suspect you tried this with references to the lobotomized versions of the AutoCAD assemblies that come with the SDK (in the /inc folder) ?  They may not cause the problem, while the actual assemblies from the AutoCAD folder might, and those are the only ones that exist on a target system. Also, load failure doesn't happen until the first use of a method that references a type in AutoCAD's managed API, or a class that has fields that reference types in the managed API.

 

 

Can't find what you're looking for? Ask the community or share your knowledge.

Post to forums  

AutoCAD Inside the Factory


Autodesk Design & Make Report