Another big thank you to Jeremy Tammik, from our DevTech team in Europe, for providing this elegant sample. This is another one Jeremy presented at the recent advanced custom entity workshop in Prague. I have added some initial commentary as well as some steps to see the code working. Jeremy also provided the code for the last post.
We sometimes want to stop entities from being modified in certain ways, and there are a few different approaches possible, for instance: at the simplest - and least granular - level, we can place entities on locked layers or veto certain commands using an editor reactor. Or we can go all-out and implement custom objects that have complete control over their behaviour. The below technique provides a nice balance between control and simplicity: it makes use of a Document event to check when a particular command is being called, a Database event to cache the information we wish to restore and finally another Document event to restore it. In this case it's all about location (or should I say "location, location, location" ? :-). We're caching an object's state before the MOVE command (which changes an object's position in the model), but if we wanted to roll back the effect of other commands, we would probably want to cache other properties.
Here's the C# code:
using System.Diagnostics;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
namespace Reactor
{
/// <summary>
/// Reactor command.
///
/// Demonstrate a simple object reactor, as well as
/// cascaded event handling.
///
/// In this sample, the MOVE command is cancelled for
/// all red circles. This is achieved by attaching an
/// editor reactor and watching for the MOVE command begin.
/// When triggered, the reactor attaches an object reactor
/// to the database and watches for red circles. If any are
/// detected, their object id and original position are
/// stored. When the command ends, the positions are
/// restored and the object reactor removed again.
///
/// Reactors create overhead, so we should add them only
/// when needed and remove them as soon as possible
/// afterwards.
/// </summary>
public class CmdReactor
{
private static Document _doc;
private static ObjectIdCollection _ids =
new ObjectIdCollection();
private static Point3dCollection _pts =
new Point3dCollection();
[CommandMethod("REACTOR")]
static public void Reactor()
{
_doc =
Application.DocumentManager.MdiActiveDocument;
_doc.CommandWillStart +=
new CommandEventHandler(doc_CommandWillStart);
}
static void doc_CommandWillStart(
object sender,
CommandEventArgs e
)
{
if (e.GlobalCommandName == "MOVE")
{
_ids.Clear();
_pts.Clear();
_doc.Database.ObjectOpenedForModify +=
new ObjectEventHandler(_db_ObjectOpenedForModify);
_doc.CommandCancelled +=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandEnded +=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandFailed +=
new CommandEventHandler(_doc_CommandEnded);
}
}
static void removeEventHandlers()
{
_doc.CommandCancelled -=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandEnded -=
new CommandEventHandler(_doc_CommandEnded);
_doc.CommandFailed -=
new CommandEventHandler(_doc_CommandEnded);
_doc.Database.ObjectOpenedForModify -=
new ObjectEventHandler(_db_ObjectOpenedForModify);
}
static void _doc_CommandEnded(
object sender,
CommandEventArgs e
)
{
// Remove database reactor before restoring positions
removeEventHandlers();
rollbackLocations();
}
static void _db_ObjectOpenedForModify(
object sender,
ObjectEventArgs e
)
{
Circle circle = e.DBObject as Circle;
if (null != circle && 1 == circle.ColorIndex)
{
// In AutoCAD 2007, OpenedForModify is called only
// once by MOVE.
// In 2008, OpenedForModify is called multiple
// times by the MOVE command ... we are only
// interested in the first call, because
// in the second one, the object location
// has already been changed:
if (!_ids.Contains(circle.ObjectId))
{
_ids.Add(circle.ObjectId);
_pts.Add(circle.Center);
}
}
}
static void rollbackLocations()
{
Debug.Assert(
_ids.Count == _pts.Count,
"Expected same number of ids and locations"
);
Transaction t =
_doc.Database.TransactionManager.StartTransaction();
using (t)
{
int i = 0;
foreach (ObjectId id in _ids)
{
Circle circle =
t.GetObject(id, OpenMode.ForWrite) as Circle;
circle.Center = _pts[i++];
}
t.Commit();
}
}
}
}
To see the code at work, draw some circles and make some of them red:
Now run the REACTOR command and try to MOVE all the circles:
Although all the circles are dragged during the move, once we complete the command we can see that the red circles have remained in the same location (or have, in fact, had their location rolled back). The other circles have been moved, as expected.