Friday, October 16, 2015

Be Careful When Handling Document Events

In one of my resent CAD application development project, I needed to handle Document's event, namely, Document.CommandWillStart/CommandEnded/CommandCancelled events, in order to identify entities being changed by particular commands.

I have done this type of coding quite a few times before, so I did not expect this would cause any issue, although the code in the CommandWillStart/CommandEnded event handlers is rather complicated because of the business requirement.

During the application testing, as it turned out, the event handlers seemed getting lost from time to time, as if no event handler is attached to Document object, even the debug-stepping through clearly shows that Document.CommandWillStart/CommandEnded += [Event Handler] was executed correctly.

Also, when running the code, when the application changes from the state of command event being handled to the state of command event handler being lost, there is no exception being raised so that the application crashes.

I could only guess that this might have something to do with my complicated code inside the event handlers, but could not figure it out what it is and why the attached event handler could be lost, because there is no exception that stops the code execution.

After countless times of stepping through the code, I finally identify a line of code that was supposed to raise an exception in certain conditions. Instead of the code execution being break and an exception being raised, to my surprise, the code execution simply jumped to the end of the command event handler. If the event handler is CommandWillStart, the command simply goes ahead. However, all the code in the event handler would not run any more, as if the event handler was not attached to the Document.

To demonstrate this behaviour, see following code:

public class Commands
{
    private static bool _handlerAdded = false;
    private static int _count = 0;
 
    [CommandMethod("HandleCmd")]
    public static void HandleCommandEvents()
    {
        Document dwg = CadApp.DocumentManager.MdiActiveDocument;
 
        if (!_handlerAdded)
        {
            SetCommandEventHandler(true);
            dwg.Editor.WriteMessage(
                "\nCommandWillStart event handler attached");
            _count = 0;
        }
        else
        {
            SetCommandEventHandler(false);
            dwg.Editor.WriteMessage(
                "\nCommandWillStart event handler detached");
        }
    }
 
    private static void SetCommandEventHandler(bool addHandler)
    {
        Document dwg = CadApp.DocumentManager.MdiActiveDocument;
 
        if (addHandler)
        {
            dwg.CommandWillStart += Dwg_CommandWillStart;
            _handlerAdded = true;
        }
        else
        {
            if (_handlerAdded)
            {
                dwg.CommandWillStart -= Dwg_CommandWillStart;
            }
            _handlerAdded = false;
        }
    }
 
    private static void Dwg_CommandWillStart(object sender, CommandEventArgs e)
    {
        _count++;
        Document dwg = CadApp.DocumentManager.MdiActiveDocument;
        dwg.Editor.WriteMessage(
            "\n====Command {0} will start. Count {1}====", 
            e.GlobalCommandName, _count);
 
        if (_count==3)
        {
            throw new ApplicationException("Error");
        }
    }
}

With above shown code, as we can see, after executing command "HandleCmd" to enable the command event handling, whenever an AutoCAD command starts, the code in the Dwg_CommandWillStart event handler will be called. However, when the third command starts, an exception is thrown, which does not break the code execution (that is, to user, it seems nothing happens). Starting any command thereafter, the command would execute as expected, by the code in Dwg_CommandWillStart event handler is no longer called, as if the event handler is not attached to the document.

After the event handler being lost, event I run "HandleCmd" again, trying to remove and re-attach the event handler, it seemed that the event handler could not be attached to the document any more. The only way to get the event handler hooked up is the close and reopen the drawing.

If I catch the possible exception inside the event handler, like this:

private static void Dwg_CommandWillStart(object sender, CommandEventArgs e)
{
    _count++;
    Document dwg = CadApp.DocumentManager.MdiActiveDocument;
    dwg.Editor.WriteMessage(
        "\n====Command {0} will start. Count {1}====", 
        e.GlobalCommandName, _count);
 
    try
    {
        if (_count == 3)
        {
            throw new ApplicationException("Error");
        }
    }
    catch { }
}

then the event handler will not be lost.

Here what I observed is quite similar to the well-known case of IExtensionApplication.Initialize(): if an exception occurs in the event handler, AutoCAD simply swallows it silently and jump out the event handler; AutoCAD continues functioning, but the event handler attached to the document will be lost and cannot be re-attached. The only way to get the event handler back is to close the document and re-open it.

Therefore, we need to be very careful when writing code in the document command event handler to make sure all possible exception can be caught, just like we do in IExtensionApplication.Initialize().

To be clear: I did not try this with all Document events but 2 (Document.CommandWilLStart/CommandEnded), thus do not know if the handlers to other Document events behave the same way or not. Nor did I tried with Database events.



Thursday, September 24, 2015

Highlight Attributes in BlockReference

An interesting question is posted in Autodesk's user discussion forum here, asking how to highlight attributes of a block (BlockReference, of course). The code posted in the question indicates that calling AttributeReference.Highlight() does nothing, which I have never tried (and never needed to), but have verified it with my own code.

Obviously, the method Highlight() of AttributeReference, although being derived from Entity, has its own overridden implementation, so that it behaves the same as its owner, the BlockReference.

Now that AttributeReference.Highlight() does not work, I thought looking into Overrule could lead to easy solution.

Naturally, I turned to HighlightOverrule first, which I posted an article about it a few years ago here. However, seeing the code in that article, I realize that it will not work, because the code still need to call AttributeReference.Highlight() inside the overridden Overrule's Hightlight() method.

As alternative, I tried to use DrawableOverrule. In this try, I successfully made AttributeRefernce being drawn in different colors (thus, a sort of "highlighting"). However, I have difficulties to make the attribute text showing in the way being highlighted in AutoCAD.

Nevertheless, using DrawableOverrule is a viable solution to "highlight" attribute. Since I have found a even better solution, I am not going to show the DrawableOverrule for attribute in this article (maybe later, if I can manage a bit time).

The solution I post here is to use Transient Graphics. The code is rather simple and does not need much explanation.

Here is the class called AttributeHighlighter:

    1 using System;
    2 using System.Collections.Generic;
    3 using System.Linq;
    4 using Autodesk.AutoCAD.DatabaseServices;
    5 using Autodesk.AutoCAD.Geometry;
    6 using Autodesk.AutoCAD.GraphicsInterface;
    7 
    8 namespace HighlightAttribute
    9 {
   10     public class AttributeHighlighter : IDisposable
   11     {
   12         private TransientManager _tManager =
   13             TransientManager.CurrentTransientManager;
   14         private List<Drawable> _entities = new List<Drawable>();
   15 
   16         public void Dispose()
   17         {
   18             DisposeDrawables();
   19         }
   20 
   21         public void Highlight(IEnumerable<ObjectId> blkIds)
   22         {
   23             DisposeDrawables();
   24 
   25             CollectDrawables(blkIds);
   26 
   27             foreach (var ent in _entities)
   28             {
   29                 _tManager.AddTransient(
   30                     ent,
   31                     TransientDrawingMode.Highlight,
   32                     128,
   33                     new IntegerCollection());
   34             }
   35         }
   36 
   37         public void Unhighlight()
   38         {
   39             _tManager.EraseTransients(
   40                 TransientDrawingMode.Highlight,
   41                 128,
   42                 new IntegerCollection());
   43         }
   44 
   45         private void DisposeDrawables()
   46         {
   47             foreach (var ent in _entities)
   48             {
   49                 ent.Dispose();
   50             }
   51 
   52             _entities.Clear();
   53         }
   54 
   55         private void CollectDrawables(IEnumerable<ObjectId> ids)
   56         {
   57             Database db = ids.First().Database;
   58             using (var tran = db.TransactionManager.StartTransaction())
   59             {
   60                 foreach (var id in ids)
   61                 {
   62                     BlockReference bref =
   63                         tran.GetObject(id, OpenMode.ForRead)
   64                         as BlockReference;
   65 
   66                     if (bref == null) continue;
   67 
   68                     foreach (ObjectId attId in bref.AttributeCollection)
   69                     {
   70                         AttributeReference att = (AttributeReference)
   71                             tran.GetObject(attId, OpenMode.ForRead);
   72 
   73                         _entities.Add(att.Clone() as Drawable);
   74                     }
   75                 }
   76 
   77                 tran.Commit();
   78             }
   79         }
   80     }
   81 }

Then here is the command to use AttributeHighlighter:

    1 using System.Collections.Generic;
    2 using Autodesk.AutoCAD.ApplicationServices;
    3 using Autodesk.AutoCAD.DatabaseServices;
    4 using Autodesk.AutoCAD.Runtime;
    5 using Autodesk.AutoCAD.EditorInput;
    6 using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
    7 
    8 [assembly: CommandClass(typeof(HighlightAttribute.Commands))]
    9 
   10 namespace HighlightAttribute
   11 {
   12     public class Commands
   13     {
   14         [CommandMethod("MyAtt")]
   15         public static void SetAttHighlight()
   16         {
   17             Document dwg = CadApp.DocumentManager.MdiActiveDocument;
   18             Editor ed = dwg.Editor;
   19 
   20             IEnumerable<ObjectId> ids = SelectAllBlocks(ed);
   21             if (ids != null)
   22             {
   23                 using (AttributeHighlighter hl = new AttributeHighlighter())
   24                 {
   25                     hl.Highlight(ids);
   26                     ed.UpdateScreen();
   27                     ed.GetString("\nPress any key to continue...");
   28                     hl.Unhighlight();
   29                     ed.UpdateScreen();
   30                 }
   31             }
   32 
   33             Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
   34         }
   35 
   36         private static IEnumerable<ObjectId> SelectAllBlocks(Editor ed)
   37         {
   38             TypedValue[] vals = new TypedValue[]
   39             {
   40                 new TypedValue((int)DxfCode.Start,"INSERT")
   41             };
   42 
   43             PromptSelectionResult res = ed.SelectAll(new SelectionFilter(vals));
   44             if (res.Status == PromptStatus.OK)
   45                 return res.Value.GetObjectIds();
   46             else
   47                 return null;
   48         }
   49     }
   50 }

What the command does is select all blocks, which have attributes, and pass the selected blocks' ObjectId to AttributeHighlighter, which searches through all the blocks and create a set of clones of all attributes and uses these cloned attributes to draw Transient Graphics (highlight) on top of the attributes. To user's eye, it looks like the attributes are highlighted. See this short video clip.


Sunday, June 28, 2015

Moving to AutoCAD 2015/16 Issues: Quitting Application

I have been recently helping upgrading AutoCAD 2012 to AutoCAD 2015 in my office. During the process of moving up to AutoCAD 2015, I encountered a few interesting issues that actually break some of my existing AutoCAD .NET applications.

I have been quite busy these days and have no posted in my blog for quite a while. Hopefully I share some of my experiences with moving up to AutoCAD 2015/16 here.

The topic of this post is about calling Autodesk.AutoCAD.ApplicationServices.Application.Quit().

One of the add-in applications I developed is kind of like "auto-update" application. It has code to handle Application.Idle event.In the event handler, if certain condition is met, the code would call Application.Quit() to shut down AutoCAD. This application has been working well up to the last pre-2015 version of AutoCAD (actually, AutoCAD 2012. We never used AutoCAD 2013/14 in production).

However, this application stops working in AutoCAD 2015. To be exactly, it is the call to Application.Quit() in the event handler (handling Application.Idle event) hangs AutoCAD. When I say "hangs", it is literally "hangs", that is, AutoCAD freezes entirely and can only be shut down by going to Task Manager". I can also see in the Task Manager that AutoCAD spins quite some CPU cycles (30-50%) for nothing after Application.Quit() is called.

Below is the code to reproduce this behaviour.

    1 using System;
    2 using System.Windows.Forms;
    3 
    4 using Autodesk.AutoCAD.ApplicationServices;
    5 using Autodesk.AutoCAD.EditorInput;
    6 using Autodesk.AutoCAD.Runtime;
    7 using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
    8 
    9 [assembly: CommandClass(typeof(QuitAppInEvent.MyCommands))]
   10 [assembly: ExtensionApplication(typeof(QuitAppInEvent.MyCommands))]
   11 
   12 namespace QuitAppInEvent
   13 {
   14     public class MyCommands : IExtensionApplication
   15     {
   16         private static bool _quit = false;
   17 
   18         public void Initialize()
   19         {
   20             CadApp.Idle += CadApp_Idle;
   21         }
   22 
   23         public void Terminate()
   24         {
   25 
   26         }
   27 
   28         [CommandMethod("DoQuit")]
   29         public void RunMyCommand()
   30         {
   31             _quit = false;
   32 
   33             Document dwg = CadApp.DocumentManager.MdiActiveDocument;
   34             Editor ed = dwg.Editor;
   35 
   36             PromptKeywordOptions opt = new PromptKeywordOptions(
   37                 "Do you want to quit AutoCAD?");
   38             opt.Keywords.Add("Yes");
   39             opt.Keywords.Add("No");
   40             opt.Keywords.Default = "Yes";
   41             opt.AppendKeywordsToMessage = true;
   42 
   43             PromptResult res = ed.GetKeywords(opt);
   44             if (res.Status == PromptStatus.OK)
   45             {
   46                 if (res.StringResult == "Yes")
   47                 {
   48                     _quit = true;
   49                 }
   50             }
   51         }
   52 
   53         private void CadApp_Idle(object sender, EventArgs e)
   54         {
   55             if (_quit)
   56             {
   57                 DialogResult res = MessageBox.Show(
   58                     "Do you really want to quit AutoCAD?", "My Quit App",
   59                     MessageBoxButtons.YesNo, MessageBoxIcon.Question,
   60                     MessageBoxDefaultButton.Button2);
   61 
   62                 //Once "Yes" button is clicked AutoCAD freezes!
   63                 if (res == DialogResult.Yes)
   64                 {
   65                     CadApp.Idle -= CadApp_Idle;
   66                     CadApp.Quit();
   67                 }
   68                 else
   69                 {
   70                     _quit = false;
   71                 }
   72             }
   73         }
   74     }
   75 }

I also tried to call Application.Quit() in a normal CommandMethod like this:

   75         [CommandMethod("MyQuitCmd", CommandFlags.Session)]
   76         public static void MyQuitCommand()
   77         {
   78             Document dwg = CadApp.DocumentManager.MdiActiveDocument;
   79             Editor ed = dwg.Editor;
   80 
   81             PromptKeywordOptions opt = new PromptKeywordOptions(
   82                 "Do you want to quit AutoCAD?");
   83             opt.Keywords.Add("Yes");
   84             opt.Keywords.Add("No");
   85             opt.Keywords.Default = "Yes";
   86             opt.AppendKeywordsToMessage = true;
   87 
   88             PromptResult res = ed.GetKeywords(opt);
   89             if (res.Status == PromptStatus.OK)
   90             {
   91                 if (res.StringResult == "Yes")
   92                 {
   93                     CadApp.Quit();
   94                 }
   95             }
   96         }


This, of course, works as expected in AutoCAD 2015.

I also tried the code in AutoCAD 2016. The result is the same as with AutoCAD 2015.

So, I had to figure out a way to make my existing application work as before. Fortunately, the solution I found is very simple: calling COM API's AcadApplication.Quit() instead:

   53 private void CadApp_Idle(object sender, EventArgs e)
   54         {
   55             if (_quit)
   56             {
   57                 DialogResult res = MessageBox.Show(
   58                     "Do you really want to quit AutoCAD?", "My Quit App",
   59                     MessageBoxButtons.YesNo, MessageBoxIcon.Question,
   60                     MessageBoxDefaultButton.Button2);
   61 
   62                 //Once "Yes" button is clicked AutoCAD freezes!
   63                 if (res == DialogResult.Yes)
   64                 {
   65                     CadApp.Idle -= CadApp_Idle;
   66                     //CadApp.Quit();
   67                     dynamic comApp = CadApp.AcadApplication;
   68                     comApp.Quit();
   69                 }
   70                 else
   71                 {
   72                     _quit = false;
   73                 }
   74             }
   75         }


However, I cannot explain why .NET API's Quit() stops working in the event handler, while COM API's Quit() works, even both are the wrapper of C++ ObjectARX code. I guess the issue with .NET API might be a bug introduced by removing FIBER in AutoCAD 2015. To me, it is good enough that with the minor change (of using COM API's AcadApplication.Quit()) my existing application can still live with AutoCAD 2015/16.

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.

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.