A blog for developers programming with Autodesk platforms, particularly AutoCAD and Forge. With a special focus on AR/VR and IoT.

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

{

publicclassCommands

{

// 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")]

publicvoid 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 =

newPromptStringOptions(

"\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 = newSortedList<string, string>();

_ids = newObjectIdCollection();

db.ObjectErased +=

newObjectErasedEventHandler(OnObjectErased);

doc.CommandEnded +=

newCommandEventHandler(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")]

publicvoid 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 =

newPromptStringOptions(

"\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 =

newPromptKeywordOptions(

"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 -=

newObjectErasedEventHandler(OnObjectErased);

doc.CommandEnded -=

newCommandEventHandler(OnCommandEnded);

}

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

ListBlocksBeingWatched(ed);

}

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

privatevoid 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

privatevoid 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

privatevoid 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 asDocument;

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

privatevoid 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 asBlockReference;

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