August 06, 2007

A handy .NET class to help manage long operations in AutoCAD

This post was almost called "Generating Koch fractals in AutoCAD using .NET - Part 3", following on from Parts 1 & 2 of the series. But by the time I'd completed the code, I realised it to be of more general appeal and decided to provide it with a more representative title.

I started off by adding a progress meter and an escape key handler to the code in the last post. Then, while refactoring the code, I decided to encapsulate the functionality in a standalone class that could be dropped into pretty much any AutoCAD .NET project (although I've implemented it in C#, as usual).

So what we have is a new class called LongOperationManager, which does the following:

Displays and updates a progress meter (at the bottom left of AutoCAD's window)

Allowing you to set an arbitrary message and total number of operations

Listens for "escape" in case the user wants to interrupt the current operation

Here's the class implementation:

publicclassLongOperationManager :

IDisposable, System.Windows.Forms.IMessageFilter

{

// The message code corresponding to a keypress

constint WM_KEYDOWN = 0x0100;

// The number of times to update the progress meter

// (for some reason you need 600 to tick through

// for each percent)

constint progressMeterIncrements = 600;

// Internal members for metering progress

privateProgressMeter pm;

privatelong updateIncrement;

privatelong currentInc;

// External flag for checking cancelled status

publicbool cancelled = false;

// Constructor

public LongOperationManager(string message)

{

System.Windows.Forms.Application.

AddMessageFilter(this);

pm = newProgressMeter();

pm.Start(message);

pm.SetLimit(progressMeterIncrements);

currentInc = 0;

}

// System.IDisposable.Dispose

publicvoid Dispose()

{

pm.Stop();

pm.Dispose();

System.Windows.Forms.Application.

RemoveMessageFilter(this);

}

// Set the total number of operations

publicvoid SetTotalOperations(long totalOps)

{

// We really just care about when we need

// to update the timer

updateIncrement =

(totalOps > progressMeterIncrements ?

totalOps / progressMeterIncrements :

totalOps

);

}

// This function is called whenever an operation

// is performed

publicbool Tick()

{

if (++currentInc == updateIncrement)

{

pm.MeterProgress();

currentInc = 0;

System.Windows.Forms.Application.DoEvents();

}

// Check whether the filter has set the flag

if (cancelled)

pm.Stop();

return !cancelled;

}

// The message filter callback

publicbool PreFilterMessage(

ref System.Windows.Forms.Message m

)

{

if (m.Msg == WM_KEYDOWN)

{

// Check for the Escape keypress

System.Windows.Forms.Keys kc =

(System.Windows.Forms.Keys)(int)m.WParam &

System.Windows.Forms.Keys.KeyCode;

if (m.Msg == WM_KEYDOWN &&

kc == System.Windows.Forms.Keys.Escape)

{

cancelled = true;

}

// Return true to filter all keypresses

returntrue;

}

// Return false to let other messages through

returnfalse;

}

}

In terms of how to use the class... first of all you create an instance of it, setting the string to be shown on the progress meter (just like AutoCAD's ProgressMeter class). As the LongOperationManager implements IDisposable, then at the end you should either call Dispose or manage it's scope with the using() statement.

I chose to separate the setting of the total number of operations to be completed from the object's construction, as in our example we need to an initial pass before we know how many objects we're working with (and we want to at least put the label on the progress meter while we perform that initial pass).

Then we just call the Tick() method whenever we perform an operation - this updates the progress meter and checks for use of the escape key. The idea is that you set the total number of operations and then call Tick() for each one of those individual operations - the class takes care of how often it needs to update the progress meter. If it finds escape has been used, the Tick() method will return false.

That's about it, aside from the fact you can also query the "cancelled" property to see whether escape has been used.

Here's the basic approach:

LongOperationManager lom =

new LongOperationManager("Fractalizing entities");

using (lom)

{

...

lom.SetTotalOperations(totalOps);

...

while (true)

{

...

if (!lom.Tick())

{

ed.WriteMessage("\nFractalization cancelled.\n");

break;

}

}

}

Here's the code integrated into the previous example, with the significant lines in red: