Watching for deletion of a specific AutoCAD block using .NET

I received this question by email from Vito Lee:

I am trying to write an event handler function in C# and can use your expertise. I am trying to display an alert box whenever a user erases a specific block in a drawing. Which event handler would be best for this situation?

This one is interesting, because it's quite a general problem and there are a few ways to solve it. To start with, let's generalise the problem description to cover watching for editing operations on drawing objects. We're indeed going to solve the specific problem stated above – albeit while maintaining a list of block names, rather than a single one, and by sending information to the command-line rather than via a message-box – but this technique can be used for watching for all kinds of editing operations. I could probably have said identifiable drawing objects, but as all drawing-resident objects have – at a minimum – an ObjectId, they are always identifiable. In our case we're going to identify relevant BlockReferences by the name of the BlockTableRecord to which they refer, but that's actually besides the point: we could also maintain a list of ObjectIds to the entities we care about.

The core technique for most solutions to this problem is to attach an event handler to check when objects are modified (in our case erased). The best way – in general – to do this is via a Database notification of some kind: it is certainly possible to use more specific object events (I have also used persistent object reactors from ObjectARX to do this, in the past), but the simplest approach overall is to handle events at the Database level (which in our case means handling Database.ObjectErased()).

Now it's possible to do a fair amount of testing/verification from directly within the ObjectModified()/ObjectErased() notifications, but I tend to prefer to use these events to identify the objects that have been modified/erased. The heavy lifting of analysing the specific properties of the objects I tend to leave until the command has ended (such as during Document.CommandEnded()). This way we can process a list of objects more efficiently, without having to create multiple transactions, etc., but it also avoids potential issues that could arise when attempting to access (although in general this means modify) objects in the drawing database as other objects are being modified.

Here's the C# code I wrote to solve this problem:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using System.Collections.Generic;

 

namespace WatchErasure

{

  public class Commands

  {

    // A list of erased entities, populated during OnErased()

 

    ObjectIdCollection _ids = null;

 

    // A list of blocks to look out for, popultade during AddWatch()

 

    SortedList<string, string> _blockNames = null;

 

    // A command to add a watch for a particular block

 

    [CommandMethod("AW")]

    public void AddWatch()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;

 

      // Start by displaying the watches currently in place

 

      ListBlocksBeingWatched(ed);

 

      // Ask for the name of a block to watch for

 

      PromptStringOptions pso =

        new PromptStringOptions(

          "\nEnter block name to watch: "

        );

      pso.AllowSpaces = true;

 

      PromptResult pr = ed.GetString(pso);

 

      if (pr.Status != PromptStatus.OK)

        return;

 

      // Use all capitals for the block name

 

      string blockName = pr.StringResult.ToUpper();

 

      // If there currently isn't a list of block names,

      // create on, along with the erased entity list

      // Then attach our event handlers

 

      if (_blockNames == null)

      {

        _blockNames = new SortedList<string, string>();

        _ids = new ObjectIdCollection();

 

        db.ObjectErased +=

          new ObjectErasedEventHandler(OnObjectErased);

        doc.CommandEnded +=

          new CommandEventHandler(OnCommandEnded);

      }

 

      // If the list contains our block, no need to add it

 

      if (_blockNames.ContainsKey(blockName))

      {

        ed.WriteMessage(

          "\nAlready watching block \"{0}\".",

          blockName

        );

      }

      else

      {

        // Otherwise add the block name and display the list

 

        _blockNames.Add(blockName, blockName);

 

        ListBlocksBeingWatched(ed);

      }

    }

 

    // A command to stop watching for a particular block

 

    [CommandMethod("RW")]

    public void RemoveWatch()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;

 

      // Start by displaying the watches currently in place

 

      ListBlocksBeingWatched(ed);

 

      // if there are no watches in place, nothing to do

 

      if (_blockNames == null || _blockNames.Count == 0)

        return;

 

      // Ask for the name of a block to stop watching for

 

      PromptStringOptions pso =

        new PromptStringOptions(

          "\nEnter block name to stop watching <All>: "

        );

      pso.AllowSpaces = true;

 

      PromptResult pr = ed.GetString(pso);

 

      if (pr.Status != PromptStatus.OK)

        return;

 

      // Use all capitals for the block name

 

      string blockName = pr.StringResult.ToUpper();

 

      // If a particular block was chosen...

 

      if (blockName != "")

      {

        // Remove it from our list, if it's on it

 

        if (_blockNames.ContainsKey(blockName))

        {

          _blockNames.Remove(blockName);

 

          ed.WriteMessage(

            "\nWatch removed for block \"{0}\".",

            blockName

          );

        }

        else

        {

          ed.WriteMessage(

            "\nNot currently watching a block named \"{0}\".",

            blockName

          );

        }

      }

 

      // If that was the last entry, or we're clearing the list...

 

      if (blockName == "" || _blockNames.Count == 0)

      {

        // Start by asking for confirmation, if we're clearing

 

        if (blockName == "")

        {

          PromptKeywordOptions pko =

            new PromptKeywordOptions(

              "Stop watching all blocks? [Yes/No]: ",

              "Yes No"

            );

 

          pko.Keywords.Default = "No";

 

          pr = ed.GetKeywords(pko);

          if (pr.Status != PromptStatus.OK ||

              pr.StringResult == "No")

          {

            return;

          }

        }

 

        // Now we remove the entity list and set it to null

 

        if (_ids != null)

        {

          _ids.Dispose();

          _ids = null;

        }

 

        // And the same for the list of block names

 

        if (_blockNames != null)

          _blockNames = null;

 

        // And we detach our event handlers

 

        db.ObjectErased -=

          new ObjectErasedEventHandler(OnObjectErased);

        doc.CommandEnded -=

          new CommandEventHandler(OnCommandEnded);

      }

 

      // Finally we report the current state of the watch list

 

      ListBlocksBeingWatched(ed);

    }

 

    // A helper function to list the block names in our list

 

    private void ListBlocksBeingWatched(Editor ed)

    {

      // Start by checking there's something on the list

 

      if (_blockNames == null)

      {

        ed.WriteMessage("\nNot watching any blocks.");

      }

      else

      {

        // If so, loop through and print the names, one by one

 

        ed.WriteMessage("\nWatching blocks: ");

        bool first = true;

        foreach(

          KeyValuePair<string, string> blockName in _blockNames

        )

        {

          ed.WriteMessage(

            "{0}{1}",

            (first ? "" : ", "),

            blockName.Key

          );

          first = false;

        }

        ed.WriteMessage(".");

      }

    }

 

    // A callback for the Database.ObjectErased event

 

    private void OnObjectErased(

      object sender, ObjectErasedEventArgs e

    )

    {

      // Very simple: we just add our ObjectId to the list

      // for later processing

 

      if (e.Erased)

      {

        if (!_ids.Contains(e.DBObject.ObjectId))

          _ids.Add(e.DBObject.ObjectId);

      }

    }

 

    // A callback for the Document.CommandEnded event

 

    private void OnCommandEnded(

      object sender, CommandEventArgs e

    )

    {

      // Start an outer transaction that we pass to our testing

      // function, avoiding the overhead of multiple transactions

 

      Document doc = sender as Document;

      if (_ids != null)

      {

        Transaction tr =

          doc.Database.TransactionManager.StartTransaction();

        using (tr)

        {

          // Test each object, in turn

 

          foreach (ObjectId id in _ids)

          {

            // The test function is responsible for presenting the

            // user with the information: this could be returned to

            // this function, if needed

 

            TestObjectAndShowMessage(doc, tr, id);

          }

 

          // Even though we're only reading, we commit the

          // transaction, as this is more efficient

 

          tr.Commit();

        }

 

        // Now we clear our list of entities

 

        _ids.Clear();

      }

    }

 

    // A function to test for the type of object we're interested in

 

    private void TestObjectAndShowMessage(

      Document doc, Transaction tr, ObjectId id

    )

    {

      // We are looking for blocks of a certain name,

      // although this function could be adapted to

      // watch for any kind of entity

 

      Editor ed = doc.Editor;

 

      // We must remember to pass true for "open erased?"

 

      DBObject obj = tr.GetObject(id, OpenMode.ForRead, true);

      BlockReference br = obj as BlockReference;

      if (br != null)

      {

        // If we have a block reference, get its associated

        // block definition

 

        BlockTableRecord btr =

          (BlockTableRecord)tr.GetObject(

            br.IsDynamicBlock ?

              br.DynamicBlockTableRecord :

              br.BlockTableRecord,

            OpenMode.ForRead

          );

 

        // Check its name against our list

 

        string blockName = btr.Name.ToUpper();

        if (_blockNames.ContainsKey(blockName))

        {

          // Display a message, if it's on it

 

          ed.WriteMessage(

            "\nBlock \"{0}\" erased.",

            blockName

          );

        }

      }

    }

  }

}

Here's what happens when we use the AW and RW commands to add and remove blocks from our list of blocks to watch, and then use the standard ERASE command to delete some blocks we created previously with the names for which we're watching:

Command: AW

Not watching any blocks.

Enter block name to watch: alpha

Watching blocks: ALPHA.

Command: AW

Watching blocks: ALPHA.

Enter block name to watch: beta

Watching blocks: ALPHA, BETA.

Command: AW

Watching blocks: ALPHA, BETA.

Enter block name to watch: gamma

Watching blocks: ALPHA, BETA, GAMMA.

Command: AW

Watching blocks: ALPHA, BETA, GAMMA.

Enter block name to watch: delta

Watching blocks: ALPHA, BETA, DELTA, GAMMA.

Command: AW

Watching blocks: ALPHA, BETA, DELTA, GAMMA.

Enter block name to watch: epsilon

Watching blocks: ALPHA, BETA, DELTA, EPSILON, GAMMA.

Command: AW

Watching blocks: ALPHA, BETA, DELTA, EPSILON, GAMMA.

Enter block name to watch: omega

Watching blocks: ALPHA, BETA, DELTA, EPSILON, GAMMA, OMEGA.

Command: RW

Watching blocks: ALPHA, BETA, DELTA, EPSILON, GAMMA, OMEGA.

Enter block name to stop watching <All>: Fred

Not currently watching a block named "FRED".

Watching blocks: ALPHA, BETA, DELTA, EPSILON, GAMMA, OMEGA.

Command: AW

Watching blocks: ALPHA, BETA, DELTA, EPSILON, GAMMA, OMEGA.

Enter block name to watch: Fred

Watching blocks: ALPHA, BETA, DELTA, EPSILON, FRED, GAMMA, OMEGA.

Command: RW

Watching blocks: ALPHA, BETA, DELTA, EPSILON, FRED, GAMMA, OMEGA.

Enter block name to stop watching <All>: Fred

Watch removed for block "FRED".

Watching blocks: ALPHA, BETA, DELTA, EPSILON, GAMMA, OMEGA.

Command: ERASE

Select objects: ALL

8 found

Select objects:

Block "EPSILON" erased.

Block "OMEGA" erased.

Block "OMEGA" erased.

Block "EPSILON" erased.

Block "DELTA" erased.

Block "GAMMA" erased.

Block "BETA" erased.

Block "ALPHA" erased.

Command: RW

Watching blocks: ALPHA, BETA, DELTA, EPSILON, GAMMA, OMEGA.

Enter block name to stop watching <All>:

Stop watching all blocks? [Yes/No] <No>: Y

Not watching any blocks.

As we can see the application maintains a sorted list of block names to watch: should any block reference be deleted that points to a named block on the list, we print a simple message to the command-line. I've used a slightly non-standard approach during the RW command for selecting the block name: "All" is not actually a keyword, it's just what happens when the user hits return directly. It's possible there's a better way to handle this (perhaps using GetKeywords() rather than GetString()) but this approach seemed reasonable, overall, and also allows the user to watch for a block named "All", should they need to. 🙂

9 responses to “Watching for deletion of a specific AutoCAD block using .NET”

  1. Hi Kean,

    Nice post, thanks. I have a basic coding style question about the process you used to check if the erased object was a block reference. In the TestObjectAndShowMessage function you used TryCast (in VB.NET) to attempt casting the object to a block reference then checking if the new object was nothing:

    BlockReference br = obj as BlockReference;
    if (br != null)

    My question is: Is that a better method (in terms of speed/safety/proper coding/etc) than:

    if (obj is BlockReference)

    and why.

    Thanks in advance for the clarification,
    -Danny

  2. Hi Danny,

    Great question - the answer comes down to a style choice, as much as anything. (For some background to my choice of style, you might check out one of my very early posts to this blog.)

    I could have written:

    if (obj is BlockReference)
    {
    BlockReference br = (BlockReference)obj;
    ...
    }

    But this is ultimately equivalent to using "as", which is a "safe" cast - i.e. it checks the type of the object before casting it. It'd be interesting to check the IL generated for each of the two forms: I suspect they'll be as close to identical as doesn't matter.

    Cheers,

    Kean

  3. Hate to disagree, this is not merely a question of style choice.

    First, just to be clear, we are talking about the choice of this:


    Object o = // assign
    if( o is BlockReference )
    {
    BlockReference blockRef = (BlockReference) o;
    // or
    BlockReference blockRef = o as BlockReference;
    // use blockRef here...
    }

    Verses this:


    Object o = // assign
    BlockReference blockRef = o as BlockReference;
    if( blockRef != null )
    {
    // Use blockRef here
    }

    The first example is inferior because it requires exactly_two_ typecasts (the 'is' operator must do a typecast to type on the right).

    In contrast, the second example requires only a single typecast. If you look at any Microsoft code in Reflector, you will see that when the destunation type is a reference type and the instance is cast to that type and subsequently used, the second pattern shown above is used exclusively, and is the least expensive, and therefore the 'correct' one.

    The 'is' operator and the 'as' operator differ only in that 'is' returns true or false if the cast succeeds, while 'as' returns the successfully cast value. Hence, it is superfluous and wasteful to use both when the object is going to subsequently be cast to the destination type and used.

    It might also be worth pointing out that DBObjects are a special case, mainly because of the ObjectClass property of the ObjectId tells you that, and can be used to determine the concrete type of a DBObject without having to open it and cast it to the destination type.

  4. Fair enough. If that's how the "is" operator works - and I fully admit I haven't looked at the resultant IL, my knowledge is based on ObjectARX's RTTI - then you're right to disagree.

    Kean

  5. Jurica Lovaković Avatar
    Jurica Lovaković

    Hi Kean,

    Just for the sake of completeness, you might want to change the code a bit. If the block being tested is a dynamic one, you should open br.DynamicBlockTableRecord instead of br.BlockTableRecord, because otherwise you'd get the name of the anonymous block.

  6. Hi Jurica,

    Thanks - I often seem to forget Dynamic Blocks in these scenarios (I'm too old school, I guess :-).

    I've gone ahead and added a line of code to enable Dynamic Block support.

    Regards,

    Kean

  7. Hi Kean,

    Very helpful post, but just one question. Using your code above, if one wanted to either prevent the erase from happening, or restore the erased blocks, how would they go about doing this? I hope the answer is a simple 'one liner' and not a topic within itself. 🙂

    Regards,
    Ricky

  8. I Think it might be a simple DBObject.Erase(false) call. I've done the oposite, using Erase(true) to prevent something from being added to the drawing. I assume you could prevent it from being removed in the same way.

  9. Hi Ricky,

    Strange that I didn't see this comment until Chris' reply (thanks, Chris!).

    And yes - Chris is right (at least that's where I'd start).

    Kean

Leave a Reply to Chris Cancel reply

Your email address will not be published. Required fields are marked *