Monday, October 14, 2019

Proceed Or Cancel Entity Erasing By Asking For Confirmation

A recent discussion in Autodesk's .NET user forum raises the question of how to ask user confirmation when entity is to be erased.

AutoCAD has very good "UNDO" mechanism built in from beginning. In majority of cases when entities to be erased from drawing, asking user to confirm the erasing not only unnecessary, but also quite annoying and interrupting. However, there may be cases when asking for confirmation is legitimate and/or desired. For example, if you have some entities associated to many other entities or data (XData, ExtensionDictionary...) via your application, when these entities are to be erased, you might want to do something, or let user know the consequence of losing some data.

The solution of the original poster of the discussion was to ask user's for confirmation after the fact of erasing, and only bring the erased entities back with "UNDO", if the user does not want the erasing. However, I though it is possible to ask for confirmation before the erasing and only proceed with user's confirmation. Of course, asking confirmation should only target specific entities (the less the better). This led me to turn to Overrule, namely ObjectOverrule.

Overrule was made available via API (AutoCAD 2010, I think). Kean Walmsley (unfortunately he does no longer write about AutoCAD programming) posted an article in his famous block Through the Interface on how to prevent AutoCAD objects from being erased. From his article we can see, it is really easy to stop erasing. It should also be easier to allow some conditions being applied so that the overrule can decide whether erasing goes ahead of not. Also, we can take advantage of Overrule's filtering mechanism to narrow down the scope of target entities easily.

So, I decided give ObjectOverrule a try to see what kind of solution and its usability it would lead to. Here is the custom ObjectOverrule class EraseOverrule:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using System;
using System.Collections.Generic;
using System.Linq;
 
namespace OverruledEntityErasing
{
    public enum EraseOverruleMode
    {
        RaiseEvent = 0,
        GatherIds = 1,
        VetoErase = 2,
    }
 
    public class EraseOverrule : ObjectOverrule
    {
        private static EraseOverrule _instance = null;
        private bool _originalOverruling = Overrule.Overruling;
        private IEnumerable<string> _layers = null;
 
        public EraseOverruleMode Mode { setget; } = EraseOverruleMode.RaiseEvent;
        public List<ObjectId> EntitiesToErase { get; } = new List<ObjectId>();
 
        public static EraseOverrule Instance
        {
            get
            {
                if (_instance==null)
                {
                    _instance = new EraseOverrule();
                }
                return _instance;
            }
        }
 
        public event AttemptEraseEventHandler AttemptErase;
        public void Start(IEnumerable<stringlayerFilters)
        {
            _layers = layerFilters;
            EntitiesToErase.Clear();
 
            Overrule.AddOverrule(RXClass.GetClass(typeof(Entity)), thisfalse);
            this.SetCustomFilter();
            Overrule.Overruling = true;
        }
 
        public void Stop()
        {
            Overrule.RemoveOverrule(RXClass.GetClass(typeof(Entity)), this);
            Overrule.Overruling = _originalOverruling;
        }
 
        public override bool IsApplicable(RXObject overruledSubject)
        {
            if (_layers == null || _layers.Count() == 0) return true;
 
            var filtered = FilteredByLayers(overruledSubject as Entity);
            return filtered;
        }
 
        public override void Erase(DBObject dbObjectbool erasing)
        {
            base.Erase(dbObjecterasing);
 
            // If user does "UNDO" to unerase entity back
            // bypass custom overrule process bellow.
            if (!erasingreturn; 
 
            switch (Mode)
            {
                case EraseOverruleMode.VetoErase:
                    throw new Autodesk.AutoCAD.Runtime.Exception(
                        ErrorStatus.NotApplicable);
 
                case EraseOverruleMode.GatherIds:
                    EntitiesToErase.Add(dbObject.ObjectId);
                    throw new Autodesk.AutoCAD.Runtime.Exception(
                        ErrorStatus.NotApplicable);
 
                case EraseOverruleMode.RaiseEvent:
                    var cancel = false;
                    var args = new AttemptEraseEventArgs(dbObject.ObjectId);
                    AttemptErase?.Invoke(thisargs);
                    cancel = args.CancelErasing;
                    if (cancel)
                    {
                        throw new Autodesk.AutoCAD.Runtime.Exception(
                            ErrorStatus.NotApplicable);
                    }
                    break;
            }  
        }
 
        private bool FilteredByLayers(Entity ent)
        {
            var layer = ent.Layer.ToUpper();
            foreach (var l in _layers)
            {
                if (l.ToUpper() == layerreturn true;
            }
            return false;
        }
    }
 
    public class AttemptEraseEventArgs : EventArgs
    {
        public ObjectId EntityId { private setget; }
        public bool CancelErasing { setget; } = false;
        public AttemptEraseEventArgs(ObjectId entId)
        {
            EntityId = entId;
        }
    }
 
    public delegate void AttemptEraseEventHandler(object senderAttemptEraseEventArgs e);
}

The EraseOverrule is derived from ObjectOverrule and overrides its Erase() method. Here are some considerations about this class:

1. I defined an enum type EraseOverruleMode. Each mode results in different behaviour of the overrule (i.e different code logic in overridden Erase() method;

  • EraseOverruleMode.RaiseEvent: an AttemptEraseEventHandler is uased as event, which is raised in overridden Erase() method. This allows external code process, which enables the overrule and subscribes this event to decide if the erasing is to be cancelled or not.;
  • EraseOverruleMode.GatherIds: all erasing to the target entities is cancelled and the ObjectIds of these entities are collected in a collection. The external code process can then test if "erasing attempt" has been applied to some entities. If yes, the code can decide if go ahead with erasing or not. If yes, stop the overrule and do the normal erasing;
  • EraseOverruleMode.VetoErase: this simply veto erasing on target entities.
The code shown later demonstrates how these EraseOverruleModes are used in real AutoCAD erasing process, either when erasing is done with .NET API, or done with AutoCAD built-in commands.

2. Considering EraseOverrule is used either to overrule erasing in .NET API code, or overrule AutoCAD built-in erasing (by "ERASE" command, or other commands that may result in entity being erased), I made it can only be instantiated as singleton object.

3. During debugging, I realized that the Erase() method in the overrule is called both when object is erased and the object is "unerased" by UNDO command. When erased entity is undone, the Erase() method's "bool erasing" argument is false. So, I needed to have this line

if (!erasing) return;

before my overrule logic, which are only needed to handle erasing of target entities.

4. Raising exception in overrule's overridden Erase() method stop erasing works well when the the overrule is used to overrule erasing done by AutoCAD built-in commands, because AutoCAD handles this raised exception in someway. However, if you have .NET API code that does the erasing (i.e. opening an entity for write in a transaction, and call Entity.Erase() method), your code MUST enclose the Entity.Erase() call in a try{...}catch{} block with a blank catch... clause.; or your program would crash AutoCAD because of the exception raised in the overrule. Also, during debugging, the throw new Exception...statement ALWAYS break the debugging run, you can hit F5 to let the debugging run continue ONLY if your code that calls Entity.Erase() has try{...}catch{} wrapped (i.e. the exception is handled).

5. For simplicity, I use Layers as the filter for the EraseOverrule. That means when EraseOverrule is in effect, entities on certain Layers are protected from being erased without being confirmed. It is very easy to code EraseOverrule to filter target objects differently based on needs.

Now move on to see how to use EraseOverrule in 2 different situations: using it with custom erasing process built with AutoCAD .NET API (i.e. doing erasing with our own code in conjunction with this overrule); or using it with AutoCAD built-in erasing process (i.e. ding erasing with AutoCAD built-in commands while the overrule is in effect).

I defined class SpecialEraser that uses EraseOverrule to govern the coded erasing, so that target entities by EraseOverrule would only be erased with user's confirmation. Here is its code:

using Autodesk.AutoCAD.DatabaseServices;
using System.Collections.Generic;
using System.Linq;
 
namespace OverruledEntityErasing
{
    public class SpecialEraser
    {
        private List<ObjectId> _entitiesNotErased = new List<ObjectId>();
        private int _erasedCount  = 0;
        private int _notErasedCount = 0;
        private string[] _protectedLayers = null;
        public SpecialEraser(string[] protectedLayers)
        {
            _protectedLayers = protectedLayers;
        }
 
        #region public methods
 
        public void ConfirmedEraseOneByOne(ObjectId[] entIds)
        {
            if (entIds.Length == 0) return;
            _erasedCount = 0;
            _notErasedCount = 0;
 
            EraseOverrule.Instance.Mode = EraseOverruleMode.RaiseEvent;
            EraseOverrule.Instance.AttemptErase += AttemptIndividualEraseHandler;
            EraseOverrule.Instance.Start(_protectedLayers);
 
            // when each entity is to be erased, if the entity is the overrule's 
            // target, the event handler allow a chance for user to confirm, 
            // in which user can cancel the erasing
            try
            {
                DoErase(entIds);
            }
            finally
            {
                EraseOverrule.Instance.AttemptErase -= AttemptIndividualEraseHandler;
                EraseOverrule.Instance.Stop();
            }
 
            var normalErased = entIds.Length - (_erasedCount + _notErasedCount);
            var msg = 
                $"Normally erased count: {normalErased}\n" +
                $"Confirmedly erased count: {_erasedCount}\n" +
                $"Confirmedly not erased count: {_notErasedCount}";
            MsgBox.ShowInfo(msg);
        }
 
        public void ConfirmedEraseInBatch(ObjectId[] entIds)
        {
            if (entIds.Length == 0) return;
            _entitiesNotErased.Clear();
 
            EraseOverrule.Instance.Mode = EraseOverruleMode.RaiseEvent;
            EraseOverrule.Instance.AttemptErase += AttemptBatchEraseHandler;
            EraseOverrule.Instance.Start(_protectedLayers);
 
            // Do the erasing, if the entity is the overrule's target, the erasing
            // will be all denied, and the entity's objectId is collected for 
            // later to be confirned for real erasing  
            try
            {  
                DoErase(entIds);
            }
            finally
            {
                EraseOverrule.Instance.AttemptErase -= AttemptBatchEraseHandler;
                EraseOverrule.Instance.Stop();
            }
 
            // do actual erasing here at once
            if (_entitiesNotErased.Count>0)
            {
                var erased = entIds.Length - _entitiesNotErased.Count;
                var msg = 
                    $"Erased entity count: {erased}\n" +
                    $"Erase-protected entity count: {_entitiesNotErased.Count}" +
                    "\n\nDo you really want to erase protected entities?";
 
                if (MsgBox.ShowYesNo(msg)
                    == System.Windows.Forms.DialogResult.Yes)
                {
                    DoErase(_entitiesNotErased);
                }
            }
        }
 
        public void ProtectedErase(ObjectId[] entIds)
        {
            if (entIds.Length == 0) return;
 
            EraseOverrule.Instance.Mode = EraseOverruleMode.VetoErase;
            EraseOverrule.Instance.Start(_protectedLayers);
 
            // Do the erasing, if the entity is the overrule's target,
            // erasing is vetoed.
            try
            {
                DoErase(entIds);
            }
            finally
            {
                EraseOverrule.Instance.Stop();
            }
 
            int count = (from id in entIds where !id.IsErased select id).Count();
            var msg = 
                $"{count} out of {entIds.Length} " +
                $"entit{(entIds.Length > 1 ? "ies" : "y")} is proected, thus not erased.";
            MsgBox.ShowInfo(msg);
        }
 
        public List<ObjectIdProtectedEraseWithEntityIds(ObjectId[] entIds)
        {
            if (entIds.Length == 0) return new List<ObjectId>();
 
            List<ObjectIdids = null;
 
            EraseOverrule.Instance.Mode = EraseOverruleMode.GatherIds;
            EraseOverrule.Instance.Start(_protectedLayers);
 
            // Do the erasing, if the entity is the overrule's target,
            // erasing is vetoed, and entity's Id is collected
            try
            {
                DoErase(entIds);
                ids = EraseOverrule.Instance.EntitiesToErase;
            }
            finally
            {
                EraseOverrule.Instance.Stop();
            }
 
            return ids;
        }
 
        public static void DoErase(IEnumerable<ObjectIdentIds)
        {
            using (var tran = 
                entIds.First().Database.TransactionManager.StartTransaction())
            {
                foreach (var id in entIds)
                {
                    var ent = tran.GetObject(idOpenMode.ForWrite);
                    try
                    {
                        ent.Erase();
                    }
                    catch { }
                }
                tran.Commit();
            }
        }
 
        #endregion
 
        #region private methods
 
        private void AttemptIndividualEraseHandler(object senderAttemptEraseEventArgs e)
        {
            var entType = e.EntityId.ObjectClass.DxfName;
            var msg = $"Entity to be erased: {entType}\n\n" +
                "Do you want to erase it?";
 
            if (MsgBox.ShowYesNo(msg) == System.Windows.Forms.DialogResult.No)
            {
                e.CancelErasing = true;
                _notErasedCount++;
            }
            else
            {
                _erasedCount++;
            }
        }
 
        private void AttemptBatchEraseHandler(object senderAttemptEraseEventArgs e)
        {
            _entitiesNotErased.Add(e.EntityId);
            e.CancelErasing = true;
        }
 
        #endregion
    }
}


The code in SpecialEraser class is fairly self-explanatory. Here is the CommandClass that actually uses the SpecialEraser to do the erasing by asking user to confirm of erasing entities "protected by" EraseOverrule; or simply enables EraseOverrule against erasing done by AutoCAD's built-in commands, where CommandEnded event is handled to determine if there was an attempt made to erase entities "protected by" EraseOverrule, if yes, the real erasing only occurs after user's confirmation. Here is the CommandClass code:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(OverruledEntityErasing.MyCommands))]
 
namespace OverruledEntityErasing
{
    public class MyCommands
    {
        private static bool _overruleOn = false;
        private static string[] _protectedLayers = new string[] { "MyLayer1""MyLayer2" };
 
        #region Command methods: erasing entities with "SpecialEraser" class
 
        [CommandMethod("ConfirmedEraseInBatch"CommandFlags.UsePickSet)]
        public static void DoConfirmedEraseInBatch()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var entIds = SelectEntities(ed);
            if (entIds == nullreturn;
 
            _overruleOn = false;
            var eraser = new SpecialEraser(_protectedLayers);
            eraser.ConfirmedEraseInBatch(entIds);
        }
 
        [CommandMethod("ConfirmedEraseOneByOne"CommandFlags.UsePickSet)]
        public static void DoConfirmedEraseOneByOne()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var entIds = SelectEntities(ed);
            if (entIds == nullreturn;
 
            _overruleOn = false;
            var eraser = new SpecialEraser(_protectedLayers);
            eraser.ConfirmedEraseOneByOne(entIds);
        }
 
        [CommandMethod("ProtectedErase"CommandFlags.UsePickSet)]
        public static void DoProtectedErase()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var entIds = SelectEntities(ed);
            if (entIds == nullreturn;
 
            _overruleOn = false;
            var eraser = new SpecialEraser(_protectedLayers);
            eraser.ProtectedErase(entIds);
        }
 
        [CommandMethod("ProtectedEraseWithIds")]
        public static void DoProtectedEraseWithEntityIdsReturned()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var entIds = SelectEntities(ed);
            if (entIds == nullreturn;
 
            _overruleOn = false;
 
            var eraser = new SpecialEraser(_protectedLayers);
            var ids = eraser.ProtectedEraseWithEntityIds(entIds);
            if (ids.Count > 0)
            {
                var msg = 
                    $"{ids.Count} entit{(ids.Count > 1 ? "ies" : "y")} " +
                    $"{(ids.Count > 1 ? "is" : "are")} protected from erasing." +
                    "\n\nDo you really want to erase?";
                if (MsgBox.ShowYesNo(msg) ==
                    System.Windows.Forms.DialogResult.Yes)
                {
                    SpecialEraser.DoErase(ids);
                }
            }
        }
 
        #endregion
 
        #region CommandMethods: Turn on/off EraseOverrule against AutoCAD built-in erasing
 
        [CommandMethod("EraseOverrule")]
        public static void EnableEraseOverrule()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            if (!_overruleOn)
            {
                TurnOnEraseOverrule();
                _overruleOn = true;
                ed.WriteMessage("\nEraseOverule is turned on during command execution.\n");
            }
            else
            {
                TurnOffEraseOverrule();
                _overruleOn = false;
                ed.WriteMessage("\nEraseOverule is turned off.\n");
            }
        }
 
        #endregion
 
        #region private methods
 
        private static ObjectId[] SelectEntities(Editor ed)
        {
            var res = ed.GetSelection();
 
            if (res.Status != PromptStatus.OK)
            {
                ed.WriteMessage("\n*Cancel*\n");
                return null;
            }
            else
            {
                return res.Value.GetObjectIds();
            }
        }
 
        private static void TurnOnEraseOverrule()
        {
            EraseOverrule.Instance.Mode = EraseOverruleMode.GatherIds;
            EraseOverrule.Instance.Start(_protectedLayers);
 
            foreach (Document dwg in CadApp.DocumentManager)
            {
                dwg.CommandEnded += CommandEndedHandler;
            }
 
            CadApp.DocumentManager.DocumentCreated += (oe) =>
              {
                  e.Document.CommandEnded += CommandEndedHandler;
              };
 
            CadApp.DocumentManager.DocumentToBeDestroyed += (oe) =>
              {
                  e.Document.CommandEnded -= CommandEndedHandler;
              };
        }
 
        private static void CommandEndedHandler(object senderCommandEventArgs e)
        {
            if (!_overruleOn) return;
 
            var ids = EraseOverrule.Instance.EntitiesToErase;
            if (ids.Count > 0)
            {
                var cmd = e.GlobalCommandName;
                var msg = $"The command \"{cmd}\" attempted to erase {ids.Count} " +
                    $"protected entit{(ids.Count > 1 ? "ies" : "y")}.\n\n" +
                    "Do you really want to erase?";
 
                if (MsgBox.ShowYesNo(msg) == System.Windows.Forms.DialogResult.Yes)
                {
                    try
                    {
                        EraseOverrule.Instance.Stop();
 
                        var dwg = CadApp.DocumentManager.MdiActiveDocument;
                        using (dwg.LockDocument())
                        {
                            SpecialEraser.DoErase(ids);
                        }
                    }
                    finally
                    {
                        EraseOverrule.Instance.Start(_protectedLayers);
                    }
                }
 
                EraseOverrule.Instance.EntitiesToErase.Clear();
            }
        }
 
        private static void TurnOffEraseOverrule()
        {
            foreach (Document dwg in CadApp.DocumentManager)
            {
                dwg.CommandEnded -= CommandEndedHandler;
            }
 
            EraseOverrule.Instance.Stop();
        }
 
        #endregion
    }
}


As usual, following video clips shows how the code works. For simplicity, EraseOverrule is designed to only target entities on specific layers, in this video demo the layers are "MyLayer1" and "MyLayer2" with layer color as Yellow and Cyan. So, the 4 circles of Yellow/Cyan are the "protected entities, and can only be erased after user's confirmation, while other entities can be erased as usual. Each clip is corresponding to a command:

Clip 0 shows the drawing setup: 4 circles on "MyLayer1" and "MyLayer2", thus are "protected" by EraseOverrule, while other entities can be freely erased;
Clip 1 showing command "ConfirmedEraseInBatch";
Clip 2 showing command "ConfirmedEraseOneByOne";
Clip 3 showing command "ProtectedErase";
Clip 4 showing command "ProtectedEraseWithIds".

Review the code of each command before watching the corresponding video clip.

Again, while asking confirmation before erasing entities (or making other changes to entities, for that matter) is possible/doable, I'd be very careful of doing it and always try to avoid it unless it is a must-do.

The source code of entire project can be downloaded here, which is Visual Studio 2019/C# project against .NET Framework 4.8 and AutoCAD 2020 (I also noticed, even the DLL is compiled against AutoCAD 2020 .NET Assemblies, I have no problem NETLOAD the DLL into AutoCAD 2019 and run it).









Friday, August 23, 2019

Selecting Multiple Nested Entities - 2 of 2

This the second post on the topic of selecting multiple nested entities (from a block reference). The first one is here, in which I demonstrated how to select multiple nested entities by one mouse click at a time. In this article, I show how to do a window-selecting, following AutoCAD's window-selecting convention: if selecting window is picked from left to right, one entities entirely enclosed inside the window would be selected, while window is picked from right to left, not only entities being enclosed inside the window, but also entities being crossed by the window would all be selected.

When I worked on code for this article, to create a new class MultiNestedEntSelector, I saw there is a lots of common code that could be shared with the class NestedEntSelector in previous article, so I decide to create a base class and derive these 2 classes on top of it. At the end of this article, I also re-post the updated NestedEntSelector class used in previous article.

First, the base class, which is an abstract class, NestedEntSeelctorBase:

using System;
 
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
 
namespace SelectMultiNestedEnts
{
    public abstract class NestedEntSelectorBase : IDisposable
    {
        protected Editor Editor { setget; }
        protected bool Highlight { setget; }
        protected int HighlightColorIndex { setget; }
        protected TransientManager TransientManager
        {
            get
            {
                return TransientManager.CurrentTransientManager;
            }
        }
 
        public DBObjectCollection SelectedClones { setget; }
 
        public abstract void SelectNestedEntities(
            bool highlight = trueint highlightColorIndex = 1);
 
        public void CleanUpClonedEntities()
        {
            if (Highlight)
            {
                foreach (DBObject obj in SelectedClones)
                {
                    this.TransientManager.EraseTransient(
                        obj as Drawablenew IntegerCollection());
                }
            }
 
            foreach (DBObject obj in SelectedClones)
            {
                // dispose the cloned entities
                // if it is not added into database
                if (obj.ObjectId.IsNull) obj.Dispose();
            }
 
            SelectedClones.Clear();
            SelectedClones.Dispose();
            SelectedClones = null;
        }
 
        public void Dispose()
        {
            CleanUpClonedEntities();
        }
    }
}

Now, something on the class for selecting multiple nested entities in a block reference MultiNestedEntSelector:

1. To make things simple, the code makes sure only 1 BlockReference is selected by the selecting window.

2. The code need to determine if an entity is inside of a selecting window, or is crossed by the selecting window. This is one of very common AutoCAD programming tasks. I believe many of us programmers have done it many times and likely have our own favorite, re-usable algorithm/code ready available. So I decide to design the class' contructor to take a Func to allow the existing/favorite code of doing "is-inside"/"is-crossing" to be injected. Of course for the completion of this article, I have my simplified "is-inside"/"is-crossing" code in a separate class Helper.

3. In order to see if entity is crossed by the selecting window, I create a closed Polyline (a rectangle), and use Polyline.InteractionWith().

4. I always test if "Is-crossing" first. If an entity is not crossed by the selecting window, it would either entirely outside the window, or entirely inside. Being entirely inside the window means any point on the entity must be inside the window. So, the "is-inside" testing, after "is-crossing" test, becomes get a point from the entity, and test if the point is inside a closed polyline. I used MPolygon for testing if a point is inside. As for getting a point from an entity, it would depend what the entity is. For any curve type, I use "StartPoint"; for DBText, I use AlignmentPoint; for MText, I use Location, which is upper left corner of MText.. For the sake of simplicity, I omitted other types of entity.

Enough explanations. Here is the class MultiNestedEntSelector:

using System;
using System.Collections.Generic;
 
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
 
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace SelectMultiNestedEnts
{
    public enum WindowSelectionState
    {
        Crossing = 0,
        Inside = 1,
        Outside = 2
    }
 
    public class MultiNestedEntSelector : NestedEntSelectorBase
    {
        private bool _crossingSelection = false;
        private Point3dCollection _window = null;
        private ObjectId _blockId = ObjectId.Null;
        private Func<EntityPoint2dCollectionWindowSelectionState> _winSelectionStateFunction = null;
 
        public MultiNestedEntSelector(
            Func<EntityPoint2dCollectionWindowSelectionStatewinSelectionStateFunction)
        {
            _winSelectionStateFunction = winSelectionStateFunction;
        }
 
        public override void SelectNestedEntities(
            bool highlight = trueint highlightColorIndex = 1)
        {
            Editor = CadApp.DocumentManager.MdiActiveDocument.Editor;
            SelectedClones = null;
 
            if (!GetSelectionWindowAndBlockReference())
            {
                Editor.WriteMessage("\nCancel*");
                return;
            }
 
            Highlight = highlight;
            HighlightColorIndex = highlightColorIndex;
 
            var clones = FindEntitiesByWindow();
            if (clones.Count > 0)
            {
                SelectedClones = new DBObjectCollection();
                foreach (var clone in clones)
                {
                    SelectedClones.Add(clone);
                }
            }
            clones.Clear();
 
            if (SelectedClones !=null && SelectedClones.Count>0)
            {
                if (Highlight)
                {
                    HighlightSelelcted();
                }
            }
        }
 
        #region private methods
 
        private void HighlightSelelcted()
        {
            foreach (Entity ent in SelectedClones)
            {
                this.TransientManager.AddTransient(
                    entTransientDrawingMode.Highlight, 128, new IntegerCollection());
            }
        }
 
        private bool GetSelectionWindowAndBlockReference()
        {
            bool done = false;
 
            // Make sure the selecting window only covers 1 block reference
            ObjectId blkId = ObjectId.Null;
            Point3dCollection window = null;
            bool isCrossing = false;
 
            while (blkId.IsNull)
            {
                if (!PickSelectionWindow(out windowout isCrossing))
                {
                    Editor.WriteMessage("\nCancel*");
                    break;
                }
 
                _window = window;
                _crossingSelection = isCrossing;
 
                blkId = IsBlockReferenceSelected(_window);
                if (blkId.IsNull)
                {
                    var kOpt = new PromptKeywordOptions(
                        "\nInvalid selecting window: no block or too many blocks covered.");
                    kOpt.AppendKeywordsToMessage = true;
                    kOpt.Keywords.Add("Window");
                    kOpt.Keywords.Add("Cancel");
                    kOpt.Keywords.Default = "Window";
 
                    var res = Editor.GetKeywords(kOpt);
                    if (res.Status== PromptStatus.OK)
                    {
                        if (res.StringResult != "Window")
                        {
                            break;
                        }
                    }
                    else
                    {
                        break;
                    }
                }
                else
                {
                    done = true;
                    break;
                }
            }
 
            if (done)
            {
                _blockId = blkId;
            }
 
            return done;
        }
 
        private bool PickSelectionWindow(
            out Point3dCollection windowPointsout bool crossingSelection)
        {
            bool picked = false;
 
            windowPoints = null;
            crossingSelection = false;
 
            var pRes = Editor.GetPoint("\nSelect first corner of selecting window:");
            if (pRes.Status== PromptStatus.OK)
            {
                var cOpt = new PromptCornerOptions(
                    "\nSelect a corner of picking window:"pRes.Value);
                cOpt.UseDashedLine = true;
                var cRes = Editor.GetCorner(cOpt);
                if (cRes.Status== PromptStatus.OK)
                {
                    SetSelectionWindow(
                        pRes.Value, cRes.Value, out windowPointsout crossingSelection);
                    picked = true;
                }
            }
 
            return picked;
        }
 
        private void SetSelectionWindow(
            Point3d firstPtPoint3d secondPtout Point3dCollection windowout bool isCrossing)
        {
            var pts = new Point3d[]
            {
                new Point3d(firstPt.X,firstPt.Y,0.0).TransformBy(Editor.CurrentUserCoordinateSystem.Inverse()),
                new Point3d(firstPt.X,secondPt.Y,0.0).TransformBy(Editor.CurrentUserCoordinateSystem.Inverse()),
                new Point3d(secondPt.X,secondPt.Y,0.0).TransformBy(Editor.CurrentUserCoordinateSystem.Inverse()),
                new Point3d(secondPt.X,firstPt.Y,0.0).TransformBy(Editor.CurrentUserCoordinateSystem.Inverse())
            };
 
            window = new Point3dCollection(pts);
            isCrossing = firstPt.X > secondPt.X;
        }
 
        private ObjectId IsBlockReferenceSelected(Point3dCollection selectWin)
        {
            var blkId = ObjectId.Null;
 
            var filter = new SelectionFilter(new TypedValue[] { new TypedValue((int)DxfCode.Start, "INSERT") });
            var res = Editor.SelectCrossingWindow(_window[0], _window[2], filter);
 
            if (res.Status== PromptStatus.OK)
            {
                if (res.Value.Count == 1)
                {
                    blkId = res.Value[0].ObjectId;
                }
            }
 
            return blkId;
        }
 
        private List<EntityFindEntitiesByWindow()
        {
            var ents = new List<Entity>();
 
            using (var tran = _blockId.Database.TransactionManager.StartTransaction())
            {
                var bref = (BlockReference)tran.GetObject(_blockId, OpenMode.ForRead);
                var bdef = (BlockTableRecord)tran.GetObject(bref.BlockTableRecord, OpenMode.ForRead);
 
                // Clone all entities in the block definition, except
                // AttributeDefinition that is not constant
                foreach (ObjectId entId in bdef)
                {
                    var ent = (Entity)tran.GetObject(entIdOpenMode.ForRead);
 
                    bool skip = false;
                    if (ent is AttributeDefinition)
                    {
                        var att = ent as AttributeDefinition;
                        if (!att.Constant) skip = true;
                    }
                    if (skipcontinue;
 
                    var clone = ((Entity)ent.Clone());
                    clone.TransformBy(bref.BlockTransform);
 
                    if (IsSelectedByWindow(clone))
                    {
                        clone.ColorIndex = HighlightColorIndex;
                        ents.Add(clone);
                    }
                    else
                    {
                        clone.Dispose();
                    }
                }
 
                // Clone all AttributeReference iin the block reference
                foreach (ObjectId id in bref.AttributeCollection)
                {
                    var att = (AttributeReference)tran.GetObject(idOpenMode.ForRead);
                    if (!att.Invisible && att.Visible)
                    {
                        if (IsSelectedByWindow(att))
                        {
                            var clone = ((Entity)att.Clone());
                            clone.ColorIndex = HighlightColorIndex;
 
                            ents.Add(clone);
                        }
                    }
                }
 
                tran.Commit();
            }
 
            return ents;
        }
 
        private bool IsSelectedByWindow(Entity ent)
        {
            var pts = new Point2dCollection();
            foreach (Point3d p in _window)
            {
                pts.Add(new Point2d(p.X, p.Y));
            }
 
            var selectionState = _winSelectionStateFunction(entpts);
            if (_crossingSelection)
            {
                return selectionState != WindowSelectionState.Outside;
            }
            else if (!_crossingSelection)
            {
                return selectionState == WindowSelectionState.Inside;
            }
 
            return false;
        }
 
        #endregion
    }
}

Here is the my "Is-inside"/"is-crossing" code in class Helper that is injected into the MultiNestedEntSelector class:

using System;
 
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using CadDb = Autodesk.AutoCAD.DatabaseServices;
 
namespace SelectMultiNestedEnts
{
    public class Helper
    {
        public static WindowSelectionState GetWindowSelectionState(
            Entity entityPoint2dCollection window)
        {
            var state = WindowSelectionState.Outside;
 
            using (var poly = CreatePolyline(window))
            {
                var interPoints = new Point3dCollection();
 
                poly.IntersectWith(
                    entityIntersect.ExtendThis, interPointsIntPtr.Zero, IntPtr.Zero);
                if (interPoints.Count>0)
                {
                    state = WindowSelectionState.Crossing;
                }
                else
                {
                    if (IsInsideWindow(polyentity))
                    {
                        state = WindowSelectionState.Inside;
                    }
                }
            }
 
            return state;
        }
 
 
        #region private methods
 
        private static CadDb.Polyline CreatePolyline(Point2dCollection points)
        {
            var poly = new Autodesk.AutoCAD.DatabaseServices.Polyline(points.Count);
            for (int i=0; i < points.Count; i++)
            {
                poly.AddVertexAt(inew Point2d(points[i].X, points[i].Y), 0.0, 0.0, 0.0);
            }
 
            poly.Closed = true;
 
            return poly;
        }
 
        private static bool IsInsideWindow(CadDb.Polyline polyEntity entity)
        {
           if (GetPointFromEntity(entityout Point2d point))
            {
                return IsPointInside(polypoint);
            }
 
            return false;
        }
 
        private static bool GetPointFromEntity(Entity entityout Point2d point)
        {
            point = Point2d.Origin;
 
            if (entity is CadDb.Curve)
            {
                var pt = ((Curve)entity).StartPoint;
 
                point = new Point2d(pt.X, pt.Y);
                return true;
            }
            else if (entity is DBText)
            {
                var pt = ((DBText)entity).AlignmentPoint;
 
                point = new Point2d(pt.X, pt.Y);
                return true;
            }
            else if (entity is MText)
            {
                var pt = ((MText)entity).Location;
 
                point = new Point2d(pt.X, pt.Y);
                return true;
            }
            else if (entity is DBPoint)
            {
                var pt = ((DBPoint)entity).Position;
 
                point = new Point2d(pt.X, pt.Y);
                return true;
            }
 
            // for the simplicity, I ignore other possible
            // entity types, such as BlockReference, Hatch...
 
            return false;
        }
 
        private static bool IsPointInside(CadDb.Polyline polyPoint2d point)
        {
            var pts = new Point2dCollection();
            for (int i=0; i < poly.NumberOfVertices; i++)
            {
                pts.Add(poly.GetPoint2dAt(i));
            }
 
            var inside = IsInside(pointpts);
            return inside;
        }
 
        private static bool IsInside(
            Point2d pointPoint2dCollection polygonPointsdouble tolerance = 0.001)
        {
            bool inside = false;
 
            if (polygonPoints.Count > 2)
            {
                var poly = new CadDb.Polyline(polygonPoints.Count);
                for (int i = 0; i < polygonPoints.Count; i++)
                {
                    poly.AddVertexAt(ipolygonPoints[i], 0.0, 0.0, 0.0);
                }
                poly.Closed = true;
 
                using (poly)
                {
                    using (var polygon = new MPolygon())
                    {
                        polygon.AppendLoopFromBoundary(polyfalsetolerance);
 
                        inside = polygon.IsPointInsideMPolygon(
                            new Point3d(point.X, point.Y, 0.0), tolerance).Count == 1;
                    }
                }
            }
 
            return inside;
        }
 
        #endregion
    }
}


Here is the updated class NestedEntSelector used in previous article, which is now derived from NestedEntSelectorBase:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace SelectMultiNestedEnts
{
    public class NestedEntSelector : NestedEntSelectorBase
    {
        public override void SelectNestedEntities(
            bool highlight=trueint highlightColorIndex=1)
        {
            Editor = CadApp.DocumentManager.MdiActiveDocument.Editor;
            SelectedClones = new DBObjectCollection();
 
            int count = 0;
            Highlight = highlight;
            HighlightColorIndex = highlightColorIndex;
 
            while (true)
            {
                var msg = $"Select a nested entity in a block ({count} selected):";
                if (SelectNestedEntity(msgcount == 0, out Entity ent))
                {
                    if (ent != null)
                    {
                        if (Highlight)
                        {
                            TransientManager.AddTransient(
                                ent, 
                                TransientDrawingMode.Highlight, 
                                128, 
                                new IntegerCollection());
                        }
                        SelectedClones.Add(ent);
 
                        count++;
                    }
                    else
                    {
                        return;
                    }
                }
                else
                {
                    if (SelectedClones.Count>0)
                    {
                        CleanUpClonedEntities();
                    }
 
                    return;
                }
            }
        }
 
        #region private methods: using Editor.GetNestedEntity()
 
        private bool SelectNestedEntity(
            string msgbool isFirstPickout Entity nestedEntity)
        {
            nestedEntity = null;
            var oked = false;
 
            var opt = new PromptNestedEntityOptions($"\n{msg}:");
            
            opt.AllowNone = true;
            opt.AppendKeywordsToMessage = true;
            if (!isFirstPick)
            {
                opt.Keywords.Add("Done");
                opt.Keywords.Add("Cancel");
                opt.Keywords.Default = "Done";
            }
            else
            {
                opt.Keywords.Add("Cancel");
                opt.Keywords.Default = "Cancel";
            }
 
            var res = this.Editor.GetNestedEntity(opt);
            if (res.Status== PromptStatus.OK || res.Status== PromptStatus.Keyword)
            {
                if (res.Status== PromptStatus.OK)
                {
                    var entId = res.ObjectId;
                    using (var tran = 
                        entId.Database.TransactionManager.StartTransaction())
                    {
                        var ent = (Entity)tran.GetObject(entIdOpenMode.ForRead);
                        var clone = ent.Clone() as Entity;
                        if (entId.ObjectClass.DxfName.ToUpper() != "ATTRIB")
                        {
                            var ids = res.GetContainers();
                            if (ids.Length>0)
                            {
                                var bref = (BlockReference)tran.GetObject(
                                    ids[0], OpenMode.ForRead);
                                clone.TransformBy(bref.BlockTransform);
                            }
                        }
 
                        nestedEntity = clone;
                        nestedEntity.ColorIndex = HighlightColorIndex;
 
                        tran.Commit();
                    }
 
                    oked = true;
                }
                else
                {
                    if (res.StringResult == "Done"oked = true;
                }
            }
 
            return oked;
        }
 
        #endregion
    }
}

The CommandClass to actually do the nested entity selecting work:

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(SelectMultiNestedEnts.MyCommands))]
 
namespace SelectMultiNestedEnts
{
    public class MyCommands
    {
        [CommandMethod("GetNested")]
        public static void RunMyCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                using (var selector = new NestedEntSelector())
                {
                    selector.SelectNestedEntities(true);
                    if (selector.SelectedClones==null)
                    {
                        ed.WriteMessage("\n*Cancel*");
                    }
                    else
                    {
                        // Now that the cloned entities, at the place as selected,
                        // are available for the calling code to do whatever needed
                        // here: adding to database, or only being used as visual hints
                        ed.WriteMessage($"\n{selector.SelectedClones.Count} selected.");
                    }
                    
                    ed.GetString("\nPress Enter to continue...");
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nError:\n{ex.Message}\n");
            }
 
            Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
        }
 
        [CommandMethod("MultiNested")]
        public static void DoMultiNestedEntitySelection()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                using (var selector = new MultiNestedEntSelector(
                    Helper.GetWindowSelectionState))
                {
                    selector.SelectNestedEntities();
                    if (selector.SelectedClones == null)
                    {
                        ed.WriteMessage("\n*Cancel*");
                    }
                    else
                    {
                        // Now that the cloned entities, at the place as selected,
                        // are available for the calling code to do whatever needed
                        // here: adding to database, or only being used as visual hints
                        ed.WriteMessage($"\n{selector.SelectedClones.Count} selected.");
                        ed.GetString("\nPress Enter to continue...");
                    }
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nError:\n{ex.Message}\n");
            }
 
            Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
        }
    }
}

Watch this video clip for the code action.

I  emphasize it again: the result of running the code is a collection of non-database-residing entities are created, which serve as Drawable objects for Transient Graphics, so that user sees highlighted entities as visual hint of the selection. It is up to the calling procedure to decide what to do with these entities. To easy the burden of calling code, I implement the base class as IDisposable, so that as long as the [Multi]NestedEntSelector is used with using(){...} block, these non-database-residing entities will be disposed automatically.


Followers

About Me

My photo
After graduating from university, I worked as civil engineer for more than 10 years. It was AutoCAD use that led me to the path of computer programming. Although I now do more generic business software development, such as enterprise system, timesheet, billing, web services..., AutoCAD related programming is always interesting me and I still get AutoCAD programming tasks assigned to me from time to time. So, AutoCAD goes, I go.