Kean Walmsley

July 2009

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  

Twitter Updates

    follow me on Twitter



    « "Through the Interface" has a new look | Main | Master list of "Through the Interface" posts »

    August 18, 2008

    Preventing an AutoCAD block from being exploded using .NET

    In response to these recent posts, I received a comment from Nick:

    By any chance would it be possible to provide an example to prevent a user from using the EXPLODE command for a given block name?

    I delved into the ADN knowledgebase and came across this helpful ObjectARX DevNote, which I used to create a .NET module to address the above question.

    Here's the C# code, which should contain enough comments to make it self-explanatory:

    using Autodesk.AutoCAD.ApplicationServices;

    using Autodesk.AutoCAD.DatabaseServices;

    using Autodesk.AutoCAD.EditorInput;

    using Autodesk.AutoCAD.Runtime;


    namespace ExplosionPrevention

    {

      public class Commands

      {

        private Document _doc;

        private Database _db;


        private ObjectIdCollection _blkDefs =

          new ObjectIdCollection();

        private ObjectIdCollection _blkRefs =

          new ObjectIdCollection();

        private ObjectIdCollection _blkConts =

          new ObjectIdCollection();


        private bool _handlers = false;

        private bool _exploding = false;


        [CommandMethod("STOPEX")]

        public void StopBlockFromExploding()

        {

          _doc =

            Application.DocumentManager.MdiActiveDocument;

          _db = _doc.Database;


          if (!_handlers)

          {

            AddEventHandlers();

            _handlers = true;

          }


          // Get the name of the block to protect


          PromptStringOptions pso =

            new PromptStringOptions(

              "\nEnter block name: "

            );

          pso.AllowSpaces = false;

          PromptResult pr =

            _doc.Editor.GetString(pso);


          if (pr.Status != PromptStatus.OK)

            return;


          Transaction tr =

            _db.TransactionManager.StartTransaction();

          using (tr)

          {

            // Make sure the block definition exists


            BlockTable bt =

              (BlockTable)

                tr.GetObject(

                  _db.BlockTableId,

                  OpenMode.ForRead

                );

            if (bt.Has(pr.StringResult))

            {

              // Collect information about the block...


              // 1. the block definition


              ObjectId blkId =

                bt[pr.StringResult];

              _blkDefs.Add(blkId);


              BlockTableRecord btr =

                (BlockTableRecord)

                  tr.GetObject(

                    blkId,

                    OpenMode.ForRead

                  );


              // 2. the block's contents


              foreach (ObjectId id in btr)

                _blkConts.Add(id);


              // 3. the block's references


              ObjectIdCollection blkRefs =

                btr.GetBlockReferenceIds(true, true);

              foreach (ObjectId id in blkRefs)

                _blkRefs.Add(id);

            }

            tr.Commit();

          }

        }


        private void AddEventHandlers()

        {

          // When a block reference is added, we need to

          // check whether it's for a block we care about

          // and add it to the list, if so


          _db.ObjectAppended +=

            delegate(object sender, ObjectEventArgs e)

            {

              BlockReference br =

                e.DBObject as BlockReference;

              if (br != null)

              {

                if (_blkDefs.Contains(br.BlockTableRecord))

                  _blkRefs.Add(br.ObjectId);

              }

            };


          // Conversely we need to remove block references

          // that as they're erased


          _db.ObjectErased +=

            delegate(object sender, ObjectErasedEventArgs e)

            {

              // This is called during as part of the cloning

              // process, so let's check that's not happening


              if (!_exploding)

              {

                BlockReference br =

                  e.DBObject as BlockReference;

                if (br != null)

                {

                  // If we're erasing, remove this block

                  // reference from the list, otherwise if

                  // we're unerasing we will want to add it

                  // back in


                  if (e.Erased)

                  {

                    if (_blkRefs.Contains(br.ObjectId))

                      _blkRefs.Remove(br.ObjectId);

                  }

                  else

                  {

                    if (_blkDefs.Contains(br.BlockTableRecord))

                      _blkRefs.Add(br.ObjectId);

                  }

                }

              }

            };


          // This is where we fool AutoCAD into thinking the

          // block contents have already been cloned


          _db.BeginDeepClone +=

            delegate(object sender, IdMappingEventArgs e)

            {

              // Only for the explode context


              if (e.IdMapping.DeepCloneContext !=

                  DeepCloneType.Explode)

                return;


              // We add IDs to the map to stop the

              // block contents from being cloned


              foreach (ObjectId id in _blkConts)

                e.IdMapping.Add(

                  new IdPair(id, id, true, true, true)

                );

            };


          // And this is where we remove the mapping entries


          _db.BeginDeepCloneTranslation +=

            delegate(object sender, IdMappingEventArgs e)

            {

              // Only for the explode context


              if (e.IdMapping.DeepCloneContext !=

                  DeepCloneType.Explode)

                return;


              // Set the flag for our CommandEnded handler


              _exploding = true;


              // Remove the entries we added on BeginDeepClone


              foreach (ObjectId id in _blkConts)

                e.IdMapping.Delete(id);

            };


          // As the command ends we unerase the block references


          _doc.CommandEnded +=

            delegate(object sender, CommandEventArgs e)

            {

              if (e.GlobalCommandName == "EXPLODE" && _exploding)

              {

                // By this point the block contents should not have

                // been cloned, but the blocks have been erased


                Transaction tr =

                  _db.TransactionManager.StartTransaction();

                using (tr)

                {

                  // So we need to unerase each of the erased

                  // block references


                  foreach (ObjectId id in _blkRefs)

                  {

                    DBObject obj =

                      tr.GetObject(

                        id,

                        OpenMode.ForRead,

                        true

                      );


                    // Only unerase it if it's needed


                    if (obj.IsErased)

                    {

                      obj.UpgradeOpen();

                      obj.Erase(false);

                    }

                  }

                  tr.Commit();

                }

                _exploding = false;

              }

            };

        }

      }

    }

    The STOPEX command takes a block name and then gathers (and stores) information about a block: its ObjectId, the IDs of its contents and its various block references. I've added some logic to handle creation of new block references (e.g. via INSERT), and erasure of ones that are no longer needed. I haven't put anything in to deal with redefinition of blocks (if the contents of blocks change then explosion may not be prevented properly), but this is left as an exercise for the reader.

    Let's define and insert a series of three blocks: LINES, ARCS and CIRCLES (no prizes for guessing which is which :-):

    Inserted blocks

    Now we run the STOPEX command on the LINES and CIRCLES blocks:

    Command: STOPEX

    Enter block name: circles

    Command: STOPEX

    Enter block name: lines

    Command: EXPLODE

    Select objects: all

    9 found

    Select objects:

    Command: Specify opposite corner:

    Selecting the "exploded" blocks, we see that only the ARCS blocks have actually been exploded:

    Exploded blocks

    TrackBack

    TrackBack URL for this entry:
    http://www.typepad.com/services/trackback/6a00d83452464869e200e553f016f08833

    Listed below are links to weblogs that reference Preventing an AutoCAD block from being exploded using .NET:

    Comments

    This example is great. It's possible extend to provide prevention for attribute edition?

    Thanks in advance,

    Genésio

    That would really be addressing a completely different requirement... the sample to roll-back changes (available here) might be a better starting point, but it would still be quite some work.

    Kean

    Hi Kean.

    Another nice example and if you don't mind, a helpful tip:

    Your code is handling events continuously, though it would seem that they only need to be handled while the EXPLODE command is running.

    Jermey's sample in another post (rolling back changes to commands) demonstrates a best practice, which is to only monitor the CommandWillStart event continuously, and begin handling all other relevant events when the command(s) of interest start, and stop handling them when the command ends. Doing that minimizes event overhead, which can be somewhat expensive during 'command-intensive' user scripting.

    Verify your Comment

    Previewing your Comment

    This is only a preview. Your comment has not yet been posted.

    Working...
    Your comment could not be posted. Error type:
    Your comment has been posted. Post another comment

    The letters and numbers you entered did not match the image. Please try again.

    As a final step before posting your comment, enter the letters and numbers you see in the image below. This prevents automated programs from posting comments.

    Having trouble reading this image? View an alternate.

    Working...

    Post a comment

    Feed & Share

    Search