Monday, April 6, 2015

Showing Progress for long code execution in AutoCAD

More often than not we need to write code to loop through large amount of data set, such as entities in a selection set or even entire ModelSpace/PaperSpace of a huge drawing. This process may take a while to complete. It is a common practice to show a progress bar during this lengthy processing to let user know that AutoCAD is busy processing data.

With AutoCAD .NET API, one can quite easily uses the built-in Autodesk.AutoCAD.Runtime.ProgressMeter object to show progress of lengthy executing process. However, from my experience of using ProgressMeter, it often does not show a satisfactory progressing visual effect. The processing effect shown by the ProgressMeter for exact long processing operation done by the exact code could be different from one AutoCAD version to another version, and in many cases, the progress meter simply does not get refreshed during the lengthy processing.

A few years back, I wrote an article on showing a progress window for a long running process in AutoCAD, where I used a modeless window/dialog box to host a progress bar. The purpose was to separate the actual lengthy operation and the visual progress effect in different component, so that the code for progress bar can be easily reused as a component. Again, it has been proving that using modeless window/dialog box has the same unstable progressing visual effect. That is the progress bar sometimes is not refreshed when AutoCAD is busy of intense processing.

I guess, using either AutoCAD built-in ProgressMeter, or ProgressBar hosted in a modeless window, we, as .NET API programmer, do not have much control on how AutoCAD handles its UI update.

The only way to guarantee the UI that shows promptly updated progress bar is to host the progress bar in a modal window/dialog box.

So, I decided to update/rebuild my progress bar code component by using a modal form. Here I used similar approach to use an Interface separating the actual code that does the lengthy AutoCAD processing and the code to display progressing bar.

Firstly, I define an Interface that will be used by the progress bar component. This Interface object tells the progress bar component when a lengthy processing starts and end, stimulate the progress bar to progress: all of these are done through vents defined in this Interface. The Interface also define an action (method), which the progress bar component calls as soon as the progress bar UI shows. See code below (including custom EventHandler and EventArgs used by those Events:

using System;
 
namespace ShowProgressBar
{
    public interface ILongProcessingObject
    {
        event LongProcessStarted ProcessingStarted;
        event LongProcessingProgressed ProcessingProgressed;
        event EventHandler ProcessingEnded;
        event EventHandler CloseProgressUIRequested;
        void DoLongProcessingWork();
    }
 
    public class LongProcessStartedEventArgs : EventArgs
    {
        private int _loopCount;
        private string _description;
        private bool _canStop;
 
        public LongProcessStartedEventArgs(
            string description, int loopCount = 0, bool canStop = false)
        {
            _loopCount = loopCount;
            _description = description;
            _canStop = canStop;
        }
 
        public int LoopCount
        {
            get { return _loopCount; }
        }
 
        public string Description
        {
            get { return _description; }
        }
 
        public bool CanStop
        {
            get { return _canStop; }
        }
    }
 
    public class LongProcessingProgressEventArgs : EventArgs
    {
        private string _progressDescription;
        private bool _cancel = false;
 
        public LongProcessingProgressEventArgs(string progressDescription)
        {
            _progressDescription = progressDescription;
        }
 
        public string ProgressDescription
        {
            get { return _progressDescription; }
        }
 
        public bool Cancel
        {
            set { _cancel = value; }
            get { return _cancel; }
        }
    }
 
    public delegate void LongProcessStarted(
        object sender, LongProcessStartedEventArgs e);
 
    public delegate void LongProcessingProgressed(
        object sender, LongProcessingProgressEventArgs e);
}

Then here the progress bar component:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace ShowProgressBar
{
    public class ProcessingProgressBar : IDisposable
    {
        private dlgProgress _dlg = null;
        private ILongProcessingObject _executingObject;
 
        public ProcessingProgressBar(ILongProcessingObject executingObj)
        {
            _executingObject = executingObj;
        }
 
        public void Start()
        {
            _dlg = new dlgProgress(_executingObject);
            _dlg.ShowDialog();
        }
 
        public void Dispose()
        {
            if (_dlg!=null)
            {
                _dlg.Dispose();
            }
        }
    }
}

As the code show, it is really simple with one public method Start(), which shows a modal dialog box where the progress bar is hosted. This component implements IDispose(), so that when it is disposed, the instance of dialog box (Windows Form) is disposed. The most important part of this component is that it holds an instance of ILongProcessingObject class: as soon as the progress bar form shows, the Start() method of this ILongProcessingObject gets called, and its events subscribed by the progress bar form stimulate the progress bar to progress and close the form when processing is done.

The dialog box form has 4 controls on it: 2 labels to show executing process' message/description, a progress bar, and a button labelled a "Stop". Here is the code behind the form:

using System;
using System.Windows.Forms;
 
namespace ShowProgressBar
{
    public partial class dlgProgress : Form
    {
        private ILongProcessingObject _executingObject = null;
        private bool _stop = false;
        private bool _isMarquee = false;
        private int _loopCount = 0;
 
        public dlgProgress()
        {
            InitializeComponent();
        }
 
        public dlgProgress(
            ILongProcessingObject executingObj)
            : this()
        {
            _executingObject = executingObj;
 
            _executingObject.ProcessingStarted +=
                new LongProcessStarted(ExecutingObject_ProcessStarted);
            _executingObject.ProcessingProgressed +=
                new LongProcessingProgressed(ExecutingObject_Progressed);
            _executingObject.ProcessingEnded +=
                new EventHandler(ExecutingObject_ProcessEnded);
            _executingObject.CloseProgressUIRequested +=
               new EventHandler(ExecutingObject_CloseProgressUIRequested);
        }
 
        private void ExecutingObject_CloseProgressUIRequested(object sender, EventArgs e)
        {
            this.DialogResult = DialogResult.OK;
        }
 
        private void ExecutingObject_ProcessEnded(object sender, EventArgs e)
        {
            pBar.Value = 0;
            lblTitle.Text = "";
            lblDescription.Text = "";
            this.Refresh();
        }
 
        private void ExecutingObject_ProcessStarted(
            object sender, LongProcessStartedEventArgs e)
        {
 
            if (e.LoopCount == 0)
            {
                pBar.Style = ProgressBarStyle.Marquee;
                lblDescription.Text = "Please wait...";
            }
            else
            {
                pBar.Style = ProgressBarStyle.Continuous;
                pBar.Minimum = 0;
                pBar.Maximum = e.LoopCount;
                pBar.Value = 0;
                lblDescription.Text = "";
                _loopCount = e.LoopCount;
            }
 
            _isMarquee = e.LoopCount == 0;
            btnStop.Visible = e.CanStop;
            lblTitle.Text = e.Description;
            Application.DoEvents();
            this.Refresh();
        }
 
        private void ExecutingObject_Progressed(
            object sender, LongProcessingProgressEventArgs e)
        {
            if (!_isMarquee)
            {
                pBar.Value++;
            }
 
            lblDescription.Text = e.ProgressDescription;
            lblDescription.Refresh();
 
            Application.DoEvents();
            if (_stop)
            {
                e.Cancel = true;
            }
        }
 
        private void dlgProgress_Shown(object sender, EventArgs e)
        {
            Application.DoEvents();
 
            if (_executingObject == null)
            {
                this.DialogResult = DialogResult.Cancel;
            }
            else
            {
                _executingObject.DoLongProcessingWork();
            }
        }
 
        private void btnStop_Click(object sender, EventArgs e)
        {
            _stop = true;
        }
    }
}

Sometimes, a long processing may not have determined loop count, or the processing loop only ends with certain condition been met. In the code showing here, I assume if the LongProcessStartedEventArg.LoopCount is 0, then it means the processing loop is unknown, so I set the ProgressBar's Style property to Marquee.

That is all the code for the progress bar component that can be used easily with my AutoCAD data processing objects, as long as they implement ILongProcessingObject Interface. Here is an example class MyCadDataHandler, in which I let it do 2 lengthy tasks:

  • one with known loop count: looping through drawing's ModelSpace for each entity and do something with it
  • one without known loop count: looping through drawing's ModelSpace to find BlockReferences with certain name, and if the count of the found BlockReferences reach a given number the looping stops

Here is he code of class MyCadDataHandler:

using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
 
namespace ShowProgressBar
{
    public class MyCadDataHandler : ILongProcessingObject
    {
        private enum LongProcessingType
        {
            Type1=0,
            Type2=1,
        }
 
        private Document _dwg;
        private LongProcessingType _processingType = LongProcessingType.Type1;
 
        public MyCadDataHandler(Document dwg)
        {
            _dwg = dwg;
        }
 
        #region public methods
 
        public void DoWorkWithKnownLoopCount()
        {
            _processingType = LongProcessingType.Type1;
 
            using (var progress=new ProcessingProgressBar(this))
            {
                progress.Start();
            }
        }
 
        public void DoWorkWithUnknownLoopCount()
        {
            _processingType = LongProcessingType.Type2;
 
            using (var progress = new ProcessingProgressBar(this))
            {
                progress.Start();
            }
        }
 
        #endregion
 
        #region Implementing ILongProcessingObject interface
 
        public event LongProcessStarted ProcessingStarted;
 
        public event LongProcessingProgressed ProcessingProgressed;
 
        public event EventHandler ProcessingEnded;
 
        public event EventHandler CloseProgressUIRequested;
 
        public void DoLongProcessingWork()
        {
            switch(_processingType)
            {
                case LongProcessingType.Type1:
                    LoopThroughModelSpace();
                    break;
                case LongProcessingType.Type2:
                    SearchForTopBlocks("StationLabel", 500);
                    break;
            }
        }
 
        #endregion
 
        #region private methods
 
        private void LoopThroughModelSpace()
        {
            try
            {
                //run 2 long processing loops
                for (int n = 0; n < 2; n++)
                {
                    using (var tran = 
                        _dwg.TransactionManager.StartTransaction())
                    {
                        //Get all entities' ID in ModelSpace
                        BlockTableRecord model = (BlockTableRecord)
                            tran.GetObject(
                            SymbolUtilityServices.GetBlockModelSpaceId(
                            _dwg.Database), OpenMode.ForRead);
 
                        ObjectId[] entIds = model.Cast<ObjectId>().ToArray();
 
                        if (ProcessingStarted != null)
                        {
                            string process = n == 0 ? 
                                "Searching ModelSpace for AAAA" : 
                                "Search ModelSpace for BBBB";
                            LongProcessStartedEventArgs e =
                                new LongProcessStartedEventArgs(
                                    process, entIds.Length, true);
 
                            ProcessingStarted(this, e);
                        }
 
                        int count = 0;
                        foreach (var entId in entIds)
                        {
                            count++;
 
                            if (ProcessingProgressed != null)
                            {
                                string progMsg = string.Format(
                                    "{0} out of {1}. {2} remaining...\n" +
                                    "Processing entity: {3}",
                                    count, 
                                    entIds.Length, 
                                    entIds.Length-count, 
                                    entId.ObjectClass.DxfName);
 
                                LongProcessingProgressEventArgs e =
                                    new LongProcessingProgressEventArgs(progMsg);
                                ProcessingProgressed(this, e);
 
                                //Since this processing is cancellable, we
                                //test if user clicked the "Stop" button in the 
                                //progressing dialog box
                                if (e.Cancel) break;
                            }
 
                            //Do something with the entity
                            Entity ent = (Entity)tran.GetObject(
                                entId, OpenMode.ForRead);
                            long s = 0;
                            for (int i = 0; i < 1000000; i++)
                            {
                                s += i * i;
                            }
                            
                        }
 
                        if (ProcessingEnded != null)
                        {
                            ProcessingEnded(thisEventArgs.Empty);
                        }
 
                        tran.Commit();
                    }
                }
            }
            finally
            {
                //Make sure the CloseProgressUIRequested event always fires, so
                //that the progress dialog box gets closed because of this event
                if (CloseProgressUIRequested != null)
                {
                    CloseProgressUIRequested(thisEventArgs.Empty);
                }
            }
        }
 
        private void SearchForTopBlocks(string blkName, int targetCount)
        {
            List<ObjectId> blkIds = new List<ObjectId>();
 
            try
            {
                if (ProcessingStarted != null)
                {
                    string msg = string.Format(
                        "Searching first {0} block refeences: \"{1}\"",
                        targetCount, blkName);
                    LongProcessStartedEventArgs e =
                        new LongProcessStartedEventArgs(msg);
 
                    ProcessingStarted(this, e);
                }
 
                using (var tran = _dwg.TransactionManager.StartTransaction())
                {
                    //Get all entities' ID in ModelSpace
                    BlockTableRecord model = (BlockTableRecord)tran.GetObject(
                        SymbolUtilityServices.GetBlockModelSpaceId(
                        _dwg.Database), OpenMode.ForRead);
 
                    foreach (ObjectId id in model)
                    {
                        if (ProcessingProgressed != null)
                        {
                            string progMsg = string.Format(
                                "{0} found\n" +
                                "Processing entity: {1}",
                                blkIds.Count, id.ObjectClass.DxfName);
                            LongProcessingProgressEventArgs e =
                                new LongProcessingProgressEventArgs(progMsg);
                            ProcessingProgressed(this, e);
                        }
 
                        if (IsTargetBlock(id, blkName, tran))
                        {
                            blkIds.Add(id);
                            if (blkIds.Count == targetCount) break;
                        }
                    }
 
                    tran.Commit();
                }
 
                if (ProcessingEnded != null)
                {
                    ProcessingEnded(thisEventArgs.Empty);
                }
            }
            finally
            {
                //Make sure the CloseProgressUIRequested event always fires,
                //so that the progress dialog box gets closed because of 
                //this event
                if (CloseProgressUIRequested != null)
                {
                    CloseProgressUIRequested(thisEventArgs.Empty);
                }
            }
        }
 
        private bool IsTargetBlock(
            ObjectId entId, string blkName, Transaction tran)
        {
            //kill a bit time to allow progress bar effect
            long s = 0;
            for (int i = 0; i < 10000000; i++)
            {
                s += i * i;
            }
 
            BlockReference blk = tran.GetObject(
                entId, OpenMode.ForRead) as BlockReference;
            if (blk!=null)
            {
                string name;
                if (blk.IsDynamicBlock)
                {
                    BlockTableRecord br = (BlockTableRecord)
                        tran.GetObject(
                        blk.DynamicBlockTableRecord, OpenMode.ForRead);
                    name = br.Name;
                }
                else
                {
                    name = blk.Name;
                }
 
                return name.ToUpper() == blkName.ToUpper();
            }
 
            return false;
        }
 
        #endregion
    }
}

As the code shows, it has 2 public methods that can be called in a CommandMethod to start the 2 lengthy data processing executions. The third public method DoLongProcessingWork() is the ILongProcessObject interface implementing method and is called when the progress bar hosting dialog box shows.

When I implementing the ILongProcessingObject interface, I have freedom to decide what exactly I want to do when ILongProcessObject.DoLongProcessWork() is called. In the code shown here, I used private enum type LongProcessingType to indicate what long processing operation to be executed. I also has the freedom (and am responsible) to decide when to raise events that stimulate progress bar and close the progress bar form.

Here is the CommandClass that uses MyCadDataHandler:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(ShowProgressBar.MyCadCommands))]
 
namespace ShowProgressBar
{
    public class MyCadCommands
    {
        [CommandMethod("LongWork1")]
        public static void RunLongWork_1()
        {
            Document dwg = CadApp.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;
 
            try
            {
                MyCadDataHandler dataHandler = new MyCadDataHandler(dwg);
                dataHandler.DoWorkWithKnownLoopCount();
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage(
                    "\nError: {0}\n{1}", ex.Message, ex.StackTrace);
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
 
        [CommandMethod("LongWork2")]
        public static void RunLongWork_2()
        {
            Document dwg = CadApp.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;
 
            try
            {
                MyCadDataHandler dataHandler = new MyCadDataHandler(dwg);
                dataHandler.DoWorkWithUnknownLoopCount();
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage(
                    "\nError: {0}\n{1}", ex.Message, ex.StackTrace);
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
    }
}


This video clip show how the code works (I used a drawing with over 9000 entities in ModelSpace).

As I stated at the beginning, I have used AutoCAD built-in ProgressMeter, used Windows.Forms.ProgressBar hosed in both modeless and modal form. So far, using modal form to host a ProgressBar has the most reliable progressing visual effect (I'd guess it would be the same with WPF modeless and modal window, but I have not tried). However, as we all know, forced UI update slows down the actual processing. So, better progressing visual effect with modal form means longer processing time. The bottom line is that psychologically, user feel things going faster with visual progressing indicator when he/she waits 10 seconds than 5 seconds while staring at frozen AutoCAD screen.

The source code can be downloaded here.

Monday, March 23, 2015

Centroid ObjectSnap

During drafting process, sometimes an AutoCAD user would wish he.she could easily find out where the centroid of an polygon (assuming it is a closed Polyline), and better yet, he/she could snap the mouse cursor at the centroid.

With Autodesk.AutoCAD.DatabaseServices.OspanOverrule, this is actually a very easy achievable thing to do. 

Here is the code sample of the custom OsnapOverrule:

using System;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
 
namespace CentroidSnap
{
    public class MyCentroidSnap : OsnapOverrule 
    {
        private static MyCentroidSnap _instance = null;
        private bool _overruling = false;
        private bool _started = false;
 
        public static MyCentroidSnap Instance
        {
            get
            {
                if (_instance == null) _instance = new MyCentroidSnap();
                return _instance;
            }
        }
 
        public bool Started
        {
            get { return _started; }
        }
 
        public void StartOSnap()
        {
            _overruling = Overruling;
 
            Overrule.AddOverrule(RXClass.GetClass(typeof(Curve)), thistrue);
            Overruling = true;
            _started = true;
        }
 
        public void StopOSnap()
        {
            Overrule.RemoveOverrule(RXClass.GetClass(typeof(Curve)), this);
            Overruling = _overruling;
            _started = false;
        }
 
        #region overriden methods
 
        public override void GetObjectSnapPoints(
            Entity entity, ObjectSnapModes snapMode, IntPtr gsSelectionMark,
            Point3d pickPoint, Point3d lastPoint, Matrix3d viewTransform,
            Point3dCollection snapPoints, IntegerCollection geometryIds)
        {
            bool hasCentre = true;
            Point3d snapPt = new Point3d();
 
            if (entity is Polyline)
            {
                Polyline pl = (Polyline)entity;
                if (pl.Closed)
                {
                    snapPt = GetPolylineCentre(pl);
                }
                else
                {
                    hasCentre = false;
                }
            }
            else if (entity is Circle)
            {
                snapPt = ((Circle)entity).Center;
            }
            else
            {
                hasCentre = false;
            }
            
            if (hasCentre)
            {
                //Clear all possible existing snap points
                snapPoints.Clear();
 
                //Set OSnap mode to NEA
                snapMode = ObjectSnapModes.ModeNear;
                snapPoints.Add(snapPt);
            }
        }
 
        public override bool IsContentSnappable(Entity entity)
        {
            return false;
        }
 
        #endregion
 
        #region private methods
 
        private Point3d GetPolylineCentre(Polyline pl)
        {
            double x = 0.0, y = 0.0, z = 0.0;
            for (int i=0; i<pl.NumberOfVertices; i++)
            {
                Point3d p = pl.GetPoint3dAt(i);
                x += p.X;
                y += p.Y;
                z += p.Z;
            }
 
            return new Point3d(
                x / pl.NumberOfVertices, 
                y / pl.NumberOfVertices, 
                z / pl.NumberOfVertices);
        }
 
        #endregion
    }
}

Here is the command class to start/stop the custom MyCentroidSnap overrule:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(CentroidSnap.MyCadCommands))]
 
namespace CentroidSnap
{
    public class MyCadCommands 
    {
 
        [CommandMethod("CSnap")]
        public static void RunMyCadCommand()
        {
            Document dwg = CadApp.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;
 
            if (!MyCentroidSnap.Instance.Started)
            {
                MyCentroidSnap.Instance.StartOSnap();
                ed.WriteMessage(
                    "\nMYCentroidOSnap is started.");
            }
            else
            {
                MyCentroidSnap.Instance.StopOSnap();
                ed.WriteMessage(
                    "\nMYCentroidOSnap is stopped.");
            }
 
            Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
        }
    }
}

I also posted on the same topic a couple of years ago herehere and here. Obviously, I could have also create this MyCentroidSnap class by deriving it from Autodesk.AutoCAD.GraphicsInterface.Glyph class. 

As usual, see this video clip for the MyCentroidSnap's action.

Friday, March 6, 2015

COM API Update to Entity And Transaction

When moving from AutoCAD VBA (COM API) programming to AutoCAD ObjectARX .NET API programming, one of the most "dramatic" changes an AutoCAD programmer might find is the concept of using Transaction. That is, the code that updates anything in drawing database (entities, table records...) must be wrapped in a transaction, and the transaction must be explicitly committed in order for the update/change to be materialized.

I believe I was not the only one who forgot to call Transaction.Commit() quite a few times during the early stage of moving from VBA to .NET programming until calling Commit() became basic instinct after writing a lot .NET code.

One good thing of moving from VBA to .NET programming is that one can still use COM API in .NET code, although it is debatable what is a good practice to mix COM API and .NET API, which is not the topic of this article.

One thing I have been wondering occasionally all the years since I moved to use .NET APIs: what would happen if the COM API calls, which change stuff in drawing, are wrapped inside a .NET API's Transaction?

Naturally, my assumption is that the Transaction is not necessary, unless I want to have a chance to undo the changes made with COM API calls (that is, Transaction.Abort() can undo changes wrapped by the Transaction, be it the changes are made via .NET API, or COM API). To verify this assumption, I wrote following code to verify it:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Interop;
using Autodesk.AutoCAD.Interop.Common;
 
[assemblyCommandClass(typeof(ComApiInTransaction.MyCommands))]
 
namespace ComApiInTransaction
{
    public class MyCommands
    {
        [CommandMethod("PureCom")]
        public static void ComWithoutTransaction()
        {
            Document dwg = Application.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;
 
            try
            {
                AcadDocument acadDoc = (AcadDocument)dwg.GetAcadDocument();
 
                UpdateEntitiesWithCom(acadDoc);
 
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nCommand \"MyCmd\" failed:");
                ed.WriteMessage("\n{0}\n{1}", ex.Message, ex.StackTrace);
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
 
        [CommandMethod("TransCom")]
        public static void ComWithTransaction()
        {
            Document dwg = Application.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;
 
            try
            {
                AcadDocument acadDoc = (AcadDocument)dwg.GetAcadDocument();
 
                using (var trans = dwg.TransactionManager.StartTransaction())
                {
                    UpdateEntitiesWithCom(acadDoc);
 
                    dwg.Editor.UpdateScreen();
 
                    PromptKeywordOptions opt = new PromptKeywordOptions(
                        "\nSelect option:");
                    opt.AppendKeywordsToMessage = true;
                    opt.Keywords.Add("Commit");
                    opt.Keywords.Add("Abort");
                    opt.Keywords.Default = "Commit";
 
                    bool commit = false;
                    PromptResult res = ed.GetKeywords(opt);
                    if (res.Status==PromptStatus.OK)
                    {
                        commit = res.StringResult == "Commit";
                    }
 
                    if (commit)
                        trans.Commit();
                    else
                        trans.Abort();
                }
 
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nCommand \"MyCmd\" failed:");
                ed.WriteMessage("\n{0}\n{1}", ex.Message, ex.StackTrace);
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
 
        [CommandMethod("NetChange")]
        public static void NetChange()
        {
            Document dwg = Application.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;
 
            try
            {
                using (var trans = dwg.TransactionManager.StartTransaction())
                {
                    UpdateEntitiesWithNetApi(dwg, trans);
 
                    //This line of code makes sure the entity color changes
                    //in AutoCAD editor before the Transaction is commited
                    trans.TransactionManager.QueueForGraphicsFlush();
 
                    dwg.Editor.UpdateScreen();
 
                    PromptKeywordOptions opt = new PromptKeywordOptions(
                        "\nSelect option:");
                    opt.AppendKeywordsToMessage = true;
                    opt.Keywords.Add("Commit");
                    opt.Keywords.Add("Abort");
                    opt.Keywords.Default = "Commit";
 
                    bool commit = false;
                    PromptResult res = ed.GetKeywords(opt);
                    if (res.Status == PromptStatus.OK)
                    {
                        commit = res.StringResult == "Commit";
                    }
 
                    if (commit)
                        trans.Commit();
                    else
                        trans.Abort();
                }
 
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nCommand \"MyCmd\" failed:");
                ed.WriteMessage("\n{0}\n{1}", ex.Message, ex.StackTrace);
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
 
        private static void UpdateEntitiesWithCom(AcadDocument dwg)
        {
            foreach (AcadEntity ent in dwg.ModelSpace)
            {
                int index=(int)ent.TrueColor.ColorIndex;
                index++;
                if (index<1 || index>7) index=1;
 
                AcadAcCmColor color=new AcadAcCmColor();
                color.ColorIndex=(AcColor)index;
                ent.TrueColor = color;
                ent.Update();
            }
        }
 
        private static void UpdateEntitiesWithNetApi(
            Document dwg, Transaction tran)
        {
            ObjectId modelId = 
                SymbolUtilityServices.GetBlockModelSpaceId(dwg.Database);
            BlockTableRecord model = (BlockTableRecord)
                tran.GetObject(modelId, OpenMode.ForRead);
            foreach (ObjectId entId in model)
            {
                Entity ent = (Entity)tran.GetObject(entId, OpenMode.ForWrite);
 
                short index = ent.Color.ColorIndex;
                index++;
                if (index < 1 || index > 7) index = 1;
 
                Autodesk.AutoCAD.Colors.Color color =
                    Autodesk.AutoCAD.Colors.Color.FromColorIndex(
                    Autodesk.AutoCAD.Colors.ColorMethod.ByColor, index);
 
                ent.Color = color;
            }
        }
    }
}

The code uses COM API to change all entities' color in ModelSpace. The first Command "PureCom" simply goes ahead to change entities' color with COM API; while the second command "TransCom" wraps the changes made by COM API in a Transaction, and user gets chance to decide whether to commit the changes or abort.

There is also a third command that uses .NET API to to the same with entities in ModelSpace, as a comparison to the COM API counterpart.

See this video clip for the code in action.


Thursday, March 5, 2015

Update Drawing After Async Task Completed

We all know that AutoCAD is not multi-thread-able when dealing with drawing database. But, this does not mean one cannot spin multiple threads from AutoCAD process. With the help of .NET framework, it is quite easy to kick off a few threads from AutoCAD process to do some time-consuming works (as long as the side threads do not deal with the drawing database loaded into AutoCAD), so that the AutoCAD's main thread is not locked waiting for those lengthy works being completed.

With more and more computing power/resources being available in the clouds, it becomes more and more common that a drafting/designing process needs AutoCAD to grab information from remote locations (network share, remote database, cloud services...). Depending on the nature of the remote resources, the time to obtain data could be lengthy enough that doing it in a background thread makes much sense.

Also, in most cases, if not all, after the background thread finishes it work, we'd like AutoCAD responds properly according to the result of the side thread, such as updating the currently opened drawing with the data processed/retrieved by the background thread.

Here is a scenario: with a drawing open in AutoCAD, user wants to check a remote data source for some information, which could takes seconds or minutes to complete; when AutoCAD gets that piece of information back, the drawing or drawings in current AutoCAD session is or are to be updated, such as updating complicated title block, creating or updating a few entities in drawing...; while AutoCAD tries to get data from the remote source, the user does not like AutoCAD UI being locked up, the user wants to continue to work with AutoCAD, at least to be able to zoom/pan..., for example.

There are some discussions can be found online on this topic. One is an article posted by ADN team member Adam Nagy. In his article, Adam shows how to use a System.Windows.Forms.Control to invoke a callback from the side thread to get back to AutoCAD main thread.

I also had an article discussing this topic a few years back, in which I used System.ComponentModel.BackgroundWorker to do work i n side thread. I'd say that using BackgroundWorker is more natural choice in most cases: it allows the side processing to expose its progress, to be cancelled and provides status of completion (being cancelled, completed with or without error).

We, as AutoCAD programmers, are probably more interested in letting AutoCAD doing something accordingly after side thread is done with its work. The code in Adam's article shows how to create a Line entity (or any type of Entity, for that matter). However, one would be wondering: what will happen if AutoCAD's main thread is in middle of something, for example, a command is in progress when the side thread is done and calls back to the main thread? Can the callback still be abler to lock the MdiActiveDocuemnt and update it regardless AutoCAD having a command in progress?

To actually experience what could happen, I decided to give it a try. I started with AutoCAD 2012, which is still my company's working version, and then with AutoCAD 2014 and 2015. It proves AutoCAD 2015 makes things a bit differently, possibly because of the removal of FIBER, which I'll point out later.

Below is the code I used.

This is class SideWorker that does the side work and update drawing when side work finishes:

using System.ComponentModel;
using Autodesk.AutoCAD.ApplicationServices;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
 
namespace BackgroundThreadCallBack
{
    public class SideWorker
    {
        Document _dwg = null;
        BackgroundWorker _worker = null;
        int _runningCount;
        int _currentCount;
 
        public SideWorker(int runningCount)
        {
            _runningCount = runningCount;
        }
 
        public void StartSideWork()
        {
            _dwg = CadApp.DocumentManager.MdiActiveDocument;
 
            _worker = new BackgroundWorker();
            _worker.RunWorkerCompleted += SideWorker_RunWorkerCompleted;
            _worker.DoWork += SideWorker_DoWork;
 
            _currentCount = 0;
 
            _worker.RunWorkerAsync();
        }
 
        private void SideWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            //Simulate a long-running process that need 10 sec to complete
            System.Threading.Thread.Sleep(10000);
        }
 
        private void SideWorker_RunWorkerCompleted(
            object sender, RunWorkerCompletedEventArgs e)
        {
            Document doc = CadApp.DocumentManager.MdiActiveDocument;
            Editor ed = doc.Editor;
 
            //Only if user has not switched current drawing
            if (doc == _dwg)
            {
                _currentCount++;
 
                if (_currentCount &lt;= _runningCount)
                {
                    AddCircle(doc, _currentCount * 10.0);
                    ed.WriteMessage(
                        "\nCircle \"{0}\" added.",
                        _currentCount);
                }
                
                if (_currentCount &lt; _runningCount)
                {
                    _worker.RunWorkerAsync();
                }
                else
                {
                    ed.WriteMessage(
                        "\n{0} circle{1} added!",
                        _currentCount,
                        _currentCount > 1 ? "s" : "");
 
                    _worker.Dispose();
                }
            }
            else
            {
                ed.WriteMessage(
                    "\nCurrent drawing has changed!");
            }
        }
 
        private void AddCircle(Document dwg, double circleRadial)
        {
            using (var dlock=dwg.LockDocument())
            {
                using (var tran=dwg.TransactionManager.StartTransaction())
                {
                    var space = (BlockTableRecord)
                        tran.GetObject(
                        dwg.Database.CurrentSpaceId, 
                        OpenMode.ForWrite);
 
                    Circle c = new Circle();
                    c.Center = new Point3d(0.0,0.0,0.0);
                    c.Radius = circleRadial;
                    c.SetDatabaseDefaults();
 
                    space.AppendEntity(c);
                    tran.AddNewlyCreatedDBObject(c, true);
 
                    tran.Commit();
                }
            }
        }
    }
}

What the code does is to kick off a few times of background thread for a long processing work (I simply have this side thread sleep for 10 second to simulate a long computing process); when each of the processing is completed, the callback to the main AutoCAD thread will update the drawing (adding a Circle).

Here the the command class that uses the class SideWorker:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
 
[assemblyCommandClass(typeof(BackgroundThreadCallBack.MyCommands))]
 
namespace BackgroundThreadCallBack
{
    public class MyCommands 
    {
        [CommandMethod("SideWork")]
        public static void RunMyCommand()
        {
            Document dwg = Application.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;
 
            SideWorker worker = new SideWorker(5);
            worker.StartSideWork();
 
            ed.WriteMessage(
                "\nSide worker is started to create new circles. " +
                "You can continue working with current drawing...");
 
            Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
        }
    }
}

I then ran the code in different condition:
  • After executing command "SideWork", let AutoCAD be idle;
  • After executing command "SideWork", start different AutoCAD command, it may or may not be completed before a background thread calls back to the main thread. 
Here is a video clip showing how AutoCAD reacts.

As one can see:
  • When AutoCAD is idle, there is no issue for side thread calling back and updating drawing. This is expected, of course.
  • When command, like "PLine", "Line" is in progress (that is, I kept picking point to draw a polyline and.or a line), the background thread callback has no problem to create circle in drawing.
  • When command "Circle" in progress (that is, I picked center point, and kept dragging the mouse to decide the radius), the background callback executed (as the command line message "Circle # added!" could be seen), but no circle being drawn by the callback, nor AutoCAD reports error.
  • When other command. such as "STRETCH" or "MOVE" on a SelectionSet of PickFirst entity or entities, the callback results are the same as when "CIRCLE" command in progress. That is, callback executed, no circle being added as the callback result, nor error raised.
Based on this experiment, it seems that we can say that if a real command in progress, which locks the document, then the side thread callback cannot lock the document to update it, in this case, AutoCAD somehow ignores the failed document locking/updating and continue. 

As for why when "PLINE" or "LINE" command allows side thread callback to update drawing, I guess is that when one keeps picking next point, the command is actually update the POLYLINE/LINE right at the point is picked, and when user hover the mouse for another point, the drawing is not actually locked. However, this is only true for AutoCAD 2014 or earlier. In AutoCAD 2015, however, with command "PLINE" or "LINE" in progress, the side thread callback could not update the drawing. See this video clip. I'd think AutoCAD 2015's behaviour in this regard makes more sense. That is, if AutoCAD has a command in progress, it should not be interrupted by the completion of a side thread execution.

Therefore, we need find a way to let AutoCAD know that side work has been completed and it is time for AutoCAD to pick up the results and do something accordingly. either automatically, or with user interaction. With some erring and trying, I came to following modified class SideWorker:

using System.ComponentModel;
using Autodesk.AutoCAD.ApplicationServices;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
 
namespace BackgroundThreadCallBack
{
    public class SideWorker
    {
        Document _dwg = null;
        BackgroundWorker _worker = null;
        int _runningCount = 1;
        int _currentCount;
        bool _actionPending = false;
        bool _showDelayedMsg = false//Used for debugging purpose
 
        public SideWorker(int runningCount)
        {
            //Indicate how many times a background task
            //is to run as side thread
            _runningCount = runningCount;
        }
 
        public void StartSideWork()
        {
            _dwg = CadApp.DocumentManager.MdiActiveDocument;
 
            _worker = new BackgroundWorker();
            _worker.RunWorkerCompleted += SideWorker_RunWorkerCompleted;
            _worker.DoWork += SideWorker_DoWork;
 
            _currentCount = 0;
 
            //Start the very first background work
            _actionPending = false;
            _worker.RunWorkerAsync();
        }
 
        private void SideWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            //Simulate a long-running process that need 10 sec to complete
            System.Threading.Thread.Sleep(10000);
        }
 
        private void SideWorker_RunWorkerCompleted(
            object sender, RunWorkerCompletedEventArgs e)
        {
            Document doc = CadApp.DocumentManager.MdiActiveDocument;
            Editor ed = doc.Editor;
 
            if (!_actionPending)
            {
                //listenning Idle event each time when 
                //side thread has done its work
                Application.Idle += Application_Idle;
                _actionPending = true;
            }
        }
 
        private void Application_Idle(object sender, System.EventArgs e)
        {
            if (!_actionPending) return;
 
            //Even Application is idle, it may not be in quiescent state
            if (!Application.IsQuiescent)
            {
                if (!_showDelayedMsg)
                {
                    //Show why AutoCAD is not in quiescent state:
                    //a command is in profress
                    Document d = Application.DocumentManager.MdiActiveDocument;
                    d.Editor.WriteMessage(
                        "\nCommand \"{0}\" is executing, drawing circle is delayed!",
                        d.CommandInProgress);
                    _showDelayedMsg = true; 
                }
 
                return;
            }
 
            _showDelayedMsg = false;
 
            Document doc = Application.DocumentManager.MdiActiveDocument;
            Editor ed = doc.Editor;
 
            //If user switches active document during side work execution
            //the side work thread will be cancelled
            if (doc != _dwg)
            {
                _worker.Dispose();
                _worker = null;
                _actionPending = false;
 
                int remaining = _runningCount - _currentCount;
 
                ed.WriteMessage(
                    "\nSide work is cancelled due to active document change." +
                    "\n{0} circle{1} remaining to be drawn!",
                    remaining, remaining > 1 ? "s" : "");
            }
            else
            {
                _currentCount++;
 
                if (_currentCount <= _runningCount)
                {
                    AddCircle(doc, _currentCount * 10.0);
                    ed.WriteMessage(
                        "\nCircle \"{0}\" added.",
                        _currentCount);
                    if (_currentCount < _runningCount)
                    {
                        _actionPending = false;
                        _worker.RunWorkerAsync();
                    }
                }
                
                if (_currentCount>=_runningCount)
                {
                    _worker.Dispose();
                    _worker = null;
                    _actionPending = false;
 
                    ed.WriteMessage(
                        "\n{0} circle{1} added!",
                        (_currentCount - 1),
                        (_currentCount - 1) > 1 ? "s" : "");
                }
            }
 
            Application.Idle -= Application_Idle;
        }
 
        private void AddCircle(Document dwg, double circleRadial)
        {
            using (var dlock=dwg.LockDocument())
            {
                using (var tran=dwg.TransactionManager.StartTransaction())
                {
                    var space = (BlockTableRecord)
                        tran.GetObject(
                        SymbolUtilityServices.GetBlockModelSpaceId(dwg.Database), 
                        OpenMode.ForWrite);
 
                    Circle c = new Circle();
                    c.Center = new Point3d(0.0,0.0,0.0);
                    c.Radius = circleRadial;
                    c.SetDatabaseDefaults(dwg.Database);
 
                    space.AppendEntity(c);
                    tran.AddNewlyCreatedDBObject(c, true);
 
                    tran.Commit();
                }
 
                dwg.Editor.UpdateScreen();
            }
        }
    }
}

As one can see, the major change is to start handling Application.Idle event when a side thread execution completed; and the actual drawing update occurs in Application.Idle event handler. Also, in Application.Idle event handler, the code needs to test if the application is in quiescent status (e.g. if there is a command in progress), because AutoCAD being idle does not necessarily mean there is no command in progress: AutoCAD may in middle of a command and waiting user an expected interaction. Once user completes or cancels the command in progress, AutoCAD will have chance to fire Idle event again. so that the action expected after a side thread work is done will have its turn when Application.Idle fires again.

This video clip shows the how the updated SideWorker class work after command "SideWork" issues:
  • If user does not do anything with AutoCAD, background work (sleep 10 seconds) is executed 5 times, and each time the side thread execution completes, a circle is drawn in current drawing;
  • If user uses transparent command, such as zoom/pan with mouse, circles are drawn at each end of side thread execution, meaning zooming/panning has not impact;
  • When user start a command to let AutoCAD do something during the background thread execution, AutoCAD UI is not locked; and when the side thread finishes and the AutoCAD's current command is still in progress, no circle is drawn until the current command is ended (then AutoCAD gets chance to fire Idle event).
  • During background work execution, user could change AutoCAD's active document. When the side work completed, no circle will be drawn and remaining side work execution is cancelled, if the current active document is not the one from which the side work is started.
The code showed here is a simplified case. In reality, we usually need the side thread go to remote resources to grab some data and return the data back to let AutoCAD do something accordingly. In code here, I let the drawing being updated  when each side thread execution completes. If the data returned by the side work requires AutoCAD to a series of tasks, it may be better to store the returned data in a queue at each end of side thread execution. When all the expected side thread executions are done, the code then listens the Application.Idle event until AutoCAD is idle. At this moment, the code can grab data one by one from the queue and let AutoCAD do corresponding task. Before that, it might be good to notify user with remote data arrival and user can then decide the next move.



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.