Showing posts with label ObjectOverrule. Show all posts
Showing posts with label ObjectOverrule. Show all posts

Wednesday, May 13, 2020

Prevent Certain Properties of an Entity from Being Changed

In the past I wrote about using ObjectOverrule to prevent entities in AutoCAD from being changed/modified and to force entities being changed in certain way.

An interesting question was raised recently in the Autodesk's discussion forum on how to disable geometry editing. To me, the question could have been a more general one: how to make some of properties of an entity not changeable, while other still can be changed? Once again, ObjectOverrule can play well in the game.

Here is the general idea of doing it with ObjectOverrule:

1. Overriding ObjectOverrule.Open() method: if an entity is opened for write, the values of some target properties, which we do not want them to be changed, will be saved, tagged with ObjectId.

2 Overriding ObjectOverrule.Close() method: check whether there is saved original property value for the entity (by its ObjectId), if found, restore the properties with original values.

Based on this idea, I designed a custom ObjectOverrule. Some explanations are following:

1. The Overrule applies to more general type Entity. It can be more specific, such as Curve, Line..., as needed;

2. No filter is set up. But if needed, proper filter would reduce the overhead in AutoCAD caused by Overrule, minor, or significant;

3. Paired Func/Action is injected into the Overrule whenever the Overrule is enabled. The pair of Func/Action plays the role of extracting original property values in Open() method, and restoring property values in Close() method. This way, the actual value extracting/restoring code is separated from the Overrule itself, so programmer can easily write code to decide what properties to target on different type of entities.

Note: It is even possible to abstract the paired Func/Action into an interface, and implement them in separate project, and make them configurable, so that the custom Overrule can be configured to target different properties, different entity types without having to update code at all. But I'll leave this out of this article.

Here are the code.

Class EntityTarget: it has EntityType property and the pair of Func/Action as properties, used to extract target entity's property values and restore them later.

public class EntityTarget
{
    public Type EntityType { setget; }
    public Func<EntityDictionary<stringobject>> PropExtractFunc
    { setget; }
    public Action<EntityDictionary<stringobject>> PropRestoreAction
    { setget; }
}

Class PropertyFreezeOverrule: its has overridden Open<> and Close() method, in which entity's target property values are extracted into a Dictionary, and then saved in Dictionary keyed with ObjectId at Overrule class level; when the entity is closed, the values of entity's target properties will be restored, if necessary. This, target properties of the entity become not changeable.

using System.Collections.Generic;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
 
namespace FreezePropertyOverrule
{
    public class PropertyFreezeOverrule : ObjectOverrule
    {
        private Dictionary<ObjectIdDictionary<stringobject>> _openedEntities = 
            new Dictionary<ObjectIdDictionary<stringobject>>();
        private List<EntityTarget> _targets = new List<EntityTarget>();
 
        private bool _overruling = false;
 
        private bool _started = false;
 
        public void Start(IEnumerable<EntityTargetoverruleTargets)
        {
            if (_started) Stop();
 
            _targets.Clear();
            _targets.AddRange(overruleTargets);
 
            _openedEntities.Clear();
 
            _overruling = Overrule.Overruling;
 
            Overrule.AddOverrule(RXClass.GetClass(typeof(Curve)), thistrue);
            Overruling = true;
            _started = true;
        }
 
        public void Stop()
        {
            Overrule.RemoveOverrule(RXClass.GetClass(typeof(Curve)), this);
            Overrule.Overruling = _overruling;
            _started = false;
        }
 
        public override void Open(DBObject dbObjectOpenMode mode)
        {
            if (mode == OpenMode.ForWrite)
            {
                ExtractTargetProperties(dbObject);
            }
            base.Open(dbObjectmode);
        }
 
        public override void Close(DBObject dbObject)
        {
            RestoreTargetProperties(dbObject);
            base.Close(dbObject);
        }
 
        #region private methods
 
        private void ExtractTargetProperties(DBObject dbObject)
        {
            foreach (var target in _targets)
            {
                if (dbObject.GetRXClass()==RXClass.GetClass(target.EntityType))
                {
                    var propData = target.PropExtractFunc(dbObject as Entity);
                    if (_openedEntities.ContainsKey(dbObject.ObjectId))
                    {
                        _openedEntities[dbObject.ObjectId] = propData;
                    }
                    else
                    {
                        _openedEntities.Add(dbObject.ObjectId, propData);
                    }
                    break;
                }
            }
        }
 
        private void RestoreTargetProperties(DBObject dbObject)
        {
            if (dbObject.IsUndoing) return;
            if (!dbObject.IsModified) return;
            if (dbObject.IsErased) return;
            if (!dbObject.IsWriteEnabled) return;
 
            if (!_openedEntities.ContainsKey(dbObject.ObjectId)) return;
 
            foreach (var target in _targets)
            {
                if (dbObject.GetRXClass() == RXClass.GetClass(target.EntityType))
                {
                    var propData = _openedEntities[dbObject.ObjectId];
                    if (propData!=null)
                    {
                        target.PropRestoreAction(dbObject as EntitypropData);
                        PropertyExtractRestoreUtils.SendMessageToCommandLine(
                            "\nWARNING: editing to this entity was overrule. No change is allowed!\n");
                    }
                    _openedEntities.Remove(dbObject.ObjectId);
                    break;
                }
            }
        }
        #endregion
    }
}

Class PropertyExtracRestoreUtils: it defines a series of paired Func/Action to extracting/restoring entity of specific type. I use the naming convention of GetXxxxProperties() and RestoreXxxxProperties(), where Xxxx is the entity type. For the simplicity, I only defined the pair methods for Line and Circle. But it is easy to add more pairs to target other entity types. Since I also only want to keep Line/Circle geometrically frozen, so, for Line, the properties to freeze are StartPoint and EndPoint; for Circle, Center and Radius.

public static class PropertyExtractRestoreUtils
{
    private const string LINE_START_POINT = "StartPoint";
    private const string LINE_END_POINT = "EndPoint";
 
    private const string CIRCLE_CENTER = "Center";
    private const string CIRCLE_RADIUS = "Radius";
 
    public static Dictionary<stringobjectGetLineProperties(Entity line)
    {
        Dictionary<stringobjectprops = null;
 
        var l = line as Line;
        if (l!=null)
        {
            props = new Dictionary<stringobject>();
            props.Add(LINE_START_POINT, l.StartPoint);
            props.Add(LINE_END_POINT, l.EndPoint);
        }
 
        return props;
    }
 
    public static void RestoreLineProperties(
        Entity lineDictionary<stringobjectproperties)
    {
        var l = line as Line;
        if (l != null)
        {
            if (properties.ContainsKey(LINE_START_POINT) &&
                properties.ContainsKey(LINE_END_POINT))
            {
                l.StartPoint = (Point3d)properties[LINE_START_POINT];
                l.EndPoint = (Point3d)properties[LINE_END_POINT];
            }
        }
    }
 
    public static Dictionary<stringobjectGetCircleProperties(Entity circle)
    {
        Dictionary<stringobjectprops = null;
        var c = circle as Circle;
        if (c!=null)
        {
            props = new Dictionary<stringobject>();
            props.Add(CIRCLE_CENTER, c.Center);
            props.Add(CIRCLE_RADIUS, c.Radius);
        }
 
        return props;
    }
 
    public static void RestoreCircleProperties(
        Entity circleDictionary<stringobjectproperties)
    {
        var c = circle as Circle;
        if (c != null)
        {
            if (properties.ContainsKey(CIRCLE_CENTER) &&
                properties.ContainsKey(CIRCLE_RADIUS))
            {
                c.Center = (Point3d)properties[CIRCLE_CENTER];
                c.Radius = (double)properties[CIRCLE_RADIUS];
            }
        }
    }
 
    public static void SendMessageToCommandLine(string msg)
    {
        var ed = Autodesk.AutoCAD.ApplicationServices.Application.
            DocumentManager.MdiActiveDocument.Editor;
        ed.WriteMessage(msg);
    }
}

Following CommandClass put all together into work:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(FreezePropertyOverrule.MyCommands))]
 
namespace FreezePropertyOverrule
{
    public class MyCommands 
    {
        private static PropertyFreezeOverrule _freezeOverrule = null;
 
        [CommandMethod("StartFreeze")]
        public static void StartMyOverrule()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            if (_freezeOverrule==null)
            {
                _freezeOverrule = new PropertyFreezeOverrule();
            }
 
            var targets = new EntityTarget[]
            {
                new EntityTarget
                {
                    EntityType=typeof(Line), 
                    PropExtractFunc=PropertyExtractRestoreUtils.GetLineProperties, 
                    PropRestoreAction=PropertyExtractRestoreUtils.RestoreLineProperties 
                },
                new EntityTarget
                {
                    EntityType=typeof(Circle),
                    PropExtractFunc=PropertyExtractRestoreUtils.GetCircleProperties,
                    PropRestoreAction=PropertyExtractRestoreUtils.RestoreCircleProperties
                }
            };
 
            _freezeOverrule.Start(targets);
            ed.WriteMessage(
                "\nEntity freezing overrule started\n");
        }
 
        [CommandMethod("StopFreeze")]
        public static void StopMyOverrule()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
 
            var ed = dwg.Editor;
            if (_freezeOverrule!=null)
            {
                _freezeOverrule.Stop();
            }
 
            ed.WriteMessage(
                "\nEntity freezing overrule stopped.\n");
        }
    }
}

The video clip below shows how the code works: if the Overrule is enabled (started), Line entity cannot be extended, shortened, moved, rotated..., while Circle also cannot be enlarged, shrunk, or moved. However, their other non-geometric properties, such as Layer, Color..., can still be changed. Once the Overrule is disabled/stopped, Line and Circle can be fully modified.



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









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.