Monday, December 30, 2019

My Class in Autodesk University 2019, Las Vegas

I attended Autodesk University 2019, Las Vegas in Nov. 2019 and presented my class (SD321455). The class' is titled as "Creating Plugable Custom Document Window Content To Enhance  AutoCAD UI".

Here is the link to the class' materials in Autodesk University website:

https://www.autodesk.com/autodesk-university/class/Creating-Plugable-Custom-Document-Window-Content-Enhance-AutoCAD-Softwares-UI-2019

The presentation itself was not as good as I wanted, because after a few minutes into it, I realized that the time (60 minutes) was much shorter than I needed as planned. So, the recorded video would not be much of good for learning. But the 48-page handout and the download-able code projects, in conjunction with PowerPoint presentation cover all details for any AutoCAD .NET API programmer to know on the topic.

By the way, to tell the truth, one of the factors that motivated me to go to Autodesk University, Las Vegas is the Rock'n Roll Las Vegas Marathon held on the Sunday right before AU (No, 17th, 2019), which was the 7th marathon races I completed in 2019😀

Friday, December 20, 2019

Retrieving Catchment Information from Its Label in Civil 3D

A post in Autodesk's Civil 3D customization discussion forum here asked how to retrieve Catchment data for exporting to external file and I suggested to use Catchment label as the source of Catchment data, as long as the label style used for the label contains the wanted information of the Catchment. Upon the request of the original poster, I worked out some code and post here in my blog, so it can be read easier, or downloaded as complete project for test run.

While code is rather simple and quite self-explanatory, I also placed a bit comments here and there.

These are 2 major classes:

using System.Collections.Generic;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace CatchmentLabelData
{
    public class CatchmentAreaInfo
    {
        public string AreaName { setget; } = "";
        public double Area { setget; } = 0.0;
        public double Coefficient { setget; } = 1.0;
        public double ConcentrationTime { setget; } = 0.0;
 
        public override string ToString()
        {
            return AreaName + "," +
                Area.ToString("#####0.000") + "," +
                Coefficient.ToString("#0.00") + "," +
                ConcentrationTime.ToString("##0.0");
        }
    }
 
    public class CatchmentLabelDataExtractor
    {
        public Document ThisDwg { private setget; }
 
        public void ExtractDataFromLabels(Document dwg)
        {
            ThisDwg = dwg;
 
            IEnumerable<CatchmentAreaInfodata = ExtractData();
            if (data!=null)
            {
                SaveData(data);
            }
            else
            {
                CadApp.ShowAlertDialog(
                    "No catchment data found!");
            }
        }
 
        #region private methods: extract data from Catchment labels
 
        private IEnumerable<CatchmentAreaInfoExtractData()
        {
            var lblIds = FindCatchmentLabels();
            if (lblIds.Count == 0) return null;
 
            var data = new List<CatchmentAreaInfo>();
            foreach (var id in lblIds)
            {
                CatchmentAreaInfo area = GetCatchmentAreaDataFromLabel(id);
                if(area!=null)
                {
                    data.Add(area);
                }
            }
 
            return data.Count > 0 ? data : null; ;
        }
 
        private CatchmentAreaInfo GetCatchmentAreaDataFromLabel(ObjectId lblId)
        {
            CatchmentAreaInfo area = null;
 
            using (var tran = lblId.Database.TransactionManager.StartTransaction())
            {
                var ent = (Entity)tran.GetObject(lblIdOpenMode.ForRead);
 
                var lblText = ExplodeLabel(ent);
 
                if (!string.IsNullOrEmpty(lblText))
                {
                    area = ParseLabelText(lblText);
                }
 
                tran.Commit();
            }
 
            return area;
        }
 
        private string ExplodeLabel(Entity ent)
        {
            var lblText = "";
            var exploded = new List<DBObject>();
 
            // first round of explosion: the result is a BlockReference
            BlockReference blk = null;
            var outEnts = new DBObjectCollection();
            ent.Explode(outEnts);
            foreach (DBObject obj in outEnts)
            {
                exploded.Add(obj);
                if (obj is BlockReference)
                {
                    blk = (BlockReference)obj;
                }
            }
 
            // second round of explosion, the result is an MText
            if (blk!=null)
            {
                outEnts = new DBObjectCollection();
                blk.Explode(outEnts);
                foreach (DBObject obj in outEnts)
                {
                    exploded.Add(obj);
                    if (obj is MText)
                    {
                        lblText = ((MText)obj).Text;
                    }
                }
            }
 
            // disposed entities resulted in by explosion
            foreach (DBObject obj in exploded)
            {
                obj.Dispose();
            }
            
            return lblText;
        }
 
        /// <summary>
        /// The text parsing logic here would heavily be depended on 
        /// the label style's content
        /// </summary>
        /// <param name="lblText"></param>
        /// <returns></returns>
        private CatchmentAreaInfo ParseLabelText(string lblText)
        {
            var txts = lblText.Replace("\r\n""|").Split('|');
            if (txts.Length!=4)
            {
                // Invalid label content
                // Since we target particular label style
                // we should know the lable content has 4 pieces of data
                return null;
            }
 
            var name = txts[0];
            var area = ParseForNumber(txts[1]);
            var coe = ParseForNumber(txts[2]);
            var time = ParseForNumber(txts[3]);
 
            CatchmentAreaInfo catchment = new CatchmentAreaInfo
            {
                AreaName = name,
                Area = area,
                Coefficient = coe,
                ConcentrationTime = time
            };
 
            return catchment;
        }
 
        private double ParseForNumber(string txt)
        {
            double num = 0.0;
 
            int start = -1;
            int end = -1;
            forint i=0; i<txt.Length; i++)
            {
                if (char.IsNumber(txt,i))
                {
                    if (start == -1) start = i;
                    break;
                }
            }
 
            if (start>=0)
            {
                for (int i=start+1; i<txt.Length;i++)
                {
                    if (!char.IsNumber(txti) &&
                        !char.IsPunctuation(txt,i))
                    {
                        end = i;
                        break;
                    }
                }
 
                if (end <= 0) end = txt.Length;
            }
 
            if (start>=0 && end>start)
            {
                var s = txt.Substring(startend - start).Trim();
                num = double.Parse(s);
            }
 
            return num;
        }
 
        private List<ObjectIdFindCatchmentLabels()
        {
            var ids = new List<ObjectId>();
            using (var tran = ThisDwg.TransactionManager.StartTransaction())
            {
                var model = (BlockTableRecord)tran.GetObject(
                    SymbolUtilityServices.GetBlockModelSpaceId(ThisDwg.Database), OpenMode.ForRead);
                foreach (ObjectId id in model)
                {
                    //ThisDwg.Editor.WriteMessage($"\nEntity => {id.ObjectClass.DxfName}");
                    if (id.ObjectClass.DxfName.ToUpper()==
                        "AECC_CATCHMENTAREA_LABEL")
                    {
                        //===========================================================
                        // we may want to further examine the label's LabelStyle
                        // in order to make sure this label does contain the data
                        // as expected according to the LabelStyle desgin.
                        // for simplicity of this sample code, I skip it
                        //===========================================================
 
                        ids.Add(id);
                    }
                }
 
                tran.Commit();
            }
 
            return ids;
        }
 
        #endregion
 
        #region private methods: save extracted data
 
        private void SaveData(IEnumerable<CatchmentAreaInfodata)
        {
            var txts = new List<string>();
            foreach (var catchment in data)
            {
                txts.Add(catchment.ToString());
            }
 
            var header = "Name,Area,Runoff Coefficient,Time of Concentration\r\n";
            var outputText = header + string.Join("\r\n"txts.ToArray());
 
            var fileName = GetFileName();
            if (!string.IsNullOrEmpty(fileName))
            {
                if (System.IO.File.Exists(fileName))
                {
                    System.IO.File.Delete(fileName);
                }
 
                System.IO.File.WriteAllText(fileNameoutputText);
            }
        }
 
        private string GetFileName()
        {
            var fileName = "";
            using (var dlg = new System.Windows.Forms.SaveFileDialog())
            {
                dlg.Title = "Save Catchment Area Information To CSV File";
                if (ThisDwg.IsNamedDrawing)
                {
                    dlg.InitialDirectory = System.IO.Path.GetDirectoryName(
                        ThisDwg.Name);
                    dlg.FileName = System.IO.Path.GetFileNameWithoutExtension(
                        ThisDwg.Name) + ".csv";
                }
                dlg.Filter = "CSV File *.csv|*.csv";
                dlg.OverwritePrompt = true;
                if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
                {
                    fileName = dlg.FileName;
                }
            }
 
            return fileName;
        }
 
        #endregion
    }
}

Here is the CommandClass that runs process:

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(CatchmentLabelData.MyCommands))]
 
namespace CatchmentLabelData
{
    public class MyCommands
    {
        [CommandMethod("CatchmentData")]
        public static void RunMyCommand()
        {
            var doc = CadApp.DocumentManager.MdiActiveDocument;
            var ed = doc.Editor;
 
            try
            {
                var extractor = new CatchmentLabelDataExtractor();
                extractor.ExtractDataFromLabels(doc);
            }
            catch(System.Exception ex)
            {
                ed.WriteMessage(
                    $"\nError: \n{ex.Message}\n");
            }
        }
    }
}


For simplicity, I let the output to be saved as CSV file. The primary goal of this article is about retrieving Catchment data from its label.

Some thoughts on this topic:

It is quite frustrating that Autodesk seems does not make much effort to update/enhance Civil 3D's API. Label is most powerful thing in Civil 3D: it can designed to automatically obtain data from Civil 3D objects (as LabelStyle's Content). Obviously, there is internal API to do this, because user can edit the label style's content. But these APIs are not exposed to outside at all (maybe, because they are too complicated. I am always amazed how labels in Civil 3D work).

Through this discussion, I can see, with lack of API support, one possible workaround of getting Civil 3D object information is to:

1. Define a label style to let its content expose the wanted information. That is, let Civil 3D to do the work of collecting data from its objects into particular labels;
2. We then can use similar approach shown in this article to obtain the data from labels.

The source code with built DLL files can be downloaded here. Note, if you want to run the DLL from this download without building from the project\s source code, you need to unlock it first (because of the executable DLL is downloaded via Internet). The project is built against Civil 3D 2020 and .NET Framework 4.72.



















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.