Kean Walmsley


  • About the Author
    Kean on Google+

July 2014

Sun Mon Tue Wed Thu Fri Sat
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31    








« An entomologist’s view of Project Butterfly | Main | Getting the list of .NET assemblies loaded into AutoCAD from LISP »

February 12, 2010

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. :-)

blog comments powered by Disqus

10 Random Posts