Monday, December 27, 2010

Showing Progress Window When Running A Long Process

It is very often a command that does a complicated drawing/entities manipulation could take fair amount of time. During the command execution, it would be desired that something visual shows in AutoCAD to indicate the processing is in progress.

One option to do it is to use AutoCAD's progress meter, of course. However, many programmer would choose to show a small window with a progress bar. With this approach, the window can not only display a progress bar, but also display messages regarding the process execution.

This article presents a reusable progress window component that can be easily plugged into a long-running drawing process execution method.

The basic idea here is to define an interface (ILongProcessCommand) that raises events at beginning, ending and progressing of a process/command. Then the drawing manipulating process would be wrapped in a class that implements the said interface. The progress window component then will contain a reference of the drawing manipulating class (as the type of ILongProcessCommand) and subscribe the events (CommandStarted, CommandEnded and CommandProgress). The actual progress window will be shown, closed and updated in the event handler.

Since the goal is to create a reusable/pluggable progress window component, I created 2 projects in a VS solution:

1. Project "ProgressWindow"

As afore-mentioned, an interface is defined. Along with the interface are delegates and classes used for event raising/handling. Here is the code:

using System;

namespace ProgressWindow
{
    public interface ILongProcessCommand
    {
        event CommandStartedEventHandler CommandStarted;
        event CommandEndedEventHandler CommandEnded;
        event CommandProgressEventHandler CommandProgress;
    }

    public delegate void CommandStartedEventHandler(
        object sender, CommandStartEventArgs e);

    public delegate void CommandEndedEventHandler(
        object sender, EventArgs e);

    public delegate void CommandProgressEventHandler(
        object sender, CommandProgressEventArgs e);

    public class CommandStartEventArgs : System.EventArgs
    {
        public string WindowTitle { set; get; }
        public int ProgressMinValue { set; get; }
        public int ProgressMaxValue { set; get; }
        public int PrograssInitValue { set; get; }
        public System.Windows.Forms.ProgressBarStyle ProgressBarStyle { set; get; }

        public CommandStartEventArgs()
        {
            WindowTitle = "Command in Progress";
            ProgressMinValue = 0;
            ProgressMaxValue = 0;
            ProgressBarStyle = System.Windows.Forms.ProgressBarStyle.Continuous;
        }
    }

    public class CommandProgressEventArgs : System.EventArgs
    {
        public int ProgressValue { set; get; }
        public string ProgressTitle { set; get; }
        public string ProgressMessage { set; get; }

        public CommandProgressEventArgs()
        {
            ProgressValue = 0;
            ProgressTitle = "";
            ProgressMessage = "";
        }
    }
}

Then, a window form is added. It has a progress bar and 2 labels. The form's ControlBox property is set to False, so that it canot be closed by user accidently. See picture below:


Here is the code of the form:

using System.Windows.Forms;

namespace ProgressWindow
{
    public partial class dlgProgress : Form
    {
        public dlgProgress()
        {
            InitializeComponent();
        }

        public string ProgressTitleMessage
        {
            set { lblTitle.Text = value; }
            get { return lblTitle.Text; }
        }

        public string ProgressDetailMessage
        {
            set { lblMessage.Text = value; }
            get { return lblMessage.Text; }
        }

        public int ProgressMinValue
        {
            set { pBar.Minimum = value; }
            get { return pBar.Minimum; }
        }

        public int ProgressMaxValue
        {
            set { pBar.Maximum=value;}
            get { return pBar.Maximum;}
        }

        public int ProgressValue
        {
            set { pBar.Value = value; }
            get { return pBar.Value; }
        }
    }
}

Finally, I added a class "ProgressIndicator", shown as following:

using System;

namespace ProgressWindow
{
    public class ProgressIndicator : IDisposable 
    {
        private ILongProcessCommand _cmdObject = null;
        private dlgProgress _window = null;

        public ProgressIndicator(ILongProcessCommand cmdObj)
        {
            _cmdObject = cmdObj;

            WireCommandEvents();
        }

        #region IDisposable Members

        public void Dispose()
        {
            if (_window!=null)
            {
                _window.Dispose();
            }
        }

        #endregion

        private void WireCommandEvents()
        {
            if (_cmdObject != null)
            {
                _cmdObject.CommandStarted += 
                    new CommandStartedEventHandler(_cmdObject_CommandStarted);

                _cmdObject.CommandEnded += 
                    new CommandEndedEventHandler(_cmdObject_CommandEnded);

                _cmdObject.CommandProgress +=
                    new CommandProgressEventHandler(_cmdObject_CommandProgress);
            }
        }

        void _cmdObject_CommandStarted(object sender, CommandStartEventArgs e)
        {
            if (_window!=null) _window.Dispose();

            _window = new dlgProgress();
            _window.Text = e.WindowTitle;
            _window.ProgressMinValue = e.ProgressMinValue;
            _window.ProgressMaxValue = e.ProgressMaxValue;
            _window.ProgressValue = e.PrograssInitValue;

            Autodesk.AutoCAD.ApplicationServices.Application.ShowModelessDialog(_window);

        }

        void  _cmdObject_CommandProgress(object sender, CommandProgressEventArgs e)
        {
            if (_window == null) return;

            _window.ProgressTitleMessage = e.ProgressTitle;
            _window.ProgressDetailMessage = e.ProgressMessage;
            _window.ProgressValue = e.ProgressValue;

            _window.Refresh();
            System.Windows.Forms.Application.DoEvents();
        }

        void _cmdObject_CommandEnded(object sender, EventArgs e)
        {
            if (_window == null) return;

            _window.Close();
        }
    }
}

Notice the constructor of the class take an ILongProcessCommand type as its argument and wire itself to the events of the ILongProcessCommand object. As long as the ILongProcessCommand fires an event (CommandStarted, CommandEnded, or CommandProgress), this class will show/close/update the progress window accordingly.

That is all for the reusable/pluggable progress window component. After building the project, it is ready to be used in drawing manipulating process as progress indicator.

2. Project "ProgressWindowSample"

This project is a regulaer AutoCAD managed DLL project. I added the first project as reference. I created a class "TestProcess". It has a method that is supposed to do some time consuming drawing data manipulating. To make things simple, I simply call Editor.SelectAll() to get ObjectId of all entities in the drawing into an ObjectIdCollection and then loop through each ObjectId to open eacg entity. If the drawing contains tens of thousands entities, the process would take a while. The kind of process would be good candidate process we want to show a progress indicator during the process.

Here is the code of class "TestProcess":

using System;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;

using ProgressWindow;

namespace ProgressWindowSample
{
    public class TestProcess : ILongProcessCommand
    {
        private ObjectIdCollection _objIds = null;
        private Document _dwg;

        public TestProcess(Document dwg)
        {
            _dwg = dwg;
        }

        #region ILongProcessCommand Members

        public event CommandStartedEventHandler CommandStarted;

        public event CommandEndedEventHandler CommandEnded;

        public event CommandProgressEventHandler CommandProgress;

        #endregion

        public void DoSomeWork()
        {
            PromptSelectionResult res = _dwg.Editor.SelectAll();
            if (res.Status != PromptStatus.OK) return;

            _objIds = new ObjectIdCollection(res.Value.GetObjectIds());

            using (ProgressIndicator progress=new ProgressIndicator(this))
            {
                //Raise CommandStarted event that causes progress window to show
                if (CommandStarted != null)
                {
                    CommandStartEventArgs args = new CommandStartEventArgs();
                    args.WindowTitle = "Process All Entities in Current Drawing";
                    args.ProgressMaxValue = _objIds.Count;
                    args.ProgressMinValue = 0;
                    args.PrograssInitValue = 0;

                    CommandStarted(this, args);
                }

                ProcessDrawing(_objIds);

                //Raise CommandEnded event that closes progress widow
                if (CommandEnded != null)
                {
                    CommandEnded(this, EventArgs.Empty);
                }
            }
        }

        private void ProcessDrawing(ObjectIdCollection ids)
        {
            int count = 0;
            int total=ids.Count;

            using (DocumentLock lck = _dwg.LockDocument())
            {
                using (Transaction tran = 
                    _dwg.Database.TransactionManager.StartTransaction())
                {
                    foreach (ObjectId id in ids)
                    {
                        count++;
                        string entClass = id.ObjectClass.Name;

                        if (CommandProgress != null)
                        {
                            CommandProgressEventArgs args = new CommandProgressEventArgs();
                            args.ProgressTitle = "Processing entity: " + entClass;
                            args.ProgressMessage = 
                                count + " out of " + total + " entities processed...Please wait...";

                            args.ProgressValue = count;

                            CommandProgress(this, args);
                        }

                        Entity ent = (Entity)tran.GetObject(id, OpenMode.ForRead);

                        //Do something....
                        for (int i = 0; i < 10000; i++)
                        {
                            int x = i + 1000;
                            x = 0;
                        }
                    }

                    tran.Commit();
                }
            }
        }
    }
}

Notice that this class implements ILongProcessCommand interface. It is the resposibility of this class to raise appropriate event using the drawing data process (in this case, in public method "DoSomeWork()"), and pass suitable data to the EventArgs.

Now, let's put everything together in a command method to see the actual effect:

using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;

namespace ProgressWindowSample
{
    public class TestCommand
    {
        [CommandMethod("DoWork")]
        public static void RunLongCommand()
        {
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;

            try
            {
                TestProcess prog = new TestProcess(dwg);
                prog.DoSomeWork();

                dwg.Editor.WriteMessage("\nProcess completed");
            }
            catch (System.Exception ex)
            {
                dwg.Editor.WriteMessage("\nError: " + ex.Message);
            }
        }
    }
}

Now, build the project, start AutoCAD, netload "ProgressWindowSample.dll". Open a drawing with a lot of entities in it and enter command "DoWork". This video clip shows the progress window's effect.

As you can see, the ProgressIndicator component knows nothing about the actual long processing and only responds to the ILongProcessCommand's events to show/close/update progress window. As long as you wrap the long running CAD operation into a class that implements ILongProcessCommand and fire up appropriate event when needed, the progress window will automatically present the progress.

3 comments:

Anonymous said...

Hi Norman,
thanks for your code, but in my application this leads to a drop in throughput from almost 2000 elements per second to around 550 per second. I guess it has to do with having to instantiate the event handler over and over again. Isn't there a way to make this a bit more performant except of doing a
'if (CommandProgress != null && myCounter % 50 == 0)'?

Regards, Bernd

Norman Yuan said...

Bernd,

The drop of throughput is expected, because of the code to refresh progress window and call to System.Windows.Forms.Application.DoEvents(), as we know that updating window visually is a very expensive operation.

If you use a window form to show progress as my code or similar, I am afraid this is the trade-off. Yes, you could reduce the the frequency of CommandProgress event being fired by only firing it every a few counts of items being processed, as you have already thought.

Or, you may consider to use AutoCAD built-in progress showing mechnique - progress meter, which I guess should have much less impact on throughput.

But do remember, visually showing progress of long process has big psycological effect to user: user would feel an actual slower, visual process is fast than a process that freezes the screen, even it is actaully faster.

Anonymous said...

Hi Norman,

thanks for your input, I wasn't aware that updating the UI is so expensive.
Nonetheless I'll probably stick to your code, as firing the event only every 50th element, as I do, leads to an (for me) acceptable performance drop of around 15% (and as you pointed out, at least the user sees something happening).

Regards, Bernd

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.