Intercept specific commands (Copy, Mirror, Array) when a BlockReference is included

Intercept specific commands (Copy, Mirror, Array) when a BlockReference is included

AlexFielder
Advisor Advisor
899 Views
7 Replies
Message 1 of 8

Intercept specific commands (Copy, Mirror, Array) when a BlockReference is included

AlexFielder
Advisor
Advisor

Hi all,

 

This very good solution by @_gile provides a method that you can run called Subscribe (or Unsubscribe should need to turn it off), but I wonder how I can add the command check based on the BlockReference type and these specific commands:

 

 

private static RXClass _entityClass = RXObject.GetClass(typeof(BlockReference));
private static HashSet<string> commandNames = new HashSet<string> { "ARRAY", "ARRAYCLASSIC", "COPY", "MIRROR"};

 

 

My first thought was to call Gile's "Subscribe" method inside Initialize() but that only works when AutoCAD opens and the addin loads and not when *any* drawing file subsequently opens.

 

Then I thought I would connect to the DocumentLockModeChanged event but that didn't really work either.

 

Basically, I need to intercept the commands in the commandNames HashSet whenever a Blockreference is included in order to check for existence and value of (and possibly reset to "") the tagstring of specific attributereferences.

 

Thanks in advance,

 

Alex.

 

0 Likes
Accepted solutions (2)
900 Views
7 Replies
Replies (7)
Message 2 of 8

AlexFielder
Advisor
Advisor

Okay, this was easier than I thought (given time to actually sit and work out what wasn't working.)

 

So here is my solution (built upon the link to Gile's excellent earlier post):

 

Within the Initialize() method I have this:

_dwgManager.DocumentLockModeChanged += new DocumentLockModeChangedEventHandler(interceptCopyToRemoveInfo);

And the rest of the code is thus:

private void interceptCopyToRemoveInfo(object sender, DocumentLockModeChangedEventArgs e)
{
    if (commandNames.Contains(e.GlobalCommandName))
    {
        var doc = Application.DocumentManager.MdiActiveDocument;
        doc.CommandWillStart += Doc_CommandWillStart;
        _handled = true;
    }
    else
    {
        var doc = Application.DocumentManager.MdiActiveDocument;
        doc.CommandWillStart -= Doc_CommandWillStart;
        _handled = false;
    }
}

// Command to subscribe to the envent
[CommandMethod("SUBSCRIBE")]
public void Subscribe()
{
    if (!_handled)
    {
        var doc = Application.DocumentManager.MdiActiveDocument;
        doc.CommandWillStart += Doc_CommandWillStart;
        _handled = true;
    }
}

// Command to unsubscribe to the envent
[CommandMethod("UNSUBSCRIBE")]
public void Unsubscribe()
{
    if (_handled)
    {
        var doc = Application.DocumentManager.MdiActiveDocument;
        doc.CommandWillStart -= Doc_CommandWillStart;
        _handled = false;
    }
}

// CommandWillStart event handler
// Saves the Database.Handseed value and subscribe to CommandEnded event.
private void Doc_CommandWillStart(object sender, CommandEventArgs e)
{
    if (commandNames.Contains(e.GlobalCommandName))
    {
        var doc = (Document)sender;
        _seed = doc.Database.Handseed.Value;
        doc.CommandEnded += Doc_CommandEnded;
    }
}

// CommandEnded event handler
// Saves the ObjectIds of newly created entites, unsubscribes to CommandEnded
// and subscribes to Application.Idle event (required for SetImpliedSelection)
private void Doc_CommandEnded(object sender, CommandEventArgs e)
{
    var doc = (Document)sender;
    doc.CommandEnded -= Doc_CommandEnded;
    _newEntityIds = GetLastEntities(doc.Database).ToArray();
    if (0 < _newEntityIds.Length)
    {
        Application.Idle += Application_Idle;
    }
}

// Application.Idle event handler
// Selects an grips the newly created entities.
private void Application_Idle(object sender, EventArgs e)
{
    var doc = Application.DocumentManager.MdiActiveDocument;
    var db = doc.Database;
    Application.Idle -= Application_Idle;
    if(_newEntityIds != null)
    {
        using (var docLock = doc.LockDocument())
        {
            using (var tr = db.TransactionManager.StartTransaction())
            {
                foreach (var objId in _newEntityIds)
                {
                    var br = tr.GetObject(objId, OpenMode.ForWrite) as BlockReference;
                    if (br != null)
                    {
                        if (br.AttributeCollection.Contains("NearestCornerBlockName"))
                        {
                            br.AttributeCollection.SetValue("NearestCornerBlockName", "");
                        }
                        if ((br.AttributeCollection.Contains("BlockNumber")))
                        {
                            br.AttributeCollection.SetValue("BlockNumber", "0");
                        }
                    }
                }
                tr.Commit();
            } 
        }
    }
    //uncomment this to select the newly  copied/arrayed/mirrored entities
    //Application.DocumentManager.MdiActiveDocument.Editor.SetImpliedSelection(_newEntityIds);
}

// Returns the ObjectIds of newly created entites since the command started.
private IEnumerable<ObjectId> GetLastEntities(Database db)
{
    using (var tr = new OpenCloseTransaction())
    {
        for (long i = _seed; i < db.Handseed.Value; i++)
        {
            if (db.TryGetObjectId(new Handle(i), out ObjectId id)
                && !id.IsEffectivelyErased
                && id.ObjectClass.IsDerivedFrom(_entityClass))
            {
                var entity = (Entity)tr.GetObject(id, OpenMode.ForWrite);
                if (entity.OwnerId == db.CurrentSpaceId)
                    yield return id;
            }
        }
    }
}

 And then lastly in Terminate():

_dwgManager.DocumentLockModeChanged -= new DocumentLockModeChangedEventHandler(interceptCopyToRemoveInfo);

 

And Voila! If the user copies/arrays/mirrors a blockreference which contains the NearestCornerBlockName and the BlockNumber attributes then the values of both are set to "" values.

 

FWIW: The reason I needed to work this out is because I am setting both those attributes to either 1 or 2 for BlockNumber and any combination of "{System}-{Level}-CR{##}" for NearestCornerBlockName to allow the recreation of a block associated to two (or more?) BlockReferences that, being BlockReferences I am unable to prevent the user from copying.

0 Likes
Message 3 of 8

ActivistInvestor
Mentor
Mentor

After reading your reply, I'm rewriting my comments...

0 Likes
Message 4 of 8

AlexFielder
Advisor
Advisor

Thank you for the advice Tony and the suggestion about using ObjectOverrule instead of the event. By way of expanding on the reason for the need for this that I (again, oops!) didn't really explain in my follow-up post either:

 

I am placing a series of blockreferences along the perimeter of a building, leaving space at either end of each wall for a "corner" block. The placement of each corner hinges upon the user selecting two blocks either side of the corner point (in a counter-clockwise direction) and the corner point itself (technically, since the orientation and insert point of the two selected blocks allows one to calculate the corner point, I don't really need the user to select the corner point but I am not minded to change/reduce the number of clicks by one for each corner at this late-stage in this particular project).

 

As a consequence of needing to have an edit function for both the wall blocks as well as the corner blocks (after a good deal of work around MVVM ViewModels and their respective bindings to the Views I am using for dialogues etc.) I decided that the easiest way to "rebuild" a corner after an edit (without needing to capture in Attributes/Xrecords every last bit of data about the corner) was to use stored information about the corner being edited in the two blockreferences the user originally selected when placing the corner. (Hopefully that makes sense?)

 

That then lead me to the question of: "what if the user has copied one/both of them to another location?" which would of course invalidate my retrieval of said BlockReference ObjectIds (if the copied blocks contained duplicate information relating to the NearestCornerBlockName and BlockNumber attributes) used to drive the initial block creation and subsequently lead me to this point.

 

Even after all these years, I am still learning things.

0 Likes
Message 5 of 8

ActivistInvestor
Mentor
Mentor
Accepted solution

Thanks for finally elaborating your underlying problem. You came seeking advice on how to capture new entities created during specific commands. The problem is that that is not really a very good way to solve the problem (BTW, your solution doesn't account for a number of other ways that a user could copy objects, which would side-step it).

 

Now, if I had known what your actual underlying problem was from the outset (e.g., not how to capture new objects created during certain commands - that is just one hurdle in your solution), I would have suggested ways of doing it, that are far-less convoluted and much more bullet-proof. I don't see anything you've described about the underlying problem would preclude using a more-sound and robust solution:

 

 

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using System;

namespace NotTheHighRoadToChina
{

   public class AttributeReferenceCopyHandler : ObjectOverrule
   {
      static RXClass rxclass = RXObject.GetClass(typeof(AttributeReference));

      public AttributeReferenceCopyHandler()
      {
         AddOverrule(rxclass, this, true);
      }

      public override DBObject DeepClone(DBObject dbObject, DBObject ownerObject, IdMapping idMap, bool isPrimary)
      {
         var result = base.DeepClone(dbObject, ownerObject, idMap, isPrimary);
         AttributeReference copy = result as AttributeReference;
         if(copy != null)
         {
            /// TODO: Use additional conditions to decide if this
            /// AttributeReference's TextString should be cleared,
            /// which could involve any or all of the arguments to
            /// this method.

            /// In this example, the TextString is cleared unconditionally:

            if(!copy.IsWriteEnabled)
               copy.UpgradeOpen();

            copy.TextString = string.Empty;
         }
         return result;
      }

      protected override void Dispose(bool disposing)
      {
         RemoveOverrule(rxclass, this);
         base.Dispose(disposing);
      }
   }
}

 

 

Message 6 of 8

AlexFielder
Advisor
Advisor

Hi @ActivistInvestor, apologies for my delayed reply; this project seems to have a habit of dragging me into a darkened room seemingly for weeks on-end. I have managed to get the OG approach I shared working sufficiently for my current needs. 

 

Were I to use the ObjectOverrule, I can see I could use the OwnerObject to query exactly which block the reference is part of and create some filtering to only empty the string when specific criteria are met.

 

The next (and final) stage of this particular project requires me to do the following and before I go running off to China (as you so eloquently put it) I should take your advice and set out what I wish to achieve:

 

1) filter the drawing to count instances of each block

2) collate the blockreferences where the "IsSystemDefinition" attribute is set to "true"

3) collate the blockreferences where the "IsCorner" attribute is set to "true"

4) use the Xrecord data stored on each blockreference from 1) & 2) to:

   a) create the sheet metal unfolds for each component within the blockreference "definition" (typically a piece of folded steel and several "brackets"

    b) turn each set of unfolded assemblies into a drawing such as this:

2024-03-18 18_02_33-.png

    c) create a manufacturing drawing such as this:

2024-03-18 17_55_09-MD02.pdf and 19 more pages - Personal - Microsoft​ Edge.png

Thanks,

 

Alex.

0 Likes
Message 7 of 8

AlexFielder
Advisor
Advisor

Out of curiousity I decided to try implementing the ObjectOverrule you very kindly shared and, having added the following code inside of my Initialize() method:

 static AttributeReferenceOverrule _attributeReferenceOverrule = null;
public void Initialize()
{
	Document dwg = Application.DocumentManager.MdiActiveDocument;
	ed = dwg.Editor;

	try
	{
		ed.WriteMessage("\nInitializing {0}...", this.GetType().Name);

		if (_attributeReferenceOverrule == null)
		{
			_attributeReferenceOverrule = new AttributeReferenceOverrule();

			ObjectOverrule.AddOverrule(RXObject.GetClass(typeof(AttributeReference)), _attributeReferenceOverrule, true);
			ObjectOverrule.Overruling = true;
			ed.WriteMessage("\nAttributeReferenceOverrule added.");
		}

	}
	catch (System.Exception ex)
	{
		ed.WriteMessage("failed:\n{0}", ex.ToString());
	}
}

and the following to Terminate():

public void Terminate()
{
    if (_attributeReferenceOverrule != null)
    {
        ObjectOverrule.RemoveOverrule(RXObject.GetClass(typeof(AttributeReference)), _attributeReferenceOverrule);
        _attributeReferenceOverrule.Dispose();
        _attributeReferenceOverrule = null;

        ed.WriteMessage("\nAttributeReferenceOverrule removed.");
    }
}

For completeness here is my (slightly) modified ObjectOverrule:

public class AttributeReferenceOverrule : ObjectOverrule
{
    static RXClass rxclass = RXObject.GetClass(typeof(AttributeReference));

    public AttributeReferenceOverrule()
    {
        AddOverrule(rxclass, this, true);
    }

    public override DBObject DeepClone(DBObject dbObject, DBObject ownerObject, IdMapping idMap, bool isPrimary)
    {
        var result = base.DeepClone(dbObject, ownerObject, idMap, isPrimary);
        AttributeReference copy = result as AttributeReference;
        if (copy != null)
        {
            /// TODO: Use additional conditions to decide if this
            /// AttributeReference's TextString should be cleared,
            /// which could involve any or all of the arguments to
            /// this method.

            /// In this example, the TextString is cleared unconditionally:

            if (!copy.IsWriteEnabled)
                copy.UpgradeOpen();

            if (copy.Tag == "NearestCornerBlockName" || copy.Tag == "BlockNumber")
            {
                copy.TextString = string.Empty; 
            }
        }
        return result;
    }

    protected override void Dispose(bool disposing)
    {
        RemoveOverrule(rxclass, this);
        base.Dispose(disposing);
    }
}

 

And it does indeed offer a near-seamless approach to zeroing out specific attributereferences if a block reference is copied and it contains one of the tags listed. 

 

However, I notice that one side-effect (once the Overrule code has loaded) is that none of my CommandMethods are recognised by AutoCAD. If I subsequently close AutoCAD and uncomment the other method I had chosen to use, everything works as-expected.

 

Is there something obvious I am missing to do with the ObjectOverrule?

0 Likes
Message 8 of 8

ActivistInvestor
Mentor
Mentor
Accepted solution

However, I notice that one side-effect (once the Overrule code has loaded) is that none of my CommandMethods are recognised by AutoCAD. If I subsequently close AutoCAD and uncomment the other method I had chosen to use, everything works as-expected.

 

Is there something obvious I am missing to do with the ObjectOverrule?


Commands not being recognized is almost always a symptom of an exception being thrown by code within or called from an IExtensionApplication's Initialize() method. Unfortunately, to this day, the problem of AutoCAD suppressing the exception entirely, has not been fixed (an egregious violation of basic software engineering principles). 

 

You can solve the problem of the exception being suppressed, by doing nothing in the Initialize() except adding a handler to the Application.Idle event, and doing the initialization in the handler of the Idle event:

 

 

public class MyApplication : IExtensionApplication
{
   public void Initialize()
   {
      Application.Idle += InitializeAsync;
   }

   void InitializeAsync()
   {
      if(Application.DocumentManager.MdiActiveDocument != null)
      {
         Application.Idle -= InitializeAsync;
         
         // TODO: Initialize your app here:
      }
   }
   
   public void Terminate()
   {
      // TODO: finalize your application here
   }
}

 

 

You can find a reusable implementation of the pattern shown above here

 

The other problem is that your code is adding and removing the Overrule in your Initialize() and Terminate() methods. Why?  Look at the AttributeReferenceOverrule class. It adds the overrule in the constructor, and removes it in the Dispose() method, so there's no need for a consumer to add/remove it at all.

 

Using it requires nothing more than creating an instance, and disposing the instance when you no longer want the overruling to happen.