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:




7 comments:

  1. Hi Norman,
    How can I click to any object in datagridview and autocad will select and zoom to that object?

    ReplyDelete
  2. Hi Quang Phan,

    I just updated the article to show how to zoom to an entity when user select an item on the UI that represents an entity. Hope it helps.

    ReplyDelete
  3. Thank you for sharing this valuable information about AutoCAD I really appreciate your hard work you put into your blog and detailed information you provide. Further More Information About AutoCAD Training Institute in Delhi So Contact Here-+91-9311002620 Or Visit Website- https://htsindia.com/autocad-training-institute

    ReplyDelete
  4. Its awesome blog thanks for sharing these kind of blog its really very helpful apart from that if anyone looking for best Digital Marketing Training Institute in Delhi So Contact Here-+91-9311002620 Or Visit Website- https://www.htsindia.com/digital-marketing-training-courses

    ReplyDelete
  5. I am reading your post from the beginning, it was so interesting to read & I feel thanks to you for posting such a good blog, keep updates regularly. If anyone want to Learn Tally Training Course Contact Us-9311002620 or Visit Website-https://www.htsindia.com/Courses/tally/tally-training-course

    ReplyDelete
  6. I am reading your post from the beginning, it was so interesting to read & I feel thanks to you for posting such a good blog, keep updates regularly. If anyone want to Learn mis Training Course Contact Us-9311002620 or Visit Website- https://www.htsindia.com/Courses/business-analytics/mis-training-instiute-in-delhi

    ReplyDelete
  7. ** Big thank you for sharing this content If anyone looking for best Sas training institute in Delhi Contact Here-+91-9311002620 Or Visit our website ****Best SAS Training Institute in Delhi** (https://www.htsindia.com/Courses/business-analytics/sas-training-institute-in-delhi)

    ReplyDelete