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.

Thursday, November 18, 2010

Dragging a Line in Certain Angle

This article is inspired by a question posted in the Autodesk's Visual Basic Customization user forum. Baiscally, while drawing a line, after picking the start point, the user wants the ghost line only stretch in certain direction/angle, similar effect as the Ortho-On mode. Well, as a programmer, not a drafter/designer, I am not very sure how often this kind of fuctionality is desired in AutoCAD use. If one wants to draw a line that he knows the line's start/end point, or start point, lenght and direction/angle, he can alway enter them easily at command line. However I can imagine that during designing (nt drafting) process, the designer may want to draw a line, starting at a known point and she'd like it to be stretched at certain angle with undecided length.

Regardless it possible use/benefit an AutoCAD user may find, here is the code to do this. Yes, as you may have guessed, I used TransientGraphics again.

Here is the class that do the dynamic dragging. At the end of AngledDrag() call, the class provides two points (Point3d) - StartPoint and EndPoint as public read-only properties for the calling procedure to use.

using System;

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

namespace AngleLockedDrag
{
    public class AngledDrag
    {
        private Document _dwg;
        private Database _db;
        private Editor _editor;

        private Point3d _startPoint = new Point3d(0.0, 0.0, 0.0);
        private Point3d _endPoint = new Point3d(0.0, 0.0, 0.0);

        private double _dragAngle = 45.0;

        private Line _dragLine = null;
        private int _colorIndex = 1;

        public AngledDrag(Document dwg)
        {
            _dwg = dwg;
            _db = dwg.Database;
            _editor = dwg.Editor;
        }

        public Point3d StartPoint
        {
            get { return _startPoint; }
        }

        public Point3d EndPoint
        {
            get { return _endPoint; }
        }

        #region public methods

        public bool DragAtAngle()
        {
            _endPoint = _startPoint;

            _editor.PointMonitor += 
                new PointMonitorEventHandler(Editor_PointMonitor);

            try
            {
                //Get end point
                if (GetEndPoint())
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
            finally
            {
                ClearTransientGraphics();
                _editor.PointMonitor -= Editor_PointMonitor;
            }
        }

        #endregion

        #region private methods

        private void Editor_PointMonitor(
            object sender, PointMonitorEventArgs e)
        {
            DrawDragLine(e.Context.RawPoint);
            if (_dragLine != null)
            {
                e.AppendToolTipText("Angle: " + 
                    _dragAngle.ToString() + "\nLength: " + 
                    _dragLine.Length.ToString());
            }
            else
            {
                e.AppendToolTipText("");
            }
        }

        private void DrawDragLine(Point3d mousePoint)
        {
            ClearTransientGraphics();

            Point3d pt = CalculateEndPoint(mousePoint);

            _dragLine = new Line(_startPoint, pt);
            _dragLine.SetDatabaseDefaults(_db);
            _dragLine.ColorIndex = _colorIndex;

            IntegerCollection col = new IntegerCollection();
            TransientManager.CurrentTransientManager.AddTransient(
                _dragLine, TransientDrawingMode.Highlight, 128, col);

            //whenever the dragged line updated, reset _endPoint
            _endPoint = pt;
        }

        private void ClearTransientGraphics()
        {
            if (_dragLine != null)
            {
                IntegerCollection col = new IntegerCollection();
                TransientManager.CurrentTransientManager.
                    EraseTransient(_dragLine, col);

                _dragLine.Dispose();
                _dragLine = null;
            }
        }

        private Point3d CalculateEndPoint(Point3d mousePoint)
        {
            Point3d pt = mousePoint;

            if (_dragAngle <= 90.0 || _dragAngle >= 270.0)
            {
                if (mousePoint.X <= _startPoint.X)
                {
                    pt = _startPoint;
                }
                else
                {
                    if (_dragAngle <= 45.0 || _dragAngle >= 315.0)
                    {
                        double y = (mousePoint.X - _startPoint.X) * 
                            Math.Tan(_dragAngle * Math.PI / 180);
                        pt = new Point3d(
                            mousePoint.X, _startPoint.Y + y, 0.0);
                    }
                    else
                    {
                        if (_dragAngle > 45.0 && _dragAngle <= 90.0)
                        {
                            if (mousePoint.Y < _startPoint.Y)
                            {
                                pt = _startPoint;
                            }
                            else
                            {
                                double x = (mousePoint.Y - _startPoint.Y) / 
                                    Math.Tan(_dragAngle * Math.PI / 180);
                                pt = new Point3d(
                                    _startPoint.X + x, mousePoint.Y, 0.0);
                            }
                        }
                        else
                        {
                            if (mousePoint.Y > _startPoint.Y)
                            {
                                pt = _startPoint;
                            }
                            else
                            {
                                double x = (mousePoint.Y - _startPoint.Y) / 
                                    Math.Tan(_dragAngle * Math.PI / 180);
                                pt = new Point3d(
                                    _startPoint.X + x, mousePoint.Y, 0.0);
                            }
                        }
                    }

                    return pt;
                }
            }

            if (_dragAngle >= 90.0 && _dragAngle <= 270.0)
            {
                if (mousePoint.X >= _startPoint.X)
                {
                    pt = _startPoint;
                }
                else
                {
                    if (_dragAngle >= 135.0 && _dragAngle <= 225.0)
                    {
                        double y = (mousePoint.X - _startPoint.X) * 
                            Math.Tan(_dragAngle * Math.PI / 180);
                        pt = new Point3d(
                            mousePoint.X, _startPoint.Y + y, 0.0);
                    }
                    else
                    {
                        if (_dragAngle >=90.0 && _dragAngle < 135.0)
                        {
                            if (mousePoint.Y <= _startPoint.Y)
                            {
                                pt = _startPoint;
                            }
                            else
                            {
                                double x = (mousePoint.Y - _startPoint.Y) / 
                                    Math.Tan(_dragAngle * Math.PI / 180);
                                pt = new Point3d(
                                    _startPoint.X + x, mousePoint.Y, 0.0);
                            }
                        }
                        else
                        {
                            if (mousePoint.Y >= _startPoint.Y)
                            {
                                pt = _startPoint;
                            }
                            else
                            {
                                double x = (mousePoint.Y - _startPoint.Y) / 
                                    Math.Tan(_dragAngle * Math.PI / 180);
                                pt = new Point3d(
                                    _startPoint.X + x, mousePoint.Y, 0.0);
                            }
                        }
                        
                    }

                    return pt;
                }
            }

            return pt;
        }

        private bool GetEndPoint()
        {
            //endPoint = new Point3d();

            bool go = true;
            bool picked = false;

            while (go)
            {
                PromptPointOptions opt = new PromptPointOptions("\nPick point:");
                //opt.BasePoint = _startPoint;
                //opt.UseBasePoint = true;
                opt.Keywords.Add("Start point");
                opt.Keywords.Add("Angle");
                opt.Keywords.Add("End point");
                opt.Keywords.Add("eXit");
                opt.Keywords.Default = "End point";
                opt.AppendKeywordsToMessage = true;
                opt.AllowArbitraryInput = false;
                opt.AllowNone = false;

                PromptPointResult res = _editor.GetPoint(opt);
                if (res.Status == PromptStatus.Cancel)
                {
                    go = false; ;
                }
                else
                {
                    switch (res.Status)
                    {
                        case PromptStatus.Keyword:
                            //_editor.WriteMessage("\n" + res.StringResult);
                            if (res.StringResult.StartsWith("Start"))
                            {
                                SetStartPoint();
                                go = true;
                            }
                            if (res.StringResult.StartsWith("Angle"))
                            {
                                SetAngle();
                                go = true;
                            }
                            if (res.StringResult.StartsWith("eXit"))
                            {
                                go = false;
                            }
                            break;
                        case PromptStatus.OK:
                            //endPoint = res.Value;
                            picked = true;
                            go = false;
                            break;
                        default:
                            go = true;
                            break;
                    }
                }
            }

            return picked;
        }

        private void SetStartPoint()
        {
            ClearTransientGraphics();
            _editor.PointMonitor -= Editor_PointMonitor;

            PromptPointOptions opt = 
                new PromptPointOptions("\nStart point:");
            PromptPointResult res = _editor.GetPoint(opt);
            if (res.Status == PromptStatus.OK)
            {
                _startPoint = res.Value;
            }

            _editor.PointMonitor += 
                new PointMonitorEventHandler(Editor_PointMonitor);
        }

        private void SetAngle()
        {
            ClearTransientGraphics();
            _editor.PointMonitor -= Editor_PointMonitor;

            PromptDoubleOptions opt = 
                new PromptDoubleOptions("\nEnter drag-angle in degree [" + 
                    _dragAngle.ToString() + "]: ");
            opt.AllowNegative = false;
            opt.AllowZero = true;
            opt.AllowNone = true;

            PromptDoubleResult res = _editor.GetDouble(opt);

            if (res.Status == PromptStatus.OK)
            {
                _dragAngle = res.Value;
                if (_dragAngle > 360.0) _dragAngle -= 360.0;
            }

            _editor.PointMonitor += 
                new PointMonitorEventHandler(Editor_PointMonitor);
        }

        #endregion
    }
}

Here is the command class that uses the AngleLockedDrag class to draw a Line in AutoCAD.

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;

[assembly: CommandClass(typeof(AngleLockedDrag.DragCommand))]

namespace AngleLockedDrag
{
    public class DragCommand
    {
        [CommandMethod("AngledDrag")]
        public void RunThisMethod()
        {
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;

            AngledDrag drag = new AngledDrag(dwg);
            try
            {
                if (drag.DragAtAngle())
                {
                    GenerateLine(dwg, drag.StartPoint, drag.EndPoint);

                    dwg.Editor.WriteMessage("\nMyCommand executed.");
                }
            }
            catch (Autodesk.AutoCAD.Runtime.Exception ex)
            {
                dwg.Editor.WriteMessage("\nError: {0}\n", ex.Message);
            }
        }

        private static void GenerateLine(
            Document dwg, Point3d startPt, Point3d endPt)
        {
            using (Transaction tran = 
                dwg.Database.TransactionManager.StartTransaction())
            {
                BlockTableRecord br = (BlockTableRecord)tran.GetObject(
                    dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
                
                Line line = new Line(startPt, endPt);
                line.SetDatabaseDefaults(dwg.Database);

                br.AppendEntity(line);
                tran.AddNewlyCreatedDBObject(line, true);
                tran.Commit();
            }
        }
    }
}

Here is the video clip that shows the "angled dragging" effect.

Friday, November 5, 2010

Dynamically Draw An Array Of Entities

One of the exciting things when an AutoCAD VBA programmer moves into AutoCAD .NET API world is that he/she suddenly gain a capability to create dynamic visual hint (ghost image) when the code ask user to interact with the drawing editor, namely the capability to create JIG. Besides, since AutoCAD 2009 introduced TransientGraphics, it is even easier do add visual hint in the code when the user has to interact with AutoCAD editor during code execution.

In this post I am going to show a piece of code that creates an array of entities in following operation steps:

1. User pick an entity he/she wants to create an array of copy of this entity;
2. The user does not know how many entities could fit into a space, but he/she has a desired distance between each 2 entities. Or the user simply does not bother to calculate how many entities could fit in, he/she just want to move the mouse and see how the array of entities fits in a give space. So, the user would enter a desired space increment for the entities in the array;
3. The user pick a base point;
4. The user moves the mouse around, the ghost image of an array of entities shows dynamically, which automatically show the count of entities in the array, depending on how far the mouse pointer is from the base point;
5. If user clicks the mouse again, an array of entity copies is created. If user cancels the pick for the second point, the ghost image is gone, no entity array is created.

There is a link at the bottom of this article that leads you to see a video clip of the result of running the code.

Again, I use TransientGraphics (I just love it!) to achieve my goal. Here is the code.

Firstly, I created an Interface IDynamicDrawTool. It is not a must. Currently, the code only create an array of entities along a straight line. Later I could create the array along an arc, circle...I would like all the later possible tool all implement this interface.

namespace DynamicDrawTool
{
    public interface IDynamicDrawTool
    {
        void DrawEntities();
    }
}

Then here is the command class:

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

[assembly: CommandClass(typeof(DynamicDrawTool.DynamicDrawCommands))]

namespace DynamicDrawTool
{
    public class DynamicDrawCommands
    {
        [CommandMethod("DynDraw")]
        public static void RunThisMethod()
        {
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;

            Editor ed = dwg.Editor;

            //Pick and entity
            PromptEntityOptions opt = new PromptEntityOptions
                ("\nPick a source entity:");

            PromptEntityResult res = ed.GetEntity(opt);

            if (res.Status != PromptStatus.OK) return;

            IDynamicDrawTool drawTool = new LinearDynamicDrawTool(dwg, res.ObjectId);

            try
            {
                drawTool.DrawEntities();

                dwg.Editor.WriteMessage(
                    "\nMyCommand executed successfully.");
            }
            catch (Autodesk.AutoCAD.Runtime.Exception ex)
            {
                dwg.Editor.WriteMessage(
                    "\nMyCommand execution failed:\n" + ex.Message);
            }
        }
    }
}

Finally, the code doing the real work:

using System;
using System.Collections.Generic;

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

namespace DynamicDrawTool
{
    public class LinearDynamicDrawTool : IDynamicDrawTool 
    {
        private Document _dwg;
        private Editor _editor;
        private ObjectId _sourceEntId;

        private Point3d _startPoint;
        private Point3d _endPoint;
        private double _increment = 0.0;

        private Line _guideLine = null;
        private List _clonedEntities = new List();

        private int _colorIndex = 1;
        private int _originalColorIndex = 0;

        public LinearDynamicDrawTool(Document dwg, ObjectId sourceEntId)
        {
            _dwg = dwg;
            _editor = _dwg.Editor;
            _sourceEntId = sourceEntId;
        }

        public int GuideLineColorIndex
        {
            set { _colorIndex = value; }
            get { return _colorIndex; }
        }

        public void DrawEntities()
        {
            //Pick start point
            if (!GetPoint("Pick start point:", out _startPoint)) return;

            //Get incremting distance
            if (!GetIncrement(out _increment)) return;

            //Hook up to PointerMoniter event
            _editor.PointMonitor += 
                new PointMonitorEventHandler(_editor_PointMonitor);

            try
            {
                //Pick end point
                if (GetPoint("Pick end point:", out _endPoint))
                {
                    //Draw real entities exactly as 
                    //the transient graphics shows
                    AddEntities();
                }
            }
            finally
            {
                //Clear transient graphics and remove PointMonitor handler
                ClearTransientGraphics();
                _editor.PointMonitor -= _editor_PointMonitor;
            }
        }

        #region private methods: draw transient graphics

        private void _editor_PointMonitor(
            object sender, PointMonitorEventArgs e)
        {
            DrawTransientGrapgics(e.Context.RawPoint);
        }

        private void DrawTransientGrapgics(Point3d pt)
        {
            //Clear existing transient graphics
            ClearTransientGraphics();

            //Draw guideline
            _guideLine = new Line(_startPoint, pt);
            _guideLine.SetDatabaseDefaults(_dwg.Database);
            _guideLine.ColorIndex = _colorIndex;

            IntegerCollection col = new IntegerCollection();
            TransientManager.CurrentTransientManager.AddTransient(
                _guideLine, TransientDrawingMode.DirectShortTerm, 128, col);

            //Draw cloned entities
            DrawClonedEntityTransientGraphics(pt);
        }

        private void DrawClonedEntityTransientGraphics(Point3d pt)
        {
            //Calculate count of cloned entities
            int count = CalculateCloneCount(pt);
            if (count < 1) return;

            Entity sourceEnt = GetSourceEntity();
            _originalColorIndex = sourceEnt.ColorIndex;

            //Draw cloned entities as transient graphics
            for (int i = 1; i <= count; i++)
            {
                Entity ent = sourceEnt.Clone() as Entity;
                ent.ColorIndex = _colorIndex;

                //Move to target location
                SetClonedEntityPosition(i, ent);

                //Draw as transient graphics
                IntegerCollection col = new IntegerCollection();
                TransientManager.CurrentTransientManager.AddTransient(
                    ent, TransientDrawingMode.DirectShortTerm, 128, col);

                _clonedEntities.Add(ent);
            }
        }

        private int CalculateCloneCount(Point3d pt)
        {
            double dis = _startPoint.DistanceTo(pt);

            if (dis <= _increment)
                return 0;
            else
                return Convert.ToInt32(Math.Floor(dis / _increment));
        }

        private Entity GetSourceEntity()
        {
            Entity ent = null;

            using (Transaction tran = 
                _dwg.Database.TransactionManager.StartTransaction())
            {
                ent = (Entity)tran.GetObject(_sourceEntId, OpenMode.ForRead);
                tran.Commit();
            }

            return ent;
        }

        private void SetClonedEntityPosition(int index, Entity ent)
        {
            double dist = _increment * index;
            Point3d pt = _guideLine.GetPointAtDist(dist);

            ent.TransformBy(
                Matrix3d.Displacement(_startPoint.GetVectorTo(pt)));
        }

        private void ClearTransientGraphics()
        {
            //Clear guide line
            if (_guideLine != null)
            {
                IntegerCollection col = new IntegerCollection();
                TransientManager.CurrentTransientManager.
                    EraseTransient(_guideLine, col);

                _guideLine.Dispose();
                _guideLine = null;
            }

            //Clear cloned entities
            foreach (Entity ent in _clonedEntities)
            {
                if (ent != null)
                {
                    IntegerCollection col = new IntegerCollection();
                    TransientManager.CurrentTransientManager.
                        EraseTransient(ent, col);

                    ent.Dispose();
                }
            }

            _clonedEntities.Clear();
        }

        private void AddEntities()
        {
            using (DocumentLock lk = _dwg.LockDocument())
            {
                using (Transaction tran = 
                    _dwg.Database.TransactionManager.StartTransaction())
                {
                    BlockTableRecord br = 
                        (BlockTableRecord)tran.GetObject(
                        _dwg.Database.CurrentSpaceId, OpenMode.ForWrite);

                    foreach (Entity ent in _clonedEntities)
                    {
                        Entity newEnt = ent.Clone() as Entity;
                        newEnt.SetDatabaseDefaults(_dwg.Database);
                        newEnt.ColorIndex = _originalColorIndex;

                        br.AppendEntity(newEnt);
                        tran.AddNewlyCreatedDBObject(newEnt, true);
                    }

                    tran.Commit();
                }
            }
        }

        #endregion

        #region private methods: miscellaneous

        private bool GetPoint(string prompt, out Point3d point)
        {
            point = new Point3d(0.0, 0.0, 0.0);

            PromptPointOptions opt = 
                new PromptPointOptions("\n" + prompt);
            opt.AllowNone = false;

            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                point = res.Value;
                return true;
            }

            return false;
        }

        private bool GetIncrement(out double increment)
        {
            increment = 0.0;

            PromptDoubleOptions opt = 
                new PromptDoubleOptions("\nIncrementing distance:");

            PromptDoubleResult res = _editor.GetDouble(opt);

            if (res.Status == PromptStatus.OK)
            {
                increment = res.Value;
                return true;
            }

            return false;
        }

        #endregion
    }
}

Click here to see the a video clip showing how it works.

Obviously, there are more can be done to improve it. I have exposed ColorIndex as public property so that we can set the color of guide line/ghost image of the entities prior to calling DrawEntities() method. We can also do the similar thing to set LineWeight or line width (if it is polyline), LineType (use dash line would be more in line with AutoCAD standard).

Of course the user input part could also be enhanced to allow user to try different distance increment during mouse move.

Tuesday, September 28, 2010

Pluggable PaletteSet

In ObjectARX .NET API, AutoCAD.Windows.PaletteSet class makes creating dockable floating winidow in AutoCAD a pretty easy thing to do. Many AutoCAD programmers use PaletteSet as a UI container to host a series of Palettes (Windows Form User Controls, usually).

Prior to AutoCAD 2009, PaletteSet is sealed class, e.g. it cannot be inherited as a base class to derive your own custom PaletteSet. Since AutoCAD 2009, PaletteSet class is not sealed any more. This opens a possiblity for our AutoCAD programmer to create a custom, generic PaletteSet UI container once, and develop pluggable palette based on business need and plug into the PaletteSet when needed (without any code change to the PaletteSet).

In this article, I'll show how to use interface to define a pluggable PaletteSet. Interface is commonly used to define a set operations/properties for different classes to implement. It is one of the OO programming basic concept and used in .NET programming very often. However, there are many AutoCAD programmers who may not be a professional software developers and may too focused on AutoCAD API itself and did not explore some "advanced" programming technique enough, such as using "Interface" to simplify development tasks.

For this article, I demonstrate how to use Interface to create a pluggable PaletteSet. In Visual Stadio 2008, I started a class library project called MyPaletteSet, which includes 3 code files.

First code file is a interface that defines Palette that will be hosted in the pluggable PaletteSet: IThePalette. Later, when creating a Win Form UserControl as Palette, the UserControl will implement the interface. This, no matter how do you design your UserControls and how different they would be, to the hosting PaletteSet, they all are IThePalette. That is, the hosting PaletteSet does not have knowledge of each individual UserControl it hosts, as long as the UserControl is an IThePalette.

Here is the code:

Code Snippet
  1. namespace MyPaletteSet
  2. {
  3.     public interface IThePalette
  4.     {
  5.         ThePaletteSet PaletteSet { set; get; }
  6.         string PaletteName { get; }
  7.         void Close();
  8.         void ClosePaletteSet();
  9.     }
  10. }

Second code file is the custom PaletteSet, derived from Autodesk.AutoCAD.Windows.PaletteSet. As aforementioned, it is only possible when you use AutoCAD 2009 or later.

Here is the code:

Code Snippet
  1. using System;
  2. using System.Drawing;
  3. using System.Collections.Generic;
  4.  
  5. using Autodesk.AutoCAD.Windows;
  6. using Autodesk.AutoCAD.ApplicationServices;
  7.  
  8. namespace MyPaletteSet
  9. {
  10.     public class ThePaletteSet : PaletteSet
  11.     {
  12.         private static Guid _guid =
  13.             new Guid("B9169E25-3EC1-442F-B518-46B2DA174A2F");
  14.         private DocumentCollection _dwgManager = null;
  15.         private List<IThePalette> _paltettes = new List<IThePalette>();
  16.  
  17.         public event DocumentCollectionEventHandler DwgBecameCurrent;
  18.  
  19.         public ThePaletteSet()
  20.             : base("ThePaletteSet",null, _guid)
  21.         {
  22.             this.Style = PaletteSetStyles.ShowAutoHideButton |
  23.                 PaletteSetStyles.ShowCloseButton |
  24.                 PaletteSetStyles.Snappable;
  25.  
  26.             this.Opacity = 100;
  27.             this.Dock = DockSides.None;
  28.             this.DockEnabled = DockSides.None;
  29.  
  30.             this.Size = new Size(500, 400);
  31.             this.MinimumSize = new Size(250, 200);
  32.  
  33.             _dwgManager = Autodesk.AutoCAD.ApplicationServices
  34.                 .Application.DocumentManager;
  35.  
  36.           //Handle DocumentCollection events to bubble up the event for IThePalette
  37.             _dwgManager.DocumentBecameCurrent +=
  38.                 new DocumentCollectionEventHandler(_dwgManager_DocumentBecameCurrent);
  39.         }
  40.  
  41.         private void _dwgManager_DocumentBecameCurrent(
  42.             object sender, DocumentCollectionEventArgs e)
  43.         {
  44.             if (DwgBecameCurrent != null)
  45.             {
  46.                 DwgBecameCurrent(this, e);
  47.             }
  48.         }
  49.  
  50.         public void AddPalette(IThePalette palette)
  51.         {
  52.             bool exists = false;
  53.             foreach (IThePalette plt in _paltettes)
  54.             {
  55.                 if (plt.PaletteName.ToUpper() == palette.PaletteName.ToUpper())
  56.                 {
  57.                     exists = true;
  58.                     break;
  59.                 }
  60.             }
  61.  
  62.             if (!exists)
  63.             {
  64.                 System.Windows.Forms.Control ctl =
  65.                     palette as System.Windows.Forms.Control;
  66.  
  67.                 //Add to paletteset
  68.                 this.Add(palette.PaletteName, ctl);
  69.  
  70.                 _paltettes.Add(palette);
  71.                 palette.PaletteSet = this;
  72.             }
  73.         }
  74.  
  75.         public void RemovePalette(string paletteName)
  76.         {
  77.             if (_paltettes.Count == 0) return;
  78.  
  79.             for (int i = 0; i < _paltettes.Count; i++)
  80.             {
  81.                 if (_paltettes[i].PaletteName.ToUpper() == paletteName.ToUpper())
  82.                 {
  83.                     System.Windows.Forms.Control ctl =
  84.                         _paltettes[i] as System.Windows.Forms.Control;
  85.  
  86.                     this.Remove(i);
  87.                     _paltettes.RemoveAt(i);
  88.  
  89.                     ctl.Dispose();
  90.  
  91.                     if (_paltettes.Count == 0) this.Visible = false;
  92.  
  93.                     return;
  94.                 }
  95.             }
  96.         }
  97.  
  98.         public void ActivatePalette(string paletteName)
  99.         {
  100.             if (_paltettes.Count == 0) return;
  101.  
  102.             for (int i = 0; i < _paltettes.Count; i++)
  103.             {
  104.                 if (_paltettes[i].PaletteName.ToUpper() == paletteName.ToUpper())
  105.                 {
  106.                     this.Activate(i);
  107.                     return;
  108.                 }
  109.             }
  110.         }
  111.     }
  112. }

Pay attention to this line of code:

public event DocumentCollectionEventHandler DwgBecameCurrent;

and this line of code:

_dwgManager.DocumentBecameCurrent += new .......

Some of your Palettes may be designed to handle drawing based data. Since PaletteSet is a floating/modeless window, when current drawing changed in AutoCAD, the data shown in certain palette should be refreshed because of current drawing change (like AutoCAD's "Properties" window). Therefore, the this type of palette must be able to handle various events originally raised by DocumentCollection. So, here I simply handle the DocumentCollection events in the custom PaletteSet and bubble the events up. It is up to the individual Palette to subscribe the events when necessary. To simplfy the example, I only handle and raise the DocumentBecameCurrent event. In real production code, we could bubble up all the DocumentCollection events. The third code file is contains a static help method to create an instance of the custom PaletteSet. Here is the code:

Code Snippet
  1. using Autodesk.AutoCAD.Runtime;
  2.  
  3. [assembly: ExtensionApplication(typeof(MyPaletteSet.ThePaletteSetInitializer))]
  4.  
  5. namespace MyPaletteSet
  6. {
  7.     public class ThePaletteSetInitializer : IExtensionApplication
  8.     {
  9.         private static ThePaletteSet _paletteSet = null;
  10.  
  11.         public void Initialize()
  12.         {
  13.             //If necessary, add some code
  14.         }
  15.  
  16.         public void Terminate()
  17.         {
  18.             if (_paletteSet != null) _paletteSet.Dispose();
  19.         }
  20.  
  21.         public static ThePaletteSet CreateThePaltetteSet(IThePalette palette)
  22.         {
  23.             if (_paletteSet == null)
  24.             {
  25.                 _paletteSet = new ThePaletteSet();
  26.             }
  27.  
  28.             //Add palette to the PaletteSet
  29.             _paletteSet.AddPalette(palette);
  30.  
  31.             return _paletteSet;
  32.         }
  33.  
  34.         public static ThePaletteSet CreateThePaltetteSet()
  35.         {
  36.             if (_paletteSet == null)
  37.             {
  38.                 _paletteSet = new ThePaletteSet();
  39.             }
  40.  
  41.             return _paletteSet;
  42.         }
  43.     }
  44. }

That's it. Now we have a generic, pluggable PaletteSet. Build the project. From now on, when you want to build a suite of your own tools that have UI to be hosted in a PaletteSet, you can focus to the development of the Palette (Win Form UserControl) and never need to update the PaletteSet host and redeploy it. Let's see a couple of sample palettes. Sample 1: FirstPalette. Start a new class library command. It can be in the same solution as the "MyPaletteSet". But to better understand the "pluggable" nature, it is recommended to do this project in different solution. This will show that you can really focus on developing your Palette without having to update the PaletteSet at all. Once the project created, set reference to the DLL generated in "MyPaletteSet" project (MyPaletteSet.dll). Add a Win Form UserControl, called FirstPalette. It looks like:

Here is the code of the UserControl:

Code Snippet
  1. using System;
  2. using System.Windows.Forms;
  3. using Autodesk.AutoCAD.ApplicationServices;
  4. using MyPaletteSet;
  5.  
  6. namespace FirstPaletteTool
  7. {
  8.     public partial class FirstPalette : UserControl, IThePalette
  9.     {
  10.         private ThePaletteSet _parent = null;
  11.  
  12.         public FirstPalette()
  13.         {
  14.             InitializeComponent();
  15.  
  16.             Document dwg = Autodesk.AutoCAD.ApplicationServices.
  17.                 Application.DocumentManager.MdiActiveDocument;
  18.             if (dwg != null) txtFileName.Text = dwg.Name;
  19.         }
  20.  
  21.         #region IThePalette Members
  22.  
  23.         public string PaletteName
  24.         {
  25.             get { return "First Palette"; }
  26.         }
  27.  
  28.         public ThePaletteSet PaletteSet
  29.         {
  30.             get
  31.             {
  32.                 return _parent;
  33.             }
  34.             set
  35.             {
  36.                 _parent = value;
  37.                 _parent.DwgBecameCurrent +=
  38.                     new DocumentCollectionEventHandler(
  39.                         _parent_DwgBecameCurrent);
  40.             }
  41.         }
  42.  
  43.         void _parent_DwgBecameCurrent(object sender,
  44.             DocumentCollectionEventArgs e)
  45.         {
  46.             txtFileName.Text = e.Document.Name;
  47.         }
  48.  
  49.         public void Close()
  50.         {
  51.             if (_parent != null)
  52.             {
  53.                 _parent.RemovePalette(this.PaletteName);
  54.             }
  55.         }
  56.  
  57.         public void ClosePaletteSet()
  58.         {
  59.             if (_parent != null) _parent.Visible = false;
  60.         }
  61.  
  62.         #endregion
  63.  
  64.         private void button2_Click(object sender, EventArgs e)
  65.         {
  66.             this.ClosePaletteSet();
  67.         }
  68.  
  69.         private void button1_Click(object sender, EventArgs e)
  70.         {
  71.             this.Close();
  72.         }
  73.     }
  74. }

Then add another class file into the project: FirstPaletteCommand. Here is the code:

Code Snippet
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.Runtime;
  3.  
  4. using MyPaletteSet;
  5.  
  6. [assembly: CommandClass(typeof(FirstPaletteTool.FirstPaletteCommand))]
  7.  
  8. namespace FirstPaletteTool
  9. {
  10.     public class FirstPaletteCommand
  11.     {
  12.         private static ThePaletteSet _pltSet;
  13.  
  14.         [CommandMethod("StartFirst", CommandFlags.Session)]
  15.         public static void RunThisMethod()
  16.         {
  17.             Document dwg = Application.DocumentManager.MdiActiveDocument;
  18.  
  19.             try
  20.             {
  21.                 FirstPalette plt = new FirstPalette();
  22.  
  23.                 _pltSet = MyPaletteSet.
  24.                     ThePaletteSetInitializer.CreateThePaltetteSet(plt);
  25.                 _pltSet.Visible = true;
  26.                 _pltSet.ActivatePalette(plt.PaletteName);
  27.             }
  28.             catch(System.Exception ex)
  29.             {
  30.                 dwg.Editor.WriteMessage("\nError: " + ex.Message);
  31.             }
  32.         }
  33.     }
  34. }

Since I want this palette to show per-drawing based data, thus, I make this palette subscribe event "DwgBecameCurrent" raised by the hosting PaletteSet (MyPaletteSet). As you can see, no matter what UI components you place onto this palette (UserControl) and what code logic you will let this palette to execute, you can plug the palette into a common PaletteSet easily. New, let me create another palette: SecondPalette. Start a new class library project, called "SecondPaletteTool", set reference to "MyPaletteSet.dll". Add a Win Form UserControl, which looks like:


Its code is here:

Code Snippet
  1. using System;
  2. using System.Windows.Forms;
  3.  
  4. using MyPaletteSet;
  5.  
  6. namespace SecondPaletteTool
  7. {
  8.     public partial class SecondPalette : UserControl, IThePalette
  9.     {
  10.         private ThePaletteSet _parent = null;
  11.  
  12.         public SecondPalette()
  13.         {
  14.             InitializeComponent();
  15.         }
  16.  
  17.         #region IThePalette Members
  18.  
  19.         public ThePaletteSet PaletteSet
  20.         {
  21.             get
  22.             {
  23.                 return _parent;
  24.             }
  25.             set
  26.             {
  27.                 _parent = value;
  28.             }
  29.         }
  30.  
  31.         public string PaletteName
  32.         {
  33.             get { return "Second Palette"; }
  34.         }
  35.  
  36.         public void Close()
  37.         {
  38.             if (_parent != null)
  39.             {
  40.                 _parent.RemovePalette(this.PaletteName);
  41.             }
  42.         }
  43.  
  44.         public void ClosePaletteSet()
  45.         {
  46.             if (_parent != null) _parent.Visible = false;
  47.         }
  48.  
  49.         #endregion
  50.  
  51.         private void button1_Click(object sender, EventArgs e)
  52.         {
  53.             this.Close();
  54.         }
  55.  
  56.         private void button2_Click(object sender, EventArgs e)
  57.         {
  58.             this.ClosePaletteSet();
  59.         }
  60.     }
  61. }

Notice that this palette does not subscribe event raised by hosting PaletteSet. Add a class into the project: SecondPaletteCommand:

Code Snippet
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.Runtime;
  3.  
  4. using MyPaletteSet;
  5.  
  6. [assembly: CommandClass(typeof(SecondPaletteTool.SecondPaletteCommand))]
  7.  
  8. namespace SecondPaletteTool
  9. {
  10.     public class SecondPaletteCommand
  11.     {
  12.         private static ThePaletteSet _pltSet;
  13.  
  14.         [CommandMethod("StartSecond", CommandFlags.Session)]
  15.         public static void RunThisMethod()
  16.         {
  17.             Document dwg = Application.DocumentManager.MdiActiveDocument;
  18.  
  19.             try
  20.             {
  21.                 SecondPalette plt = new SecondPalette();
  22.  
  23.                 _pltSet = MyPaletteSet.
  24.                     ThePaletteSetInitializer.CreateThePaltetteSet(plt);
  25.                 _pltSet.Visible = true;
  26.                 _pltSet.ActivatePalette(plt.PaletteName);
  27.             }
  28.             catch (System.Exception ex)
  29.             {
  30.                 dwg.Editor.WriteMessage("\nError: " + ex.Message);
  31.             }
  32.         }
  33.     }
  34. }

As you can see, no matter how different the second palette from the first one, it can be plugged into MyPaletteSet in the same way, because, to MyPaletteSet, these 2 palettes are the same type: IMyPalette.

Now, start AutoCAD and "NETLOAD" the 2 palette projects (e.g. load FirstPaletteTool.dll and SecondPaletteTool.dll separately, as if the 2 DLLs are deployed and loaded separately). Enter command "StartFirst" and/or "StartSecond". You can see the corresponding palette will be shown in a common PaletteSet. If you open more than one drawings in AutoCAD and switch the active drawing, you can see the file name shown on the "First Palette" changes accordingly, because this palette handles DwgBecameCurrent event raised by the hosting PaletteSet.

From now on, whenever I want to develop a new AutoCAD tool that would have a modeless window as UI, I can go ahead to develop the UI as Win Form UserControl. Once it is done, I can simply plug it into the common hosting PaletteSet. Since the newly developed palette is in its own project, it can be deployed independently to all existing palettes, yet they are hosted in the same PaletteSet.

In the FirstPalette, I have to add "using Autodesk.AutoCAD.ApplicationServices;" because the palette has to comsume DocumentCollectionEventArgs in the DwgBecameCurrent event handler. In real development code, it is best practice to not let the UserControl to be tied to AutoCAD's dll. In this case, it is better to define my own custom EvenArgs and my own custom EventHandler in the MyPaletteSet project and use them to bubble up the various DocumentCollection events.

Update note: due to the original posted code format, a key part of the code did not show correctly, which were the topic of the comments below. Now I updated the code posting format and show the line of code in red, which was previously shown wrong. - Norman, 2011-01-03.

Thursday, September 9, 2010

Command Method: Static Or Not Static

When starting to use AutoCAD .NET API, the very first code people would be writing is the the command method in a class, e.g. a public method decorated with [CommandMethod()] attribute. However, the command method can be either "static/Shared" (C#/VB.NET) or not "static/Shared" (instance method). Also, in most smaple code one can find, there is rarely one that show the class' constructor is used.


It also puzzles some beginners if the command method is not a "static" one: why an instance method (non-static method) can be called in command without the code creating an object instance of the class?


Here is what the AutoCAD .NET API document has to say:


[quote]
For an instance command method, the method's enclosing type is instantiated separately for each open document. This means that each document gets a private copy of the command's instance data. Thus there is no danger of overwriting document-specific data when the user switches documents. If an instance method needs to share data globally, it can do so by declaring static or Shared member variables.
For a static command method, the managed wrapper runtime module does not need to instantiate the enclosing type. A single copy of the method's data is used, regardless of the document context. Static commands normally do not use per-document data and do not require special consideration for MDI mode.
[/quote]
 
With a few lines of very simple code, we can see what the difference betweem static method and instance command method.
 
Here is the code:

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

namespace CommandClassTest
{
    public class MyCommand
    {
        private static int _staticCount = 0;
        private int _instanceCount = 0;

        public MyCommand()
        {
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;
            dwg.Editor.WriteMessage("\n**************");
            dwg.Editor.WriteMessage("\nConstructing...");
            dwg.Editor.WriteMessage("\n**************");
        }

        [CommandMethod("StaticCommand", CommandFlags.Session)]
        public static void RunThisMethod1()
        {
            _staticCount++;
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;
            dwg.Editor.WriteMessage("\n----------------------------");
            dwg.Editor.WriteMessage("\n" + dwg.Name);
            dwg.Editor.WriteMessage("\nStatic command executed.");
            dwg.Editor.WriteMessage("\nStatic command count: {0}", _staticCount);
        }

        [CommandMethod("InstanceCommand", CommandFlags.Session)]
        public void RunThisMethod2()
        {
            _instanceCount++;
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;
            dwg.Editor.WriteMessage("\n----------------------------");
            dwg.Editor.WriteMessage("\n" + dwg.Name);
            dwg.Editor.WriteMessage("\nInstance command executed.");
            dwg.Editor.WriteMessage("\nInstance command count: {0}", _instanceCount);
        }
    }
}

Let's run this code in AutoCAD:

1. Start AutoCAD and "NETLOAD" to DLL;
2. With current drawing "Drawing1.dwg", enter command "InstanceCommand", the command line shows:

Command: instancecommand

**************
Constructing...
**************
----------------------------
Drawing1.dwg
Instance command executed.
Instance command count: 1

As you can see, the class' constructor is called due to the instance (non-static) command method.

3. Now, run "InstanceCommand" command again with "Drawing1.dwg". We now can guess that the constructor of the class will not run again since the class instance has already been created with this drawing (Drawing1.dwg), and the count for instance command will increment to 2. Here is the command line response, as expected:

Command: instancecommand
-------------------------------
Drawing1.dwg
Instance command executed.
Instance command count: 2

4. Let's run the static command. The command line shows:
 
Command: staticcommand
--------------------------
Drawing1.dwg
Static command executed.
Static command count: 1
 
5. Now, open a new drawing in AutoCAD: Drawing2.dwg.
6. Run the static command. we would expect the static command count to increment to 2, like this:
 
Command: staticcommand

--------------------------
Drawing2.dwg
Static command executed.
Static command count: 2

7. Run instance command in Drawing2.dwg. the command shows:
 
Command: instancecommand

**************
Constructing...
**************
----------------------------
Drawing2.dwg
Instance command executed.
Instance command count: 1


As we can see, the class' constructor is called again, because the instance command runs in drawing2.dwg the first time.
 
8. Now go back to drawing1.dwg.
9. Run static command. we surely know the static command count will be 3, in spite of drawing switching:
 
Command: staticcommand

--------------------------
Drawing1.dwg
Static command executed.
Static command count: 2

10. Run instance command in drawing1.dwg, expecting that the instance command count in drawing1.dwg will be 3 and no class' constructor is called. Here is command line shows:
 
Command: instancecommand

-------------------------------
Drawing1.dwg
Instance command executed.
Instance command count: 3

Summery:
 
If your class/class method needs to manipulate data/object accross drawings in an AutoCAD session, you use static method and the data/object can be static data/object member of the class. If your static data/object member of the class has to be initialized, you cannot do it in the class' constructor. In this case, you usually delare the static data/object to a default value/null like this:
 
private static int _count=0;
 
or
 
private static MyObject _obj=null;
 
Then, in the static command method, you do:
 
if (_count==0)
{
    //Initialize to other value if necessary
}
//then use the static data accross drawings
 
or
 
if (_obj==null)
{
    _obj=new MyObject(); //instantiate the data object if it hasn't been.
}
//the use the static object accross drawings
 
If the data/object to be manipulated by the class/class method is drawing specific, for example a drawing tracking/index data object, then you can declare the data/object as the class' data/object member (non-static), and initilize it in the class' constructor. You use instance method to manipulate the data/object

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.