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