Wednesday, February 27, 2019

Visually Select A Segment Of Polyline

This post is inspired by a recent discussion in Autodesk's .NET discussion forum.

With .NET API, when user picks a polyline, it is fairly easy to figure out where on the polyline use picked, that is, which segment between 2 vertices is picked. However, if we code a process and ask user to select certain segment of a polyline, we need to provide good visual hint for user interaction, so that use would be able to explicitly make his/her selection. In the case of asking user to select a certain segment of polyline, there is no direct and simple API methods to use, but I thought it could achieved with a bit of coding, when I saw the question was raised in the discussion forum, and decided I would give it a try if no one would offer concrete solution later.

Now, I have found a bit time and completed some code which does what I thought is the answer to the original question. This time, let see the video showing how the code visually indicates a segment of polyline is selected first. Here is the video clip.

Here are some considerations to bear in mind:

1. Use Editor.GetEntity() for picking, with filter set to only allow polyline being selected;
2. Start handling Editor.PointMonitor event right before Editor.GetEntity() is called, so that when user moves cursor for selecting, the code would have chance to calculate which segment of the polyline the mouse cursor is hovering on (or not hover on a polyline at all); then draw transient graphics of the segment of the polyline to provide user a visual hint;
3. Use Polyline.GetSplitCurves() to obtain an non-DB residing entity to draw transient graphics and return as the segment selecting result (up to the method caller to either adding the segment entity to database, or dispose it).

Let us look at the code. First the class PartialPolylinePicker:
using System;
using System.Collections.Generic;
 
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.GraphicsInterface;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace SelectPartialPolyline
{
    public class PartialPolylinePicker : IDisposable
    {
        private Document _dwg = null;
        private Editor _ed = null;
 
        private ObjectId _polyId = ObjectId.Null;
        private TransientManager _tsMgr = TransientManager.CurrentTransientManager;
        private List<Drawable> _drawables = new List<Drawable>();
 
        public void Dispose()
        {
            ClearTransientGraphics();
        }
 
        public Entity PickPartialPolyline(Document dwg)
        {
            _dwg = dwg;
            _ed = dwg.Editor;
 
            Entity curve = null;
 
            ClearTransientGraphics();
 
            try
            {
                _ed.PointMonitor += Editor_PointMonitor;
 
                var opt = new PromptEntityOptions(
                    "\nSelect polyline:");
                opt.SetRejectMessage("\nInvalid: not a polyline.");
                opt.AddAllowedClass(typeof(Autodesk.AutoCAD.DatabaseServices.Polyline), true);
                
                var res = _ed.GetEntity(opt);
                if (res.Status== PromptStatus.OK)
                {
                    var pickPt = res.PickedPoint;
                    var polyId = res.ObjectId;
                    
                    var vertexIndexes = GetVertexIndexes(pickPt, polyId);
                    curve = GetSplitCurveAtVertex(polyId, vertexIndexes.Item1);
                  curve=GetPolylineSegmentCurve(pickPt, polyId);
                }             }             finally             {                 _ed.PointMonitor -= Editor_PointMonitor;             }             return curve;         }         private void Editor_PointMonitor(object sender, PointMonitorEventArgs e)         {             ClearTransientGraphics();             var curPt = e.Context.RawPoint;             var polyId = GetClosestPolylineAtPoint(_ed, curPt);             if (!polyId.IsNull)             {                 HighlightPolyline(polyId, curPt);             }         }         #region private methods
private Curve GetPolylineSegmentCurve(
          Point3d pickedPoint, ObjectId polyIdbool hightlight=false)
{
    Autodesk.AutoCAD.DatabaseServices.Curve curve = null;
 
    using (var tran = polyId.Database.TransactionManager.StartTransaction())
    {
        var poly = (Autodesk.AutoCAD.DatabaseServices.Polyline)tran.GetObject(polyId, OpenMode.ForRead);
        var index = GetSegmentIndex(pickedPoint, poly);
        curve = GetSegmentCurve(poly, index);
        if (curve!=null && hightlight)
        {
            curve.ColorIndex = poly.ColorIndex != 2 ? 2 : 1;
        }
        tran.Commit();
    }
    
    return curve;
}
 
private int GetSegmentIndex(
           Point3d pickedPosition, Autodesk.AutoCAD.DatabaseServices.Polyline poly)
{
    var closestPoint = poly.GetClosestPointTo(pickedPosition, false);
    var param = poly.GetParameterAtPoint(closestPoint);
    return Convert.ToInt32(Math.Floor(param));
}
 
private Curve GetSegmentCurve(
           Autodesk.AutoCAD.DatabaseServices.Polyline polyint index)
{
    Curve3d geCurve = null;
    var segType = poly.GetSegmentType(index);
    switch (segType)
    {
        case SegmentType.Line:
            geCurve = poly.GetLineSegmentAt(index);
            break;
        case SegmentType.Arc:
            geCurve=poly.GetArcSegmentAt(index);
            break;
    }
 
    if (geCurve!=null)
    {
        return Curve.CreateFromGeCurve(geCurve);
    }
    else
    {
        return null;
    }
}
        private Tuple<intint> GetVertexIndexes(Point3d pickedPosition, ObjectId polyId)
        {
            int first = 0;
            int second = 0;
 
            using (var tran = polyId.Database.TransactionManager.StartOpenCloseTransaction())
            {
                var poly = (Autodesk.AutoCAD.DatabaseServices.Polyline)
                    tran.GetObject(polyId, OpenMode.ForRead);
 
                var closestPoint = poly.GetClosestPointTo(pickedPosition, false);
                var len = poly.GetDistAtPoint(closestPoint);
 
                for (int i = 1; i < poly.NumberOfVertices - 1; i++)
                {
                    var pt1 = poly.GetPoint3dAt(i);
                    var l1 = poly.GetDistAtPoint(pt1);
 
                    var pt2 = poly.GetPoint3dAt(i + 1);
                    var l2 = poly.GetDistAtPoint(pt2);
 
                    if (len > l1 && len < l2)
                    {
                        first = i;
                        second = i + 1;
                        break;
                    }
                }
 
                tran.Commit();
            }
 
            return new Tuple<intint>(first, second);
        }
 
        private void ClearTransientGraphics()
        {
            if (_drawables.Count>0)
            {
                foreach (var d in _drawables)
                {
                    _tsMgr.EraseTransient(d, new IntegerCollection());
                    d.Dispose();
                }
 
                _drawables.Clear();
            }
        }
 
        private ObjectId GetClosestPolylineAtPoint(Editor ed, Point3d position)
        {
            var returnId = ObjectId.Null;
 
            var selResult = SelectAtPickBox(ed, position);
            if (selResult.Status== PromptStatus.OK)
            {
                var ids = new List<ObjectId>();
                foreach (ObjectId id in selResult.Value.GetObjectIds())
                {
                    if (id.ObjectClass.DxfName.ToUpper()=="LWPOLYLINE")
                    {
                        ids.Add(id);
                    }
                }
 
                if (ids.Count > 0)
                {
                    if (ids.Count == 1)
                    {
                        returnId = ids[0];
                    }
                    else
                    {
                        // If the pick box hover on multiple polyline, find the one that is
                        // closest to the pick box center
                        returnId = FindClosestPolyline(ids, position, ed.Document.Database);
                    }
                }
            }
 
            return returnId;
        }
 
        private PromptSelectionResult SelectAtPickBox(Editor ed, Point3d pickBoxCentre)
        {
            //Get pick box's size on screen
            System.Windows.Point screenPt = ed.PointToScreen(pickBoxCentre, 1);
     
            //Get pickbox's size. Note, the number obtained from
            //system variable "PICKBOX" is actually the half of
            //pickbox's width/height
            object pBox = CadApp.GetSystemVariable("PICKBOX");
            int pSize = Convert.ToInt32(pBox);
    
            //Define a Point3dCollection for CrossingWindow selecting
            Point3dCollection points = new Point3dCollection();
    
            System.Windows.Point p;
            Point3d pt;
    
            p = new System.Windows.Point(screenPt.X - pSize, screenPt.Y - pSize);
            pt = ed.PointToWorld(p, 1);
            points.Add(pt);
    
            p = new System.Windows.Point(screenPt.X + pSize, screenPt.Y - pSize);
            pt = ed.PointToWorld(p, 1);
            points.Add(pt);
    
            p = new System.Windows.Point(screenPt.X + pSize, screenPt.Y + pSize);
            pt = ed.PointToWorld(p, 1);
            points.Add(pt);
    
            p = new System.Windows.Point(screenPt.X - pSize, screenPt.Y + pSize);
            pt = ed.PointToWorld(p, 1);
            points.Add(pt );
    
            return ed.SelectCrossingPolygon(points);
        }
 
        private ObjectId FindClosestPolyline(IEnumerable<ObjectId> ids, Point3d position, Database db)
        {
            ObjectId polyId = ObjectId.Null;
            double dist = double.MaxValue;
 
            using (var tran = db.TransactionManager.StartOpenCloseTransaction())
            {
                foreach (var id in ids)
                {
                    var poly = (Autodesk.AutoCAD.DatabaseServices.Polyline)
                        tran.GetObject(id, OpenMode.ForRead);
                    var pt = poly.GetClosestPointTo(position, false);
                    var d = pt.DistanceTo(position);
                    if (d < dist)
                    {
                        polyId = id;
                        dist = d;
                    }
                }
 
                tran.Commit();
            }
 
            return polyId;
        }
 
        #endregion
 
        #region private methods: highlight polyline's segment
 
        private void HighlightPolyline(ObjectId polyId, Point3d curPt)
        {
            var vertexIndexes = GetVertexIndexes(curPt, polyId);
            var ent = GetSplitCurveAtVertex(polyId, vertexIndexes.Item1);
           var ent=GetPolylineSegmentCurve(curPt, polyId, true);
            if (ent != null)             {                 _drawables.Add(ent);                 _tsMgr.AddTransient(ent, TransientDrawingMode.DirectTopmost, 128, new IntegerCollection());             }         }         private Entity GetSplitCurveAtVertex(ObjectId polyId, int vertexIndex)         {             Entity curve = null;             using (var tran = polyId.Database.TransactionManager.StartOpenCloseTransaction())             {                 var poly = (Autodesk.AutoCAD.DatabaseServices.Polyline)                     tran.GetObject(polyId, OpenMode.ForRead);                 var vertices = new Point3dCollection();                 for (int i=0; i<poly.NumberOfVertices; i++)                 {                     vertices.Add(poly.GetPoint3dAt(i));                 }                 using (var dbObjs = poly.GetSplitCurves(vertices))                 {                     for (int i=0; i<dbObjs.Count; i++)                     {                         if (i==vertexIndex)                         {                             curve = (Entity)dbObjs[i];                         }                         else                         {                             dbObjs[i].Dispose();                         }                     }                     dbObjs.Clear();                 }                 if (curve!=null)                 {                     curve.ColorIndex = poly.ColorIndex != 1 ? 1 : 2;                 }                 tran.Commit();             }             return curve;         }                  #endregion     } }

Here is the command class:
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(SelectPartialPolyline.MyCommand))]
 
namespace SelectPartialPolyline
{
    public class MyCommand 
    {
        [CommandMethod("PickPoly")]
        public static void DoPartialPolylineSelection()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                Entity curve = null;
 
                using (var picker = new PartialPolylinePicker())
                {
                    curve = picker.PickPartialPolyline(dwg);
                }
 
                if (curve!=null)
                {
                    ed.WriteMessage("\nPartial polyline picking suceeded.\n");
 
                    // Do whatever with the curve segment from the polyline,
                    // which is not database residing at the moment
                    // if it is not to be added to database, make sure to dispose it
                    curve.Dispose();
                }
                else
                {
                    ed.WriteMessage("\nPartial polyline picking was cancelled\n");
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError: {0}", ex.Message);
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
    }
}

Download the source code here.
Also, see this video clip here showing how the code works:



Update:

While this bug fix update is quite late, I did it anyway: now it works with closed polyline as expected. Actually the updated code is much simpler than the buggy old code. I do not remember what I was thinking then😅 The source code for download has also been updated. By the way, I have a later article, that was also about nearly the same topic same topic here.


Friday, November 30, 2018

Exporting Shape From AutoCAD Civil/Map, 2D or 3D?

It is very common practice when using AutoCAD Civil/Map that user needs to export AutoCAD geometries as Shape file (*.shp). AutoCAD Map provides built-in command "MapExport" for doing this. AutoCAD Map also comes with ObjectARX Map APIs that includes Autodesk.Gis.Map.ImportExport namespace, which allows the shape exporting to be done programmatically. I have a few custom applications written and used from many years since AutoCAD Map 2006, exporting 2D shape files. These applications had been worked as expected, until our recent upgrade from Civil2015 to Civil2018: suddenly these custom applications exported shape file in 3D, which is not we want (for regulatory data submission).

When using "MapExport" command to export shape file, user is given a chance to decide exporting shape as 2D or 3D, as picture below shows:


In our practice at work, when manually exporting shape, we never needed to click "Driver Options.." button to decide exporting to 2D or 3D. AutoCAD Map always defaults to 2D exporting. This is also the case for my Map API code, where I had never needed to have code to set exporting driver option for the object Autodesk.Gis.Map.ImportExport.Exporter, well, until we moved to Civil/Map 2018. However, the built-in command still always defaults to 2D export, even you re-run the command after the previous command where you chose to export to 3D.

So, I had to modify my code of more than 10 years of old to force 2D export, using the methods GetDriveOptions()/SetDriverOptions() of Autodesk.Gis.Map.ImportExport.Exporter class.

Due to the poor AutoCAD Map API documentation, I ran into a obstacle: what is the value for the method SetDriveOptions()'s argument of Autodesk.Gis.Map.ImportExport.NameValueCollection type, in order to export in 2D or 3D? I searched all over the Internet and came back empty-handed. But I eventually figured it out by saving an exporting profile (*.epf file), as shown in picture below:


*.epf file is actually an XML file. The "Driver Options" for 2D/3D exporting can be easily spotted by opening this *.epf file in NotePad, as the picture shows below:



With this information finally available, I went ahead modifying my shape exporting code like this (in red):

public class  MyShapeExporter
{
    private Exporter _exporter = null;
 
    public MyShapeExporter()
    {
 
    }
 
    public void ExportClosedPolylines(IEnumerable<ObjectId> entIds, string shapeFileName)
    {
        try
        {
            _exporter = HostMapApplicationServices.Application.Exporter;
            _exporter.Init("SHP", shapeFileName);
            _exporter.SetStorageOptions(StorageType.FileOneEntityType, GeometryType.Polygon, null);
            _exporter.ClosedPolylinesAsPolygons = true;
            _exporter.SetSelectionSet(new ObjectIdCollection(entIds.ToArray()));
            var options = _exporter.GetDriverOptions();
            var opt = new Autodesk.Gis.Map.Utilities.StringPair("FDO_SHAPE_DIMENSION", "2D");
            if (!options.Contains(opt))
            {
                options.Add(opt);
            }
            _exporter.SetDriverOptions(options);
            _exporter.Export(true);
 
        }
        finally
        {
            _exporter = null;
        }
    }
}


In summary, the code I showed here in red was never needed until we moved from AutoCAD Civil/Map2015 to AutoCAD Civil/Map2018 (it could be since 2016 or 2017, which I never actually used/tested), which indicates some AutoCAD Map API behaviour change, although the manual process with built-in command "MapExport" remains the same from early version to AutoCAD Civil/Map 2018.

Obviously, with this added a few lines of code, we can now explicitly decide to export shape as 2D or 3D.




Wednesday, October 3, 2018

Executing Command from PaletteSet

This article is inspired by the question post in Autodesk's .NET discussion forum here. There could be different solutions to that question and I thought it would be better to put forth mine with actual code provided, which might be too long to post as reply in the discussion forum. So, I decided to post it here for better readability.

When using PaletteSet as UI to allow user to interact with AutoCAD (i.e. letting AutoCAD do particular processing), it is common practice to use Document.SendStringToExecute() to call AutoCAD command, either built-in one, or custom-built one. PaletteSet is a modeless UI, floating on top of AutoCAD window and user can freely change the focus between AutoCAD window, or the PaletteSet window. This would cause issue when a command is active with AutoCAD (usually, it is in the middle of the command, waiting for user input) and user goes to the PaletteSet to trigger another command, as described in the question posted in the discussion forum, such as clicking a button to call a command to insert a block. In this case, the interaction with PaletteSet either results in AutoCAD command line showing error message; or nothing happens at AutoCAD command line - the active command is still waiting to be either completed, or cancelled.

One way to handle this issue is always test if there is active command in PaletteSet' user interaction event handler first, and only goes ahead when there is no active command.

The other approach is whenever PaletteSet's user interaction even is triggered, always cancel any active command first, just like we usually do with any menu/toolbar/ribbon item macro: prefixing it with "^C^C" to cancel possible active command before the macro is called.

Obviously the latter approach would be desirable in most cases and and more compliant with AutoCAD conventions.

So, here is my solution to the question posted in the forum aforementioned.

Firstly I created a custom PaletteSet. One should ALWAYS derive custom PaletteSet from Autodesk.AutoCAD.Windows.PaletteSet class. DO NOT directly use PaletteSet class without subclass it.

Following is the code of the Palette (System.Windows.Forms.UserControl), which simply has 2 buttons; each button's Tag property is given a valid block name; that means, clicking on each button would trigger a block inserting command. Since the UI is very simple, I only show it code behind here:

using System;
using System.Windows.Forms;
 
namespace SendCommandFromPaletteSet
{
    public partial class BlockPalatte : UserControl
    {
 
        public BlockPalatte()
        {
            InitializeComponent();
        }
 
        public event BeginBlockInsertingEventHandler BeginBlockInserting;
 
        // the 2 buttons' Click event is wired to this event handler
        private void ButtonClick(object sender, EventArgs e)
        {
            var tag = ((Control)sender).Tag;
 
            if (tag!=null)
            {
                var blkName = tag.ToString();
                if (!string.IsNullOrEmpty(blkName))
                {
                    BeginBlockInserting?.Invoke(
                        sender, new BeginBlockInsertingEventArgs(blkName));
                }
            }
        }
    }
}

Here is the class MyBlockPaletteSet:

using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Windows;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace SendCommandFromPaletteSet
{
    public class MyBlockPaletteSet : PaletteSet
    {
        private BlockPalatte _blkPalette;
        private bool _doInsertion = false;
        public string CurrentBlockName { private setget; }
 
        public MyBlockPaletteSet() : base(
            "My Block PaletteSet""BlkPs"new Guid("B33AE81D-0FBB-49EA-83D2-62D667EEDDCA"))
        {
            this.Style = PaletteSetStyles.ShowCloseButton |
                 PaletteSetStyles.UsePaletteNameAsTitleForSingle |
                  PaletteSetStyles.Snappable;
 
            this.MinimumSize = new System.Drawing.Size(400, 400);
            this.KeepFocus = true;
 
            _blkPalette = new BlockPalatte();
            Add("Blocks", _blkPalette);
 
            _blkPalette.BeginBlockInserting += BlkPalette_BeginBlockInserting;
        }
 
        private void BlkPalette_BeginBlockInserting(object sender, BeginBlockInsertingEventArgs e)
        {
            if (!string.IsNullOrEmpty(e.BlockName))
            {
                var dwg = CadApp.DocumentManager.MdiActiveDocument;
                CurrentBlockName = e.BlockName;
 
                var cmdActive = Convert.ToInt32(CadApp.GetSystemVariable("CMDACTIVE"));
                if (cmdActive>0)
                {
                    dwg.CommandCancelled += Dwg_CommandCancelled;
 
                    _doInsertion = true;
                    dwg.SendStringToExecute("\x03\x03"falsetruefalse);
                }
                else
                {
                    DoBlockInsert(dwg);
                } 
            }
        }
 
        private void Dwg_CommandCancelled(object sender, CommandEventArgs e)
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
 
            if (_doInsertion)
            {
                dwg.CommandCancelled -= Dwg_CommandCancelled;
                _doInsertion = false;
                DoBlockInsert(dwg);
            }
        }
 
        private void DoBlockInsert(Document dwg)
        {
            CadApp.MainWindow.Focus();
            dwg.SendStringToExecute("InsBlk "truefalsefalse);
        }
    }
 
    public class BeginBlockInsertingEventArgs : EventArgs
    {
        public string BlockName { private setget; }
        public BeginBlockInsertingEventArgs(string blkName)
        {
            BlockName = blkName;
        }
    }
 
    public delegate void BeginBlockInsertingEventHandler(object sender, BeginBlockInsertingEventArgs e);
}

As the code shows, the user interaction event (clicking the buttons) in the Palette is bubbled to the custom PaletteSet, where the actual command execution is called (via SendStringToExecute()). Also, custom PaletteSet conveys the user input information (what block is to insert).

The trick of dealing the issue raised in aforementioned question is to test if there is active command with current active document by examine system variable "CMDACTIVE". If no, go ahead to send command to execution; if yes, hook up the CommandCancelled event of active drawing, and then send "^C^C" as command to cancel active command, what ever it is, and then set a flag to indicate new command is waiting to be executed. Therefore, the Command_Cancelled event handler would be triggered and pending command from user interaction with Palette is sent to execution.

Following is the command class that shows the custom PaletteSet and does the actual work: inserting multiple blocks in a loop whenever user clicks a button in the Palette. To simplify the code, I only have code to let user to pick insertion point in a loop until user either cancels the loop, or click the button in the Palette, which cancels the active point picking loop. Here is the code:

using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(SendCommandFromPaletteSet.Commands))]
 
namespace SendCommandFromPaletteSet
{
    public class Commands
    {
        private static MyBlockPaletteSet _blkPs = null;
 
        [CommandMethod("BlkPs"CommandFlags.Session)]
        public static void RunCommand()
        {
            if (_blkPs==null)
            {
                _blkPs = new MyBlockPaletteSet();
            }
 
            _blkPs.Visible = true;
        }
 
        [CommandMethod("InsBlk"CommandFlags.NoHistory )]
        public static void InsertBlock()
        {
            if (_blkPs == null ||
                !_blkPs.Visible) return;
 
            var blkName = _blkPs.CurrentBlockName;
                InsertBlock(blkName);
        }
 
        #region private methods
 
        private static void InsertBlock(string blkName)
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var count = 0;
            while(true)
            {
                if (!PickInsertionPoint(ed, out Point3d pt))
                {
                    break;
                }
                else
                {
                    count++;
                    ed.WriteMessage($"Inserting block \"{blkName}\" #{count}...");
                }
            }
        }
 
        private static bool PickInsertionPoint(Editor ed, out Point3d pt)
        {
            pt = Point3d.Origin;
 
            var res = ed.GetPoint("\nSelect block position:");
            if (res.Status== PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }
 
        #endregion
    }
}

Watch this video clip for the code in action. As the video clip shows, when there is active command waiting for user input, be it AutoCAD built-in command, or a custom command, or the command started by the PaletteSet, whenever user clicks a button in the PaletteSet, the active command is cancelled, and whatever command tied to the button-click starts.



Friday, September 28, 2018

Showing Modeless Form with Help of PerDocumentClass

Sometimes, we want to provide UI as modeless form that floats on top of AutoCAD, presenting useful information to user and allowing user to still interact with either AutoCAD or the UI. However, if the data to be presented is document specific, the modeless form has to be updated/refreshed whenever the active document in AutoCAD changes.

I wrote an article on this topic a few years ago, where 2 approaches were described: using singleton form instance, or multiple form instances. If the data used in the UI is document specific, using singleton form would require UI refreshing, which in turn may require lengthy data re-collecting/re-loading. In that article, the multiple-form approach relies on non-static CommandMethod call to instantiate the CommandClass per document, so that the UI can be member of the CommandClass and be created somehow automatically.

However, to make AutoCAD plug-in code cleaner, more modular, one may not want to put document specific data models and/or UI components directly in a CommandClass. Also, per-document CommandClass is only instantiated when a non-static CommandMethod is called. So, what if you want the per-document data available before a non-static CommandMethod is called against each document?

In this article, I demonstrate how to take advantage of PerDocumentClassAttribute class, introduced by AutoCAD 2015, to show modeless form easily with document specific data.

Firstly, Kean Wamsley had posted 2 articles about "PerDocumentClassAttribute" here and here. One may want to read them first before following me further here.

Here is a PerDocumentClass that holds some data for each document opened in AutoCAD and holds an UI (a modeless form) to allow user to view/edit the per-document data:

using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyPerDocumentClass(typeof(PerDocModelessForm.MyDocData))]
 
namespace PerDocModelessForm
{
    public class MyDocData : IDisposable
    {
        private const string USERDATA_KEY = "My_PER_DOC_DATA";
 
        public string UserName { private setget; }
        public string DrawingName { private setget; }
        public string DwgNote1 { internal setget; }
        public string DwgNote2 { internal setget; }
        public MyDocDataView DataView { private setget; }
        public bool WasShown { internal setget; }
 
        private IntPtr _dwgPointer = IntPtr.Zero;
        private bool _saved = false;
        private Document _dwg = null;
 
        public MyDocData(Document dwg)
        {
            _dwg = dwg;
            DwgNote1 = "";
            DwgNote2 = "";
            UserName = CadApp.GetSystemVariable("LOGINNAME").ToString();
            DrawingName = dwg.Name;
            DataView = new MyDocDataView(this);
 
            _dwgPointer = dwg.UnmanagedObject;
            _saved = false;
 
            dwg.UserData.Add(USERDATA_KEY, this);
 
            // Update DrawingName property if file is saved to a new file name
            dwg.Database.SaveComplete += (o, e) =>
            {
                if (e.FileName.ToUpper()!=DrawingName.ToUpper())
                {
                    DrawingName = e.FileName;
                }
            };
 
            // Show the view when document is activated, if
            // the view was shown when the document was activate prevuoisly
            CadApp.DocumentManager.DocumentActivated += (o, e) =>
            {
                if (e.Document.UnmanagedObject == _dwgPointer)
                {
                    if (WasShown)
                    {
                        DataView.Visible = true;
                    }
                }
            };
 
            // Hide the data view if the document is deactivated
            CadApp.DocumentManager.DocumentToBeDeactivated += (o, e) =>
            {
                if (e.Document.UnmanagedObject == _dwgPointer)
                {
                    if (DataView.Visible)
                    {
                        WasShown = true;
                        DataView.Visible = false;
                    }
                }
            };
 
            // Save myDocData to somewhere
            CadApp.DocumentManager.DocumentToBeDestroyed += (o, e) =>
            {
                var doc = e.Document;
                if (doc.UnmanagedObject == _dwg.UnmanagedObject)
                {
                    var data = doc.UserData[USERDATA_KEY] as MyDocData;
                    SaveDwgNotes(data);
                }
            };
        }
 
        public static MyDocData Create(Document dwg)
        {
            return new MyDocData(dwg);
        }
 
        public static void ShowDocData(Document dwg)
        {
            var data = dwg.UserData[USERDATA_KEY] as MyDocData;
            CadApp.ShowModelessDialog(CadApp.MainWindow.Handle, data.DataView, true);
        }
 
        public void Dispose()
        {
            if (DataView!=null)
            {
                DataView.Dispose();
            }
        }
 
        #region private methods
 
        private void SaveDwgNotes(MyDocData data)
        {
            if (!data._saved)
            {
                System.Windows.Forms.MessageBox.Show(
                    "Saving drawing note data to somewhere...",
                    "Per Document Data in \"" + System.IO.Path.GetFileName(_dwg.Name) + "\"...",
                    System.Windows.Forms.MessageBoxButtons.OK,
                    System.Windows.Forms.MessageBoxIcon.Information);
 
                //To DO: save drawing note data to somewhere
 
 
                data._saved = true;
            }
        }
 
        #endregion
    }
}

Here is the command that allows user to view/edit data for each document:

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(PerDocModelessForm.MyCommands))]
 
namespace PerDocModelessForm
{
    public class MyCommands
    {
        [CommandMethod("ShowData")]
        public static void RunCommandA()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            MyDocData.ShowDocData(dwg);
        }
    }
}


This video clip shows the code in action

Some Thought

PerDocumentClass greatly simplifies the process of creating and cleaning "per-document" data in AutoCAD - a multiple document application: the class is instantiated to each existing open document when the .NET assembly, where the PerDocumentClass is defined, is loaded and to each newly opened document.

Using PerDocumentClass would make data segregation in the principle of "separate-concerns" much easier. My code here shows how multiple modeless forms are used for documents in AutoCAD with each only being associated to specific document.

In this example of using modeless form, I use Windows Form. Using WPF window would be the same. Using PaletteSet for each document is doable in the same way, but might not be desirable, because PaletteSet is designed to run at application level, as an UI (pallete) container, especially if a GUID is used to instantiate a PaletteSet, and AutoCAD remembers a PaletteSet based on its GUID per-application.

Download source code 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.