Monday, March 15, 2021

Highlight Entity In Block - Upated: Added VB.NET Code

 A question on how to highlight Polyline in a BlockReference was posted in Autodesk's .NET discussion forum. In my reply, I proposed to use TransientGraphics to render the highlight. Here I put together a quick project to show how easy to achieve that.

By the way, in the past I posted 2 articles on highlighting attributes in BlockReference here and here. However, because attribute (AttributeReference) is owned by BlockReference, while entity we see in BlockReference is actually part of block definition (BlockTableRecord), we cannot highlight it individually. That is where TransientGraphics comes into play: we create a clone of the target entity in block definition, transform its location to where the BlockReference is, and then use the cloned entity as Drawable to draw TransientGraphics

Here is the class BlockNestedEntityHighlighter:

using System;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
 
namespace HighlightEntityInBlock
{
    public class BlockNestedEntityHighlighter : IDisposable
    {
        private Entity _entClone = null;
        private readonly TransientManager _tsManager = 
            TransientManager.CurrentTransientManager;
        private int _colorIndex = 2;
 
        public void HighlightEntityInBlock(ObjectId nestedEntId, Matrix3d transform)
        {
            ClearHighlight();
            using (var tran = nestedEntId.Database.TransactionManager.StartTransaction())
            {
                var ent = (Entity)tran.GetObject(nestedEntId, OpenMode.ForRead);
                _entClone = ent.Clone() as Entity;
                tran.Commit();
            }
 
            _entClone.ColorIndex = _colorIndex;
            _entClone.TransformBy(transform);
 
            _tsManager.AddTransient(
                _entClone, 
                TransientDrawingMode.Highlight, 
                128, 
                new IntegerCollection());
        }
 
        public void Dispose()
        {
            ClearHighlight();
        }
 
        private void ClearHighlight()
        {
            if (_entClone != null)
            {
                _tsManager.EraseTransient(
                    _entClone, new IntegerCollection());
                _entClone.Dispose();
                _entClone = null;
            }
        }
    }
}

Here is the code running to highlight a selected nested entity from a BlockReference:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(HighlightEntityInBlock.MyCommands))]
 
namespace HighlightEntityInBlock
{
    public class MyCommands
    {
        [CommandMethod("HlEntInBlk")]
        public static void RunMyCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            if (SelectNestedEntityInBlock(
                ed, out ObjectId nestedEntId, out Matrix3d blkTransform))
            {
                using (var highlighter = new BlockNestedEntityHighlighter())
                {
                    highlighter.HighlightEntityInBlock(nestedEntId, blkTransform);
                    ed.GetString("\nPress Enter to continue...");
                }
                ed.PostCommandPrompt();
            }
            else
            {
                ed.WriteMessage("\n*Cancel*\n");
            }
        }
 
        private static bool SelectNestedEntityInBlock(Editor ed,
            out ObjectId entId, out Matrix3d blkTransform)
        {
            entId = ObjectId.Null;
            blkTransform = Matrix3d.Identity;
 
            var res = ed.GetNestedEntity("\nPick an entity in a block:");
            if (res.Status== PromptStatus.OK)
            {
                entId = res.ObjectId;
                blkTransform = res.Transform;
                ed.WriteMessage($"\nentId: {entId}");
                return true;
            }
            else
            {
                return false;
            }
        }
    }
}

See this video clip for the effect:


For simplicity, I hard-coded the highlight color as yellow. If the color of the nested entity is yellow (whether is ByBlock, or ByLayer, or by itself) , the code could be enhanced to choose a different color automatically to make the highlight stand out; f the nested entity is a Polyline, the code could make its global width thicker; the code could also use different line type, or line weight.

Update

Since the author of the original post in Autodesk .NET discussion asked for a VB.NET version of the code shown here, I did a quick conversion and now also post it here:

Class BlockNestedEntityHighlighter:

Imports System
Imports Autodesk.AutoCAD.DatabaseServices
Imports Autodesk.AutoCAD.Geometry
Imports Autodesk.AutoCAD.GraphicsInterface
 
Namespace HighlightEntityInBlock
    Public Class BlockNestedEntityHighlighter
        Implements IDisposable
 
        Private _entClone As Entity = Nothing
        Private ReadOnly _tsManager As TransientManager = TransientManager.CurrentTransientManager
        Private _colorIndex As Integer = 2
 
        Public Sub HighlightEntityInBlock(ByVal nestedEntId As ObjectIdByVal transform As Matrix3d)
            ClearHighlight()
 
            Using tran = nestedEntId.Database.TransactionManager.StartTransaction()
                Dim ent = CType(tran.GetObject(nestedEntId, OpenMode.ForRead), Entity)
                _entClone = TryCast(ent.Clone(), Entity)
                tran.Commit()
            End Using
 
            _entClone.ColorIndex = _colorIndex
            _entClone.TransformBy(transform)
            _tsManager.AddTransient(_entClone, TransientDrawingMode.Highlight, 128, New IntegerCollection())
        End Sub
 
        Public Sub Dispose() Implements IDisposable.Dispose
            ClearHighlight()
        End Sub
 
        Private Sub ClearHighlight()
            If _entClone IsNot Nothing Then
                _tsManager.EraseTransient(_entClone, New IntegerCollection())
                _entClone.Dispose()
                _entClone = Nothing
            End If
        End Sub
 
    End Class
End Namespace

CommandClass MyCommands:

Imports Autodesk.AutoCAD.DatabaseServices
Imports Autodesk.AutoCAD.EditorInput
Imports Autodesk.AutoCAD.Geometry
Imports Autodesk.AutoCAD.Runtime
Imports CadApp = Autodesk.AutoCAD.ApplicationServices.Application
Imports System.Runtime.InteropServices
 
<AssemblyCommandClass(GetType(HighlightEntityInBlock.MyCommands))>
Namespace HighlightEntityInBlock
    Public Class MyCommands
        <CommandMethod("HlEntInBlk")>
        Public Shared Sub RunMyCommand()
            Dim dwg = CadApp.DocumentManager.MdiActiveDocument
            Dim ed = dwg.Editor
            Dim nestedEntId As ObjectId = Nothing, blkTransform As Matrix3d = Nothing
 
            If SelectNestedEntityInBlock(ed, nestedEntId, blkTransform) Then
 
                Using highlighter = New BlockNestedEntityHighlighter()
                    highlighter.HighlightEntityInBlock(nestedEntId, blkTransform)
                    ed.GetString(vbLf & "Press Enter to continue...")
                End Using
 
                ed.PostCommandPrompt()
            Else
                ed.WriteMessage(vbLf & "*Cancel*" & vbLf)
            End If
        End Sub
 
        Private Shared Function SelectNestedEntityInBlock(ByVal ed As Editor, <OutByRef entId As ObjectId, <OutByRef blkTransform As Matrix3dAs Boolean
            entId = ObjectId.Null
            blkTransform = Matrix3d.Identity
            Dim res = ed.GetNestedEntity(vbLf & "Pick an entity in a block:")
 
            If res.Status = PromptStatus.OK Then
                entId = res.ObjectId
                blkTransform = res.Transform
                ed.WriteMessage($"\nentId: {entId}")
                Return True
            Else
                Return False
            End If
        End Function
    End Class
End Namespace

Thursday, March 11, 2021

Drag & Drop In AutoCAD (3) - Drag From PlugIn UI And Drop Onto AutoCAD

This is the third of this series on  Drag & Drop operation in AutoCAD. See previous ones here:

    Drag & Drop In AutoCAD (1) - The Scenarios

    Drag & Drop In AutoCAD (2) - Drag From External Application And Drop Onto AutoCAD

In this post I look into the scenario of dragging from an AutoCAD plugin UI and dropping onto AutoCAD's editor. Typically, we may have a form/window UI in our plugin application, where some data representing entities to be created in drawing. User can click a button on the UI to trigger the creation in the drawing with data in the UI; or we can let user to drag certain portion of the UI (i.e. dragging some data) and drop the data onto AutoCAD editor to let AutoCAD do something with the dropped data, usually, it is to create entity or entities with the data at the location of dropping.

If one drags something on the UI and drop it onto the same UI, the code logic of doing it is like:

1. Set the drop target control's AllowDrop property to True;

2. Handle MouseMove event of the dragging target control;

3. In the MouseMove event handler, test if mouse' left button is pressed when mouse is moving;

4. If mouse' left button is pressed, it is dragging; collect data you want to be transferred for dropping and package the data into IDataObject; then call drop target control's DoDragDrop() method with the IDataObject passed in;

5. Handle drop target control's DragDrop event, where you can extract data transferred via IDataObject, and then do whatever with the data suitable to the drop target control.

Now that we want to change the drop target to AutoCAD application, not somewhere on the same UI as the drag target, the code logic for this kind of drag and drop process changes to:

1.Handle MouseMove event of drag target control on the UI;

2. In the MouseMove event handler, test if mouse' left button is pressed when mouse is moving;

3. If mouse' left button is pressed, it is dragging; collect data you want to be transferred for dropping and package the data into IDataObject; then call Application.DoDragDrop(), where you not only pass IDataObject into it, you also need to implement the abstract class of Autodesk.AutoCAD.Windows.DropTarget and pass it into Application.DoDragDrop();

4. In your custom DropTarget class, you override the abstract method OnDrop(): that is where you unpackage data from the IDataObject and do whatever you need to do with the data, mostly, it is to create entity or entities at the dropping location.

I have to point out that dragging from plugin's UI and dropping onto AutoCAD only works with modeless plugin UI.

This is the plugin UI, which is a WinForm modeless dialog:


With the form floating on top of AutoCAD, user can drag one the 2 blue labels and drop onto AutoCAD to generate a line or circle entity; or user can drag a block name from the block list box to insert a block reference at dropping location. Here is the form's code behind:

using System;
using System.Linq;
using System.Windows.Forms;
 
namespace DragFromPluginUiToAcad
{
    public partial class DrawForm : Form
    {
        public DrawForm()
        {
            InitializeComponent();
        }
 
        private void BUttonClose_Click(object sender, EventArgs e)
        {
            Visible = false;
        }
 
        private void InitailizeControls()
        {
            try
            {
                Cursor.Current = Cursors.Default;
                
                var blocks = CadHelper.GetBlockList();
 
                if (blocks.Count() > 0)
                {
                    foreach (var blk in blocks)
                    {
                        BlockListBox.Items.Add(blk);
                    }
                }
            }
            finally
            {
                Cursor.Current = Cursors.Default;
            }
 
            ComboAngle.SelectedIndex = 0;
        }
 
        private void DrawForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            e.Cancel = true;
            Visible = false;
        }
 
        private void DrawForm_Load(object sender, EventArgs e)
        {
            InitailizeControls();
        }
 
        private void Linelabel_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button== MouseButtons.Left)
            {
                EntityInfo entInfo = new LineInfo
                {
                    Length = Convert.ToDouble(TextBoxLength.Text),
                    Angle = Convert.ToDouble(ComboAngle.Text)
                };
                EntityDropper.DropEntity(
                    thisEntityType.Line, entInfo, CadHelper.CreateLine);
            }
        }
 
        private void Circlelabel_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                EntityInfo entInfo = new CircleInfo
                {
                    Radius=Convert.ToDouble(TextBoxRadius.Text)
                };
                EntityDropper.DropEntity(
                    thisEntityType.Circle, entInfo, CadHelper.CreateCircle);
            }
        }
 
        private void BlockListBox_MouseMove(object sender, MouseEventArgs e)
        {
            if (BlockListBox.SelectedIndex < 0) return;
 
            if (e.Button == MouseButtons.Left)
            {
                EntityInfo entInfo = new BlockInfo
                {
                    BlockName=BlockListBox.Text
                };
                EntityDropper.DropEntity(
                    thisEntityType.BlockReference, entInfo, CadHelper.CreateBlockReference);
            }
        }
    }
}

As the code shows, the dragging begins by handling MouseMove event when the left mouse button is pressed, and the data used for creating entity is passed into a static method DropEntity() from a utility class EntityDropper, where Application.DoDragDrop() will be called to complete the drag & drop operation.

First, I needed to implement custom class that is derived from abstract class Autodesk.AutoCAD.Windows.DropTarget:

using System;
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Windows;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace DragFromPluginUiToAcad
{
    public class EntityDropTarget : DropTarget
    {
        private readonly Func<DocumentEntityInfoPoint3dObjectId> _createFunction;
        private Point3d _position = Point3d.Origin;
 
        public EntityDropTarget(Func<DocumentEntityInfoPoint3dObjectId> entityCreation)
        {
            _createFunction = entityCreation;
        }
 
        public ObjectId EntityId { private setget; } = ObjectId.Null;
        public override void OnDrop(DragEventArgs e)
        {
            //Convert windows location of mouse into AutoCAD editor's
            //WCS coordinate (Point3d)
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;
 
            System.Drawing.Point pt = new System.Drawing.Point(e.X, e.Y);
            _position = dwg.Editor.PointToWorld(pt);
 
            // Get data passed from Application.DoDragDrop()
            var data = e.Data.GetData(typeof(EntityInfo)) as EntityInfo;
 
            //Create the entity
            EntityId = _createFunction(dwg, data, _position);
 
            CadApp.MainWindow.Focus();
            dwg.Editor.WriteMessage($"\nEntity {EntityId} has been created @{_position}");
        }
    }
}

As the code shows in EntityDropTarget class that the drag & drop process is completed in OnDrop() method, where the data required for creating entity is extracted from IDataObject and then the actual entity generating work is done. In order to decouple the drag & drop process with how entity is to be created, I used an injected function (Func<Document, EntityInfo, Point3d, ObjectId>) to do the entity generating work. This allows me coding different way to generate entity without having to change the code for drag & drop. For example, for most user-friendly way in real production, it would be better to use a Jig to create entity, which would allow user to position the entity more accurately. But in this article, for simplicity, I just supply a simple entity creating process. Also, it is with OnDrop() method, the code can translate the mouse's drop location into AutoCAD's coordinate, so that the code would know where to create the entity. However, because it is quite difficult for user to drop (release let button) at an accurate location without usual AutoCAD drafting assistant (object snapping, coordinate entering), the drop location obtained from OnDrop() method would often be not good enough. That is why I just said, in real production, it would be ideal to use Jig for accurate entity creation.

Here is the utility class that has a static method to actually call Application.DoDragDrop():

using System;
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace DragFromPluginUiToAcad
{
    public enum EntityType
    {
        Line=0,
        Circle=1,
        BlockReference=2,
    }
 
    public class EntityDropper
    {
        public static ObjectId  DropEntity(
            System.Windows.Forms.Form userUi, 
            EntityType entType, 
            EntityInfo entityInfo,
            Func<DocumentEntityInfoPoint3dObjectId> entityCreation)
        {
            var target = new EntityDropTarget(entityCreation);
            object data;
            IDataObject dataObject;
            if (entType== EntityType.Line)
            {
                data = (LineInfo)entityInfo;
            }
            else if (entType== EntityType.Circle)
            {
                data = (CircleInfo)entityInfo;
            }
            else
            {
                data = (BlockInfo)entityInfo;
            }
            dataObject = new DataObject();
            dataObject.SetData(typeof(EntityInfo), data);
            
            CadApp.DoDragDrop(userUi, dataObject, DragDropEffects.Copy, target);
 
            return target.EntityId;
        }
    } 
}

Obviously I also need a data class for transferring entity creating information between dragging and dropping via IDataObject:

namespace DragFromPluginUiToAcad
{
    public abstract class EntityInfo
    {
    }
 
    public class LineInfo : EntityInfo
    {
        public double Angle { setget; }
        public double Length { setget; }
    }
 
    public class CircleInfo : EntityInfo
    {
        public double Radius { setget; }
    }
 
    public class BlockInfo : EntityInfo
    {
        public string BlockName { setget; }
    }
}

In order to inject an entity creating function, all the data classes (LineInfo, CircleInfo and BlockInfo) are derived from an abstract class EntityInfo, which does not have its own properties/methods and only serves as a base class, so that the injected entity creating function can have a unified method signature.

Of course I need those injectable entity creating functions:

using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace DragFromPluginUiToAcad
{
    public class CadHelper
    {
        public static IEnumerable<string> GetBlockList()
        {
            IEnumerable<string> names = null;
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var tbl = (BlockTable)tran.GetObject(dwg.Database.BlockTableId, OpenMode.ForRead);
                names = GetBlockNames(tbl, tran);
 
                tran.Commit();
            }
            return names;
        }
 
        public static ObjectId CreateLine(Document dwg, EntityInfo lineInfo, Point3d position)
        {
            var newId = ObjectId.Null;
 
            var len = ((LineInfo)lineInfo).Length;
            var ang = ((LineInfo)lineInfo).Angle * Math.PI / 180.0;
 
            var start = position;
            var x = position.X + len * Math.Cos(ang);
            var y = position.Y + len * Math.Sin(ang);
            var end = new Point3d(x, y, position.Z);
 
            using (dwg.LockDocument())
            {
                using (var tran = dwg.TransactionManager.StartTransaction())
                {
                    var line = new Line(start, end);  
                    line.SetDatabaseDefaults();
 
                    var space = (BlockTableRecord)tran.GetObject(
                        dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
                    newId = space.AppendEntity(line);
                    tran.AddNewlyCreatedDBObject(line, true);
 
                    tran.Commit();
                }
            }
 
            return newId;
        }
 
        public static ObjectId CreateCircle(Document dwg, EntityInfo circleInfo, Point3d position)
        {
            var newId = ObjectId.Null;
            using (dwg.LockDocument())
            {
                using (var tran = dwg.TransactionManager.StartTransaction())
                {
                    var circle = new Circle();
                    circle.Center = position;
                    circle.Radius = ((CircleInfo)circleInfo).Radius;
                    circle.SetDatabaseDefaults();
 
                    var space = (BlockTableRecord)tran.GetObject(
                        dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
                    newId = space.AppendEntity(circle);
                    tran.AddNewlyCreatedDBObject(circle, true);
 
                    tran.Commit();
                }
            }
 
            return newId;
        }
 
        public static ObjectId CreateBlockReference(Document dwg, EntityInfo blkInfo, Point3d position)
        {
            var newId = ObjectId.Null;
            using (dwg.LockDocument())
            {
                using (var tran = dwg.TransactionManager.StartTransaction())
                {
                    var bt = (BlockTable)tran.GetObject(
                        dwg.Database.BlockTableId, OpenMode.ForRead);
                    if (bt.Has(((BlockInfo)blkInfo).BlockName))
                    {
                        var blk = new BlockReference(position, bt[((BlockInfo)blkInfo).BlockName]);
                        blk.SetDatabaseDefaults();
 
                        var space = (BlockTableRecord)tran.GetObject(
                            dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
                        newId = space.AppendEntity(blk);
                        tran.AddNewlyCreatedDBObject(blk, true);
 
                    }
                    tran.Commit();
                }
            }
 
            return newId; ;
        }
 
        #region private methods
 
        private static IEnumerable<string> GetBlockNames(BlockTable tbl, Transaction tran)
        {
            var names = new List<string>();
            foreach (ObjectId id in tbl)
            {
                var blk = (BlockTableRecord)tran.GetObject(id, OpenMode.ForRead);
                if (!blk.IsLayout)
                {
                    names.Add(blk.Name);
                }
            }
 
            return from n in names orderby n ascending select n;
        }
 
        #endregion
    }
}

As I have already mentioned, in these injectable entity creating function, I just create entities (Line/Circle/BlockReference) at drop location, for the reason of simplicity. In real production, I would use EntityJig for better usability.

Finally, here is the CommandClass that put everything together to work, which simply show the plugin UI as singleton modeless dialog to allow user drag something from it and drop onto AutoCAD to create entities.

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(DragFromPluginUiToAcad.MyCommands))]
 
namespace DragFromPluginUiToAcad
{
    public class MyCommands 
    {
        private static DrawForm _floatForm = null;
 
        [CommandMethod("AddEntByDrag"CommandFlags.Session)]
        public static void AddEntityFromModelessForm()
        {
            if (_floatForm==null)
            {
                _floatForm = new DrawForm();
            }
 
            CadApp.ShowModelessDialog(CadApp.MainWindow.Handle, _floatForm);
        }
    }
}

See this video for the code in action:


Some Thoughts

While using drag & drop to create entities from custom plugin UI gives us programmers another "fancy way" to deliver CAD user experience, it is more common to trigger the process with proper control on the UI, typically, user would click button for this operation, which can be used with both modal and modeless UI (if the UI is modal, the code needs to call Editor.StartUserInteraction() to temporarily hide the modal UI). However, if the UI is modeless, the action (of creating entities) triggered from UI should be wrapped in a custom CommandMethod and UI's event handler (for the button click) should call SendStringToExecute() (see this article of mine on this topic); but when doing drag & and drop, as shown here, the action triggered from modeless UI (i.e. dragging, and then dropping) is not wrapped in a CommandMethod and no SendStringToExecute() involved.

Again, the dropping part of drag & drop operation does not provide an accurate dropping location as AutoCAD native drafting function does, it may not be as attractive as it looks like at first, considering that user has to hold left button while moving the mouse, in comparison to a single click of a button, or a double-click on something (such as a block icon on Tool Palette, or the latest block inserting palette). But, since drag & drop is a traditional GUI trick, some users may like it, if we can provide it.



Tuesday, March 9, 2021

Modeless Form/Window - Do Something And Update Its View - Updated

Using modeless from/window in AutoCAD plugin brings an unique challenge to programmers because of it being presented in ApplicationContext. One of most common issues with using modeless form/window is how to handle user interaction on the form/window, if the user interaction is meant to make changes to current drawing (MdiActiveDocument), such as user clicking a button on the form/window in order to create entities or update entities in drawing, and afterward update/refresh the modeless form/window to reflect the changes just made to the drawing.

It has been strongly recommended since the earlier stage of AutoCAD .NET API that the drawing database change action triggered from modeless form/window should be wrapped in a command and executed by calling SendStringToExecute() method, rather than directly from the form/window's user interaction event handler.

However, if the action executed by SendStringToExecute() causes drawing change and the change needs to be reflected in the modeless form/window, a proper care is needed in order for the form/window to wait until the sent command complete before the form/window update begins. 

A recent question on this issue was posted in Autodesk's .NET discussion forum and I posted a reply by suggesting use SendStringToExecute(), and I also proposed to use CommandEnded event handler to trigger modeless UI update. I just completed a sample project, so that anyone who is interested in this topic would be able to see a more complete picture of handling this issue. 

This is a simplified WinForm UI (the principle in in this article would apply to WPF UI, of course):


Here the form's code behind:
using System;
using System.Windows.Forms;
 
using Autodesk.AutoCAD.ApplicationServices;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace ModelessDialogWithAction
{
    public partial class EntitiesForm : Form
    {
        private readonly DocumentCollection _dwgManager = null;
        private string _currentCommand = "";
 
        public EntitiesForm()
        {
            InitializeComponent();
        }
 
        public EntitiesForm(DocumentCollection dwgManager) : this()
        {
            _dwgManager = dwgManager;
            _dwgManager.DocumentActivated += (o, e) =>
              {
                  if (e.Document.UnmanagedObject != DocumentPointer)
                  {
                      try
                      {
                          Cursor.Current = Cursors.WaitCursor;
                          RefreshEntityData(e.Document);
                      }
                      finally
                      {
                          Cursor.Current = Cursors.Default;
                      }
                  }
              };
        }
 
        public IntPtr DocumentPointer { private setget; }
 
        public void RefreshEntityData(Document dwg)
        {
            try
            {
                Cursor.Current = Cursors.WaitCursor;
 
                ListViewEntities.Items.Clear();
 
                var ents = CadUtil.CollectEntitiesInModelSpace(dwg);
                foreach (var ent in ents)
                {
                    var item = new ListViewItem(ent.EntityType);
                    item.SubItems.Add(ent.EntityId.ToString());
                    item.SubItems.Add(ent.EntityLayer);
 
                    ListViewEntities.Items.Add(item);
                }
 
                LabelCount.Text = ListViewEntities.Items.Count.ToString();
            }
            finally
            {
                Cursor.Current = Cursors.Default;
            }
 
            DocumentPointer = dwg.UnmanagedObject;
        }
 
        private void ExecuteCommand(string commandName)
        {
            _dwgManager.MdiActiveDocument.CommandEnded += MdiActiveDocument_CommandEnded;
            _currentCommand = commandName;
            CadApp.MainWindow.Focus();
            _dwgManager.MdiActiveDocument.SendStringToExecute(
                commandName + "\n"truefalsefalse);
        }
 
        private void MdiActiveDocument_CommandEnded(object sender, CommandEventArgs e)
        {
            if (e.GlobalCommandName.ToUpper() == _currentCommand.ToUpper())
            {
                _dwgManager.MdiActiveDocument.CommandEnded -= MdiActiveDocument_CommandEnded;
 
                RefreshEntityData(
                    _dwgManager.MdiActiveDocument);
            }
        }
 
        private void ButtonAddLine_Click(object sender, EventArgs e)
        {
            ExecuteCommand(MyCommands.ADD_LINE_COMMAND);
        }
 
        private void ButtonAddCircle_Click(object sender, EventArgs e)
        {
            ExecuteCommand(MyCommands.ADD_CIRCLE_COMMAND);
        }
 
        private void EntitiesForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            e.Cancel = true;
            Visible = false;
        }
 
        private void ButtonClose_Click(object sender, EventArgs e)
        {
            Visible = false;
        }
    }
}
Here is the main class (CommandClass) that initializes/shows the modeless form, and defines 2 CommandMethods for the modeless form to call with SendStringToExecute(), noticing that I set CommandFlags.NoHistory with these 2 commands, which is not mandatory, but is meant for the 2 commands not being seen by user easily.
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(ModelessDialogWithAction.MyCommands))]
[assemblyExtensionApplication(typeof(ModelessDialogWithAction.MyCommands))]
 
namespace ModelessDialogWithAction
{
    public class MyCommands : IExtensionApplication
    {
        public const string ADD_LINE_COMMAND = "ADDLINE";
        public const string ADD_CIRCLE_COMMAND = "ADDCIRCLE";
 
        private static EntitiesForm _entitiesForm = null;
 
        #region IExtensionApplication implementing
 
        public void Initialize()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                ed.WriteMessage($"\nInitializing custom add-in \"{this.GetType().Name}\"...");
 
                _entitiesForm = new EntitiesForm(CadApp.DocumentManager);
                _entitiesForm.RefreshEntityData(CadApp.DocumentManager.MdiActiveDocument);
 
                ed.WriteMessage($"\nIntializing done.\n");
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nInitializing error:\n{ex.Message}\n");
            }
        }
 
        public void Terminate()
        {
 
        }
 
        #endregion
 
        [CommandMethod("ShowForm"CommandFlags.Session)]
        public static void ShowForm()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            if (_entitiesForm.DocumentPointer!=dwg.UnmanagedObject)
            {
                _entitiesForm.RefreshEntityData(dwg);
            }
 
            CadApp.ShowModelessDialog(_entitiesForm);
        }
 
        [CommandMethod(ADD_LINE_COMMAND, CommandFlags.NoHistory)]
        public static void AddLineCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                CadUtil.AddLine(dwg);
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nError:\n{ex.Message}\n");
            }
        }
 
        [CommandMethod(ADD_CIRCLE_COMMAND, CommandFlags.NoHistory)]
        public static void AddCircleCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                CadUtil.AddCircle(dwg);
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nError:\n{ex.Message}\n");
            }
        }
    }
}
Finally, here is a helper class that does the drawing/database changes, which can be executed from UI/CommandMethod:
using System.Collections.Generic;
using System.Linq;
 
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
 
namespace ModelessDialogWithAction
{
    public class EntityInfo
    {
        public ObjectId EntityId { setget; }
        public string EntityType { setget; }
        public string EntityLayer { setget; }
    }
 
    public class CadUtil
    {
        public static IEnumerable<EntityInfo> CollectEntitiesInModelSpace(Document dwg)
        {
            var lst = new List<EntityInfo>();
 
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var model = (BlockTableRecord)tran.GetObject(
                    SymbolUtilityServices.GetBlockModelSpaceId(dwg.Database), OpenMode.ForRead);
                foreach (ObjectId id in model)
                {
                    var ent = (Entity)tran.GetObject(id, OpenMode.ForRead);
                    lst.Add(new EntityInfo
                    {
                        EntityId = id,
                        EntityType = id.ObjectClass.DxfName,
                        EntityLayer = ent.Layer
                    });
                }
 
                tran.Commit();
            }
 
            return from e in lst 
                   orderby 
                   e.EntityType ascending, 
                   e.EntityLayer ascending, 
                   e.EntityId.ToString() ascending 
                   select e;
        }
 
        public static ObjectId AddLine(Document dwg)
        {
            var res = dwg.Editor.GetPoint("\nSelect line's Start Point:");
            if (res.Status== PromptStatus.OK)
            {
                var start = res.Value;
                var opt = new PromptPointOptions(
                    "\nSelect line's End Point:");
                opt.UseBasePoint = true;
                opt.BasePoint = start;
                opt.UseDashedLine = true;
                res = dwg.Editor.GetPoint(opt);
                if (res.Status== PromptStatus.OK)
                {
                    var end = res.Value;
                    return CreateLine(dwg.Database, start, end);
                }
            }
            dwg.Editor.WriteMessage("\n*Cancel*");
            return ObjectId.Null;
        }
 
        public static ObjectId AddCircle(Document dwg)
        {
            var res = dwg.Editor.GetPoint("\nSelect circle's Center Point:");
            if (res.Status == PromptStatus.OK)
            {
                var center = res.Value;
                var opt = new PromptDoubleOptions(
                    "\nEnter circle's Radius:");
                opt.AllowNegative = false;
                opt.AllowNone = false;
                opt.DefaultValue = 300.0;
                
                var dRes = dwg.Editor.GetDouble(opt);
                if (dRes.Status == PromptStatus.OK)
                {
                    var radius = dRes.Value;
                    return CreateCircle(dwg.Database, center, radius);
                }
            }
            dwg.Editor.WriteMessage("\n*Cancel*");
            return ObjectId.Null;
        }
 
        private static ObjectId CreateLine(Database db, Point3d startPt, Point3d endPt)
        {
            var line = new Line(startPt, endPt);
            line.SetDatabaseDefaults(db);
 
            return AddEntityToModelSpace(db, line);
        }
 
        private static ObjectId CreateCircle(Database db, Point3d centerPt, double radius)
        {
            var circle = new Circle();
            circle.Center = centerPt;
            circle.Radius = radius;
            circle.SetDatabaseDefaults(db);
 
            return AddEntityToModelSpace(db, circle);
        }
 
        private static ObjectId AddEntityToModelSpace(Database db, Entity ent)
        {
            var id = ObjectId.Null;
            using (var tran = db.TransactionManager.StartTransaction())
            {
                var model = (BlockTableRecord)tran.GetObject(
                    SymbolUtilityServices.GetBlockModelSpaceId(db), OpenMode.ForWrite);
                model.AppendEntity(ent);
                tran.AddNewlyCreatedDBObject(ent, true);
 
                tran.Commit();
            }
            return id;
        }
    }
}
There is no DocumentLock is used in the code, in spite the adding entity action is triggered from modeless form/window, because the command executed by SendStringToExecute() does the document locking/unlock automatically. 
Following video clip shows the modeless form/window gets updated properly by handling CommandEnded event, targeting the 2 CommandMethods. However, depending on what data is shown on the form/window, only handling the commands directly triggered by the form/window may not enough for the form/window to reflect the data changes, but this is beyond my discussion here.

Update
A comment of this article asks how to let AutoCAD zoom to an entity that is selected in a DataGridView (or any control that contains selectable items, for that matter) on the modeless form/window.
Because the action of user interaction on the modeless form/window (selecting an item in a container control) that results in AutoCAD zooming to certain entity only change the editor's current view, not the drawing database, and there is no feedback requiring the modeless form/window to refresh/update, we can directly execute zooming code from the modeless form/window without having to go the route of using SendStringToExecute()
First, I added a static method in the CadUtil class for zooming to an entity operation:
public static void ZoomToEntity(ObjectId entId)
{
    GetZoomWindow(entId, out double[] corner1, out double[] corner2);
    dynamic comApp = CadApp.AcadApplication;
    comApp.ZoomWindow(corner1, corner2);
}
 
private static void GetZoomWindow(ObjectId entId, 
    out double[] corner1, out double[] corner2)
{
    var ext = GetEntityGeoExtents(entId);
    var h = ext.MaxPoint.Y - ext.MinPoint.Y;
    var w = ext.MaxPoint.X - ext.MinPoint.X;
    var len = Math.Max(h, w)/4.0;
 
    var pt1 = new Point3d(ext.MinPoint.X - len, ext.MinPoint.Y - len, ext.MinPoint.Z);
    var pt2 = new Point3d(ext.MaxPoint.X + len, ext.MaxPoint.Y + len, ext.MaxPoint.Z);
 
    corner1 = pt1.ToArray();
    corner2 = pt2.ToArray();
}
 
private static Extents3d GetEntityGeoExtents(ObjectId entId)
{
    Extents3d ext;
 
    using (var tran=entId.Database.TransactionManager.StartTransaction())
    {
        var ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
        ext = ent.GeometricExtents;
 
        tran.Commit();
    }
 
    return ext;
}
In order to react to user's action of selecting an item in a container control on the form/window, I updated the form by adding a check box:

The updated form's code behind:
using System;
using System.Windows.Forms;
 
using Autodesk.AutoCAD.ApplicationServices;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace ModelessDialogWithAction
{
    public partial class EntitiesForm : Form
    {
        private readonly DocumentCollection _dwgManager = null;
        private string _currentCommand = "";
 
        public EntitiesForm()
        {
            InitializeComponent();
        }
 
        public EntitiesForm(DocumentCollection dwgManager) : this()
        {
            _dwgManager = dwgManager;
            _dwgManager.DocumentActivated += (o, e) =>
              {
                  if (e.Document.UnmanagedObject != DocumentPointer)
                  {
                      try
                      {
                          Cursor.Current = Cursors.WaitCursor;
                          RefreshEntityData(e.Document);
                      }
                      finally
                      {
                          Cursor.Current = Cursors.Default;
                      }
                  }
              };
        }
 
        public IntPtr DocumentPointer { private setget; }
        public Autodesk.AutoCAD.DatabaseServices.ObjectId SelectedEntity { private setget; } =
            Autodesk.AutoCAD.DatabaseServices.ObjectId.Null;
        
        public void RefreshEntityData(Document dwg)
        {
            try
            {
                Cursor.Current = Cursors.WaitCursor;
 
                ListViewEntities.Items.Clear();
 
                var ents = CadUtil.CollectEntitiesInModelSpace(dwg);
                foreach (var ent in ents)
                {
                    var item = new ListViewItem(ent.EntityType);
                    item.SubItems.Add(ent.EntityId.ToString());
                    item.SubItems.Add(ent.EntityLayer);
 
                    item.Tag = ent.EntityId;
                    item.Selected = false;
 
                    ListViewEntities.Items.Add(item);
                }
 
                SelectedEntity = Autodesk.AutoCAD.DatabaseServices.ObjectId.Null;
                LabelCount.Text = ListViewEntities.Items.Count.ToString();
            }
            finally
            {
                Cursor.Current = Cursors.Default;
            }
 
            DocumentPointer = dwg.UnmanagedObject;
        }
 
        private void ExecuteCommand(string commandName)
        {
            _dwgManager.MdiActiveDocument.CommandEnded += MdiActiveDocument_CommandEnded;
            _currentCommand = commandName;
            CadApp.MainWindow.Focus();
            _dwgManager.MdiActiveDocument.SendStringToExecute(
                commandName + "\n"truefalsefalse);
        }
 
        private void MdiActiveDocument_CommandEnded(object sender, CommandEventArgs e)
        {
            if (e.GlobalCommandName.ToUpper() == _currentCommand.ToUpper())
            {
                _dwgManager.MdiActiveDocument.CommandEnded -= MdiActiveDocument_CommandEnded;
 
                RefreshEntityData(
                    _dwgManager.MdiActiveDocument);
            }
        }
 
        private void ButtonAddLine_Click(object sender, EventArgs e)
        {
            ExecuteCommand(MyCommands.ADD_LINE_COMMAND);
        }
 
        private void ButtonAddCircle_Click(object sender, EventArgs e)
        {
            ExecuteCommand(MyCommands.ADD_CIRCLE_COMMAND);
        }
 
        private void EntitiesForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            e.Cancel = true;
            Visible = false;
        }
 
        private void ButtonClose_Click(object sender, EventArgs e)
        {
            Visible = false;
        }
 
        private void ListViewEntities_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (!CheckBoxZoom.Checked) return;
            if (ListViewEntities.SelectedItems.Count == 0) return;
 
            SelectedEntity = (Autodesk.AutoCAD.DatabaseServices.ObjectId)
                ListViewEntities.SelectedItems[0].Tag;
 
            // Directly execute the zooming code without using 
            // SendStringToExecute()
            CadUtil.ZoomToEntity(SelectedEntity);
        }
    }
}
The lines in red is modified code. As shown in ListViewEntities_SelectIndexChanged event handler, I simply to have the entity zooming code directly executed from the modeless form without using SendStringToExecute() and handling CommandEnded event for UI update, because it is not needed in this case. Following video clip shows the code in action:




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.