AutoCAD API Question: Capturing Changes from Properties Window

AutoCAD API Question: Capturing Changes from Properties Window

M_Kocyigit
Enthusiast Enthusiast
1,886 Views
17 Replies
Message 1 of 18

AutoCAD API Question: Capturing Changes from Properties Window

M_Kocyigit
Enthusiast
Enthusiast

Hello everyone,

Is it possible to determine via the AutoCAD API whether changes to an object were made through the Properties window?

For example: An object is selected, and its insertion point is modified using the Properties window.

Which event could be used to solve this? Do you have any ideas?

1,887 Views
17 Replies
Replies (17)
Message 2 of 18

norman.yuan
Mentor
Mentor
Accepted solution

No knowing your goal, knowledge of AutoCAD .NET API..., I can only suggest to look into either handle Database.ObjectModified event (maybe also other relevant events), or ObjectOverrule, or combination of both. Be warning, this is quite advanced topic, especially with Overrule. You may want to search this forum for "ObjectModified", "ObjectOverrule".

 

 

Norman Yuan

Drive CAD With Code

EESignature

Message 3 of 18

M_Kocyigit
Enthusiast
Enthusiast

Hi Norman,

 

Thank you for your response and the helpful suggestions!

My knowledge of the AutoCAD .NET API is rather moderate since I primarily work with ISD products as an application developer. My main focus is more on design and construction rather than full-stack programming.

You are absolutely right that `Database.ObjectModified` could be a possible solution, and I will look into it further. I will also search the forum for the terms you mentioned, such as "ObjectModified" and "ObjectOverrule," to gain more insight.

If other forum members have additional ideas or suggestions, I would greatly appreciate further guidance as well.

Thank you again for your support!

 

Best regards
Muharrem

Message 4 of 18

ActivistInvestor
Mentor
Mentor
Accepted solution

 

Out of curiosity, why would you want to distinguish between changes made from the OPM and changes made via other means?  

 

/// <summary>
/// Example showing how to detect if changes to
/// objects were triggered by a user action in
/// the OPM.
/// </summary>

public static class ModelessOperationObserver
{
   static bool opmPropertyChanging = false;
   static bool modelessOperationInProgress = false;
   static string context;

   /// <summary>
   /// From an ObjectModified event handler, or an ObjectOverrule
   /// override, this property will be true if the modification
   /// was triggered by an OPM user action
   /// </summary>

   public static bool IsOPMPropertyChanging => opmPropertyChanging;

   /// <summary>
   /// True if any type of modeless operation is in-progress.
   /// </summary>
   public static bool IsModelessOperationInProgress => modelessOperationInProgress;

   /// <summary>
   /// The context of the current modeless operation
   /// </summary>
   public static string Context => context ?? "";

   [CommandMethod("OPMObserverExample")]
   public static void OPMObserverExample()
   {
      Document doc = Application.DocumentManager.MdiActiveDocument;
      doc.ModelessOperationWillStart += modelessOperationWillStart;
      doc.Database.ObjectModified += objectModified;

      static void modelessOperationWillStart(object sender, ModelessOperationEventArgs e)
      {
         Document doc = (Document)sender;
         doc.ModelessOperationEnded += modelessOperationEnded;
         context = e.Context;
         modelessOperationInProgress = true;
         opmPropertyChanging = e.Context == "OPM_CHGPROP";
      }

      static void modelessOperationEnded(object sender, ModelessOperationEventArgs e)
      {
         Document doc = (Document)sender;
         doc.ModelessOperationEnded -= modelessOperationEnded;
         modelessOperationInProgress = false;
         opmPropertyChanging = false;
         context = string.Empty;
      }

      static void objectModified(object sender, ObjectEventArgs e)
      {
         if(IsOPMPropertyChanging)
         {
            Write("\n<<Object change triggered by OPM action>>");
         }
      }

      static void Write(string msg)
      {
         Application.DocumentManager.MdiActiveDocument?.
            Editor.WriteMessage(msg);
      }
   }
}

 

Message 5 of 18

M_Kocyigit
Enthusiast
Enthusiast

Hi @ActivistInvestor,

Thank you very much for this example. I tried it, and it works flawlessly. It matches exactly what I needed.

My initial approach was to intercept commands that modify an object using doc.CommandWillStart. The commands were, for instance, "MOVE" or "GRIP_STRETCH". However, I didn’t know how to detect changes made through the properties panel.


I know many might think: "Oh my God, why is he making it so complicated?" In fact, this could also be solved directly with doc.Database.ObjectModified.

Nevertheless, you showed us how to solve this case, and it was very instructive.


At this point, I would like to sincerely thank you and everyone else for your help. Mentors like @norman, @_gile, @kerry_w_brown, and @ntclmain also do an excellent job here. I look forward to deepening my experience further.

 

 

doc.CommandWillStart += new CommandEventHandler( event_CommandWillStart );

private void
event_CommandWillStart   ( object sender, CommandEventArgs e )
{
  if ( e.GlobalCommandName == "MOVE" || e.GlobalCommandName == "GRIP_STRETCH")
  {
     isMoveCmd = false;
     doc.Database.ObjectModified += new ObjectEventHandler( OnObjectModified );
  }
}

private void
OnObjectModified ( object sender, ObjectEventArgs e )
{
  if ( e.DBObject is BlockReference blockRef )
  {
    if ( !isMoveCmd && doc != null )
    {
      // to do

      isMoveCmd = true;
    }
  }
}

 

 

 

 

 

 

 

Message 6 of 18

ActivistInvestor
Mentor
Mentor
Accepted solution

@M_Kocyigit wrote:

Hi @ActivistInvestor,

Thank you very much for this example. I tried it, and it works flawlessly. It matches exactly what I needed.

My initial approach was to intercept commands that modify an object using doc.CommandWillStart. The commands were, for instance, "MOVE" or "GRIP_STRETCH". However, I didn’t know how to detect changes made through the properties panel.


I know many might think: "Oh my God, why is he making it so complicated?" In fact, this could also be solved directly with doc.Database.ObjectModified.

Nevertheless, you showed us how to solve this case, and it was very instructive.


At this point, I would like to sincerely thank you and everyone else for your help. Mentors like @norman, @gile, @kerry_w_brown, and @ntclmain also do an excellent job here. I look forward to deepening my experience further.

 

 

 

doc.CommandWillStart += new CommandEventHandler( event_CommandWillStart );

private void
event_CommandWillStart   ( object sender, CommandEventArgs e )
{
  if ( e.GlobalCommandName == "MOVE" || e.GlobalCommandName == "GRIP_STRETCH")
  {
     isMoveCmd = false;
     doc.Database.ObjectModified += new ObjectEventHandler( OnObjectModified );
  }
}

private void
OnObjectModified ( object sender, ObjectEventArgs e )
{
  if ( e.DBObject is BlockReference blockRef )
  {
    if ( !isMoveCmd && doc != null )
    {
      // to do

      isMoveCmd = true;
    }
  }
}

 

 

I can tell you just from looking at your code above, that you're taking the wrong approach to doing whatever it is you intend/want to do where your "// to do" comment appears. 

 

You would be much better off using an Overrule (ObjectOverrule or TransformOverrule, depending on what you want to do) and not discriminating about what command may be performing a 'logical' move/translation of the objects of interest.

 

Not to worry, as this is fairly-common for those who are new to the API and aren't familiar with all of the tools available to achieve their objectives. Overrules provide a much more lower-level and 'transparent' way get control when objects are modified, and a way to act on those objects, regardless of how they are modified (by any command, or even by scripting for that matter).

 

To offer more advice on whether overrules would be a better way to achieve your objectives, I would need to know more about that (e.g., what is it you want to do when a user moves an object of interest)?

 

Message 7 of 18

M_Kocyigit
Enthusiast
Enthusiast

Thank you for your feedback and recommendation on Overrules:

Hi @AktivistInvestor,

Thank you for your detailed feedback and the recommendation to use an Overrule (e.g., ObjectOverrule or TransformOverrule). Your explanation will certainly be very helpful, though I will need to deepen my understanding of this topic first.

To give you a clearer idea of my goal, here’s what I’m trying to achieve:

I want to trigger a specific action when a user moves a static block that has three descriptive attributes: X, Y, and Z coordinates. These attributes should be automatically populated with specific information based on the new position of the block.

Thank you in advance for your support!

Best regards

 

Message 8 of 18

ActivistInvestor
Mentor
Mentor
Accepted solution

After reading your last reply I can definitely say you're approaching the solution from the wrong direction. Your idea is to 'watch' the user's actions to determine when you need to act, which is flawed because there are other ways that entities can be moved/modified that you haven't taken into account, and that means that if a block reference is modified via any of those unaccounted-for ways, its attributes will contain invalid values.


As I see it, there are two possible solutions that are as close as one can get to bullet-proof:


The first is FIELDS, with a formula that can perform calculations using parameters from the containing block reference (such as its insertion point), or parameters obtained from another referenced object. That may or may not work, depending on precisely how the attribute text values are calculated (feel free to elaborate).


If FIELDS will not work, then an ObjectOverrule will work, because it can update the attribute values whenever the block reference containing them is modified, regardless of what triggered the change. That relieves you from having to watch all sorts of events to try to infer what's happening from user-actions. The ObjectModified event is not a good solution, because it is raised for every object in the Database that's modified, and can impose noticeable overhead in operations involving a large number of objects.

 

I was able to quickly modify an existing example I had written years ago to do something similar to what you're after. You can find the example here and a sample DWG file here.

 

Message 9 of 18

M_Kocyigit
Enthusiast
Enthusiast
Hi @ActivistInvestor,

Thank you very much for your detailed and helpful response. Your explanation has given me a new perspective on the challenges of my current approach, and I completely agree that monitoring user actions is not a robust long-term solution.

I have already reviewed the example and the DWG file you shared, and I find the approach highly insightful. As I delve deeper into the implementation, I may reach out again in this forum if I encounter any challenges.

Once again, thank you for your support and valuable insights! I wish you and everyone else a relaxing and peaceful Sunday.

Best regards,
M_Kocyigit
Message 10 of 18

ActivistInvestor
Mentor
Mentor
Accepted solution

As usual, in my haste to bring 10-year-old code back from the dead, I introduced a minor bug. If you downloaded the files at the links I posted, you should get the fixed versions there now. You'll also find a couple of examples here

Message 11 of 18

M_Kocyigit
Enthusiast
Enthusiast
Thank you for your suggestion. Best regards!
0 Likes
Message 12 of 18

M_Kocyigit
Enthusiast
Enthusiast

Hi @ActivistInvestor ,

I tried your code, and I have to say this approach is brilliant! I'm especially impressed by your willingness to help – I really appreciate it.

I have another question regarding this:

 

 

 

        protected override void Update ( AttributeReference att, BlockReference owner )
        {
            Point3d pos = owner.Position;

            AppUtils.Write($"\n Attribute Overrule {att.Tag}");

            if ( att.Tag == "Y" )
            {
                level = pos.Y / 1000;

                att.TextString = $"{level:0.000}";

                /*
                using (Transaction tr = new OpenCloseTransaction())
                {
                    // LINQ verwenden, um das gewünschte Attribut zu finden

                    AttributeReference attribute = owner.AttributeCollection
                        .Cast<ObjectId>()
                        .Select(attId => tr.GetObject(attId, OpenMode.ForRead) as AttributeReference)
                        .FirstOrDefault(attRef => attRef != null && attRef.Tag.Equals("REFHEIGHT", StringComparison.OrdinalIgnoreCase));

                    tr.Commit ();
                }*/
            }
        }

 

 

 

What do I need to check in this method to ensure that the transaction doesn't throw any errors?

Best regards

0 Likes
Message 13 of 18

ActivistInvestor
Mentor
Mentor

You can't use GetObject() to open AttributeReferences from the Update() method, because they are already being opened one at a time by the code calling Update(), so if you try to do that, it will fail. But, I don't see why you would need to do that, when you can have the Update() method called and passed the REFHEIGHT attribute, by just passing its tag to the constructor. 

 

The BlockAttributeOverrule's constructor takes one or more strings that identify the tag(s) that you want the Update() method to be passed, so you don't have to redundantly try to open the attribute from the Update() method.

 

You just specify the "REFHEIGHT" tag in the constructor like this:

 

MyBlockAttributeOverrule() : base(true, "MYBLOCKNAME", "REFHEIGHT")
{
}

 

Where "MYBLOCKNAME" is the name of the block (or a wildcard pattern matching the names of multiple blocks), and "REFHEIGHT" is the tag of the attribute that you want Update() to be passed.

 

With the above constructor code, only one call to Update() will happen, and it will be passed the AttributeReference having the tag 'REFHEIGHT'. 

 

So, if that's the attribute whose display you want to alter, then:

 

protected override void Update(AttributeReference att, BlockReference owner)
{
   double level = owner.Position.Y / 1000;
   att.TextString = $"{level:0.000}";
}

 

You don't have to check the Tag property because with the above constructor, Update() will only be called for the attribute having the tag 'REFHEIGHT'.

0 Likes
Message 14 of 18

M_Kocyigit
Enthusiast
Enthusiast

Hi @ActivistInvestor,

Thank you for your response and the clarification regarding the `BlockAttributeOverrule` constructor!
I am already familiar with the principle you demonstrated in your `OrdinateAttributeOverrule` class, where multiple tags can be specified through the constructor. This approach, e.g., `base("MYBLOCKNAME", "LEVEL", "REFHEIGHT")`, is well understood.


My goal was to correctly capture the value of an attribute like `REFHEIGHT` and subtract it from the current Y-position of the block’s insertion point.

The calculation is as follows:
`VALUE = Block_InsertionPoint.Y - REFHEIGHT`


This approach works flawlessly when I assign this calculated value to the `att.TextString`.

However, my misleading question regarding "Transaction" had the following context:

I want to update an AutoCAD block with field functions, ensuring that the Overrule automatically updates the block and evaluates the fields. This would eliminate the need for a subsequent regeneration.

The challenge arises when I need to replace or read a field, as this requires an active transaction.

My question is:
What would be the best approach to correctly manage the transaction in this scenario while ensuring the Overrule reliably updates the field functions? Is there a recommended strategy for achieving this in combination with Overrules?

Thank you again for your support and valuable insights!

 

0 Likes
Message 15 of 18

ActivistInvestor
Mentor
Mentor

I want to update an AutoCAD block with field functions, ensuring that the Overrule automatically updates the block and evaluates the fields. This would eliminate the need for a subsequent regeneration.

I'm not entirely clear on what you mean by 'update an AutoCAD block with field functions'. If you can offer further details and/or a sample .DWG file showing a block with the fields you mention above, perhaps I can offer some advice.

 

The challenge arises when I need to replace or read a field, as this requires an active transaction.

The attributes are already open in a Transaction (an OpenCloseTransaction, which is required in an Overrule context) when they are passed to the Update() method, but the BlockReference is not open in a Transaction, and probably can't be because it is already opened by AutoCAD for write.

 

An OpenCloseTransaction is not really a Transaction in the same sense that one started with StartTransaction() is, and any operation that requires an object to be open in a Transaction, will most likely require one that's started with StartTransaction(), which is a no-no from within an Overrule, because it will corrupt UNDO/REDO.

 

i can add a virtual method that is called after all attributes have been passed to Update(), which might be helpful in your use case, but that remains to be seen as I'm still not sure what you are actually trying to achieve.

 

0 Likes
Message 16 of 18

M_Kocyigit
Enthusiast
Enthusiast

Hello @ActivistInvestor,

I have provided you with the following block (See the two attached files). I would like this block to update automatically, similar to a regeneration process. Unfortunately, I couldn’t solve the issue on my own and need your help with it.

 

For your information:
I was frustrated that I couldn’t solve such a simple task. For this reason, I implemented a workaround that works. This block is directly downloaded and inserted from a tool palette. However, the first time I used it, AutoCAD generated an error message. I wanted to avoid this.


Furthermore, I modified your base class BlockAttributeOverrule by retrieving the current transaction. I know this will upset you, but I really didn’t know how else to move forward. Please help me get back on track so that I don’t make serious mistakes or follow incorrect approaches in my future work (or even in my whole career).

Thank you for your understanding and support!

 

 

using System;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;

namespace Tools.ACAD.Overrules
{
    public class ElevationLevelOverrule : BlockAttributeOverrule
    {
        public static ElevationLevelOverrule instance = null;

        public ElevationLevelOverrule() 
            : base(true, "*DYN-LEVEL_AUTO*", "HÖHENMAß")
        {

            InsertEnabled = true;
        }

        protected override void Update(AttributeReference att, BlockReference owner)
        {
            try
            {
                // The block is inserted via tool palettes.
                // For some reason, AutoCAD generates an error message.
                // Therefore, I bypass this issue by inserting the block in another way.
                // --> Any ideas what it could have been? <--
                if (AppUtils.ActiveCommand != "EXECUTETOOL")
                {
                    var ed = Application.DocumentManager.MdiActiveDocument.Editor;
                    ed.WriteMessage($"{Environment.NewLine}Active Command: {AppUtils.ActiveCommand}{Environment.NewLine}");

                    Point3d pos = owner.Position;
                    double dLevel = pos.Y;

                    if (att.Tag == "HÖHENMAß")
                    {
                        // New property implemented in BlockAttributeOverrule.
                        var tr = base.GetTransaction;
                        if (tr == null) return;

                        string refLevel = "REFERENZHÖHE";
                        AttributeReference attRefHeight = null;

                        foreach (ObjectId attId in owner.AttributeCollection)
                        {
                            var attRef = tr.GetObject(attId, OpenMode.ForRead) as AttributeReference;
                            if (attRef != null && attRef.Tag.Equals(refLevel, StringComparison.OrdinalIgnoreCase))
                            {
                                attRefHeight = attRef;
                                break;
                            }
                        }

                        if (attRefHeight != null)
                        {
                            if (double.TryParse(attRefHeight.TextString, out double nRefHeight))
                            {
                                dLevel -= nRefHeight;
                            }
                        }

                        string sLevel = NormalizeDouble(dLevel, 3, out string format);
                        if (double.TryParse(sLevel, out double nLevel))
                        {
                            nLevel /= 1000;
                            att.TextString = BlockExtension.GetPrefixForYPosition(nLevel) + string.Format(format, nLevel);
                            BlockExtension.CheckAndDisableWipeoutFrame();
                        }
                    }
                }
            }
            catch (Autodesk.AutoCAD.Runtime.Exception ex)
            {
                Application.DocumentManager.MdiActiveDocument.Editor.WriteMessage($"ERROR: {ex.Message}");
            }
        }

        /// <summary>
        /// Normalizes the given double value to a specified number of decimal places, 
        /// returning the value as a string in a specified format. 
        /// The format string used for conversion is also outputted.
        /// </summary>
        /// <param name="value">The double value to normalize.</param>
        /// <param name="decimalPlaces">The number of decimal places to normalize the value to.</param>
        /// <param name="format">An output parameter that will contain the format string used to format the value.</param>
        /// <returns>A string representing the normalized double value with the specified number of decimal places.</returns>

        public static string NormalizeDouble(double value, int decimalPlaces, out string format)
        {
            format = $"{{0:0.{new string('0', decimalPlaces)}}}";
            return string.Format(format, value);
        }
    }
}

 

 

 

 

 

 

 

 

 

0 Likes
Message 17 of 18

ActivistInvestor
Mentor
Mentor

I'll have a look at your files tomorrow, but I should also mention that because I'm adding the BlockAttributeOverrule to AcMgdLib along with a few examples, I wanted to bring it out of the dark ages, adding significant optimizations. The version I've been refining has no problems with the toolpallete, but it's very possible the old version you have may.

 

 

The problem with tool palettes may have been solved by adding one line of code to the Close() method, which checks to see if the Block's Owner is the current space:

 

 

 

public sealed override void Close(DBObject obj)
{
   try
   {
      if(obj.IsReallyClosing

         // Optimization:
         //
         // Operating on the block reference is entirely
         // pointless if the expression below returns false.
         // When the INSERT* (or PASTECLIP) command is used
         // to insert targeted block references, this Close() 
         // override will be called EVERY TIME THE MOUSE MOVES 
         // while dragging the insertions, even though the 
         // dragged objects are not updated (they are clones 
         // that are not passed to this method).
         //
         // Exiting this method if the following expression
         // returns false is a significant optimization:

         && obj.Database.CurrentSpaceId == obj.OwnerId   // <- Optimzation

         && obj.IsWriteEnabled
         && obj.IsModified

     <balance of code omitted>

 

 

I'll take a look at your files and see if I can recommend a solution, but you will probably want to migrate to the new version I'll be committing in the near future, because it has many optimizations that solve a problem the original code had, which is that it updates the attributes whenever the block reference is modified, regardless of how or why it was modified. So for example, if you change the block reference's Layer, or some other property but do not move it, the code will still update the attributes even though there's no need for it (because the position did not change).

 

The new version of the code will only update the attributes when the BlockReference's Position changes, which is a major improvement over the 'legacy' code example.

Message 18 of 18

M_Kocyigit
Enthusiast
Enthusiast

Hi @ActivistInvestor ,

Great that you're responding so quickly and taking the time for it.
I'm waiting eagerly like a little child waiting for their Christmas gift.

In this sense, have a nice day and thanks again.

0 Likes