Friday, March 7, 2014

Real-Time AutoCAD Command Monitoring with ASP.NET SignalR

Introduction

What is ASP.NET SignalR? From to Microsoft's ASP.NET website, it says:

ASP.NET SignalR is a new library for ASP.NET developers that makes developing real-time web functionality easy. SignalR allows bi-directional communication between server and client. Servers can now push content to connected clients instantly as it becomes available. SignalR supports Web Sockets, and falls back to other compatible techniques for older browsers. SignalR includes APIs for connection management (for instance, connect and disconnect events), grouping connections, and authorization.

Go to here for more information on ASP.NET SignalR. Searching the Internet would bring up tons of links on this technology, including code samples. tutorials. While this technology is mainly meant for web application, it does support .NET client.

In my recent web application study/development, I had opportunity to explore this technology a little bit. Just a months ago, I explored using ASP.NET Web API to automate AutoCAD. I thought why not to give it a try to make ASP.NET SignalR and AutoCAD work together? 

Since SignalR is about bi-directional communication between server and client and primarily meant for web browser as the client side UI, I felt it would be interesting to make a running AutoCAD to communicate with a web browser.

The core of communication via SignalR is a set of API, called Hub API, hosted somewhere as communication server, the clients (multiple web browsers) talk to each other via the Hub. Like Web API, SignalR can either be hosted in a web server (IIS), or be self-hosted (as a console application, Windows service application...).

Many AutoCAD programmers may have done (or been asked to develop) something that monitors AutoCAD usage at some point. I did this kind of applications with AutoCAD VBA and .NET API. So, I decide to see how easy (or how difficult) to build a simple application that can monitor AutoCAD usage in real-time mode by tracking AutoCAD commands executed by an AutoCAD user. I did not mean to create an application that has practical value, just want it to be a proof of concept.

Here are couple of development considerations:

1. I decided to self-host SignalR's server in a console application. Thus my code can run with any Windows computer with .NET 4.5 installed without relying on a web server somewhere on the network for hosting SignalR server.

2. Since SignalR requires .NET 4.5, the AutoCAD add-in that uses SignalR .NET client API must also target 4.5. Although latest AutoCAD (2013/2014) only officially support .NET 4.0, AutoCAD NET add-in built on .NET 4.5 should work OK in general.


A Pilot Development
 
I started with a Visual Studio 2013 solution with 4 projects in it:

 
 
1. Project SignalRSelfHost

A console application that host SignalR's Hub API. For this project, NuGet Package Microsoft ASP.NET SignalR Self Host is added, which also brings in a few other dependency packages, such as Microsoft.Owin, NewtonSoft.Json.


The code of this project looks like:

using Microsoft.AspNet.SignalR;
using Microsoft.Owin.Cors;
using Microsoft.Owin.Hosting;
using Owin;
using SignalRCadData;
using System;
 
namespace SignalRSelfHost
{
    class Program
    {
        static void Main(string[] args)
        {
            string url = "http://localhost:8080";
            using (WebApp.Start(url))
            {
                Console.Write("server running on {0}", url);
                Console.WriteLine("Press any key to exit...");
                Console.ReadLine();
            }
        }
    }
 
    class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseCors(CorsOptions.AllowAll);
            app.MapSignalR();
        }
    }
 
    public class MyHub : Hub
    {
        //Test method, not used in this blog
        public void Send(string name, string message)
        {
            Clients.All.addMessage(name, message);
        }
 
        //AutoCAD calls this method by sending an 
        //AcadCommandTrack object to MyHub
        public void RelayAcadMessage(AcadCommandTrack cmdTrack)
        {
            //Write to console, in order to see the server does
            //receive calls from SignalR client
            Console.Write("Received data: " + cmdTrack.ToString());
 
            //MyHub calls all connected clients where an Action
            //named as "getAcadMessage" (function in JavScript)
            //will do something on the client side
            Clients.All.getAcadMessage(cmdTrack.ToString());
 
            Console.WriteLine("...data has been sent to client.");
            Console.WriteLine("Press any key to exit...");
        }
    } 
}

Note: I came across an exception when running the console application the first time. It turned out the Microsoft.Owin.Security package brought in by Microsoft ASP.NET SignalR Self Host package as dependency package were out-of-date. After added Microsoft.Owin.Security package separately, the exception went away:



2. Project SignalRCadData

This project only contains a simple data model class AcadCommandTrack:

using System;
 
namespace SignalRCadData
{
    public class AcadCommandTrack
    {
        public string UserName { setget; }
        public string ComputerName { setget; }
        public string CommandName { setget; }
        public DateTime CmdExecTime { setget; }
        public string DwgFileName { setget; }
 
        public override string ToString()
        {
            return "Command \"" + CommandName + "\"" +
                " executed in drawing \"" + DwgFileName + "\"" +
                " at " + CmdExecTime.ToLongTimeString() +
                " on computer \"" + ComputerName + "\"" +
                " by \"" + UserName + "\"";
        }
 
        public AcadCommandTrack()
        {
            UserName = "None";
            ComputerName = "None";
            CommandName = "None";
            CmdExecTime = DateTime.Now;
            DwgFileName = "";
        }
    }
}


3. Project JavaScriptClient

This project is a web application with a single and simple HTML page AcadCmdTrack.html (the Default.html was created for test purpose, which I did not bother to remove it). To be able to act as SignalR client running inside a web browser, the NuGet package Microsoft ASP.NET SignalR JavaScript Client must be added into this project:


The AcadCmdTrack.html page's markup and JavaScript are quite simple:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>AutoCAD Command Tracking</title>
    <style type="text/css">
        .container {
            background-color#99CCFF;
            borderthick solid #808080;
            padding20px;
            margin20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <ul id="discussion"></ul>
    </div>
    <!--Script references. -->
    <!--Reference the jQuery library. -->
    <script src="Scripts/jquery-1.6.4.min.js"></script>
    <!--Reference the SignalR library. -->
    <script src="Scripts/jquery.signalR-2.0.2.min.js"></script>
    <!--Reference the autogenerated SignalR hub script. -->
    <script src="http://localhost:8080/signalr/hubs"></script>
    <!--Add script to update the page and send messages.-->
    <script type="text/javascript">
        $(function () {
            //Set the hubs URL for the connection
            $.connection.hub.url = "http://localhost:8080/signalr";
 
            // Declare a proxy to reference the hub.
            var chat = $.connection.myHub;
 
            // Create a function that the hub can call to broadcast messages.
            chat.client.getAcadMessage = function (message) {
                // Html encode display name and message.
                var encodedMsg = $('<div />').text(message).html();
                // Add the message to the page.
                $('#discussion').append('<li><strong>' + encodedMsg + '</li>');
            };
 
            // Start the connection.
            $.connection.hub.start();
        });
    </script>
</body>
</html>

In the JavaScript, we can see how the web page makes connection to the SignalR server's hub MyHub:

var chat = $.connection.myHub;

and how it defines a function getAcadMessage() so that the SignalR hub on the server side can call an action on the client side.

 
4. Project ConsoleClient

Before I jumped into AutoCAD, I decided to first create a simple console application as a SignalR .NET client to simulate the work of AutoCAD: sending message (represented by class AcadCommandTrack) to SignalR server, so that the message can be instantly shown in web browser (that is, the web browser works like a real time monitor of an AutoCAD session).

The NuGet package Microsoft ASP.NET SignalR .NET Client must be added to this project:


Here is the code for this project:

using Microsoft.AspNet.SignalR.Client;
using System;
using SignalRCadData;
 
namespace ConsoleClient
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Connecting SignalR server...");
 
            var hubConnection = new HubConnection("http://localhost:8080");
 
            bool connected = true;
 
            //IHubProxy must be created before the connection's Start()
            IHubProxy hubProxy = hubConnection.CreateHubProxy("MyHub");
            hubConnection.Start().ContinueWith(task =>
                {
                    if (task.IsCompleted)
                    {
                        Console.WriteLine("Connected successfully.");
                    }
                    else
                    {
                        connected = false;
                        Console.WriteLine("Cannot connect to server: {0}", 
                            task.Exception.GetBaseException().Message);
                    }
                }).Wait();
            
            if (!connected)
            {
                Console.WriteLine("Press any key to exit...");
                Console.ReadLine();
            }
            else
            {
                //This make this client gets server call on the 
                //Action 'getAcadMessage'
                hubProxy.On<string>("getAcadMessage", param =>
                {
                    Console.WriteLine(param);
                });
 
                string serverMethod = "RelayAcadMessage";
                Console.WriteLine("Enter M to send message, or press Enter to exit...");
                while(true)
                {     
                    string input = Console.ReadLine();
                    if (string.IsNullOrEmpty(input)) break;
 
                    //Call the server's RelayAcadMessage() method with
                    //AcadCmdTrack object passed in.
                    //Then all connected SignalR clients will be called
                    //by the server, as long as an Action called
                    //'getAcadMessage' is defined inside the client
                    hubProxy.Invoke<AcadCommandTrack>(
                        serverMethod,
                        new AcadCommandTrack
                        {
                            CommandName = "EDM-YUAN",
                            UserName = "norman.yuan",
                            ComputerName = "LINE",
                            CmdExecTime = DateTime.Now,
                            DwgFileName="Xxxxxx.dwg"
                        }).ContinueWith(task =>
                            {
                                if (task.IsCompleted)
                                {
                                    Console.WriteLine("AcadCommandTrack message is sent.");
                                }
                                else
                                {
                                    Console.WriteLine("Calling \"" + serverMethod + "\" failed: {0}", 
                                        task.Exception.GetBaseException().Message);
                                }
                            }).ContinueWith(c=>
                                {
                                    Console.WriteLine("Enter M to send message, or press Enter to exit...");
                                });
                }
 
                hubConnection.Stop();
                hubConnection.Dispose();
            }
        }
    }
}


After the solution is built successfully, it is time to give it a try. In order to be able to make the server and clients projects all start, I right-clicked the solution in the Solution Explorer, and selected "Set Startup Project...":


so that the server project is started before the client projects.

Here is the video clip showing how they work together as server-client communication goes.


Put AutoCAD In Action

With the success of the solution SignalR_Study, I then brought AutoCAD into the picture in a second Visual Studio solution AcadSignalRStudy:


The solution added the project SignalRCadData from the first solution as external project, so that the AutoCAD addin project AcadSignalRClient can haave reference to it.

The project AcadSignalRClient targets .NET 4.51 because of the requirement of SignalR 2.0. According to Microsoft, the SignalR client can be in older version of SignalR server. That means I could have used older version of SignalR client, so that my AutoCAD addin can still target .NET 4.0. However, using .NET 4.51 in this AutoCAD addin did not cause any issue.

Here is the idea of the entire thing:

I am an very picky boss who supervise a bunch of CAD users. I want to analyse how a Cad user does a particular drafting task by monitoring what commands he/she uses to fulfill that task. So, I do:
  • Set up a SignalR server
  • In AutoCAD, I ask the user turn on AutoCAD command real-time tracking addin
  • Launch a SignalR client web application in my web browser, start watching how user commands AutoCAD working.
What the AutoCAD addin does is basically handling Document.CommandWillStart event. In the event handler a SignalR client (IHubProxy object) collect information on current AutoCAD command and sends the information to SignalR server. The SignalR server in turn sends the information to the connected web browser.

Here is the code for the AutoCAD addin:

using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Microsoft.AspNet.SignalR.Client;
 
[assemblyCommandClass(typeof(AcadSignalRClient.MyCommands))]
 
namespace AcadSignalRClient
{
    public class MyCommands
    {
        private static bool _trackCommand = false;
        private static HubConnection _hubConnection = null;
        private static IHubProxy _hubProxy = null;
        private const string HOST_URL = "http://localhost:8080";
 
        [CommandMethod("TrackCmd"CommandFlags.Session)]
        public static void RunMyCommand()
        {
            Document dwg = Application.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;
 
            if (!_trackCommand)
            {
                try
                {
                    ed.WriteMessage(
                        "\nConnecting to SignalR server...");
 
                    //Attach event handler to DocumentCollection/Document
                    AttachDocumentEventHandlers();
 
                    //Setup SignalR client
                    bool connected = CreateHubConnection();
                    if (connected)
                    {
                        ed.WriteMessage(
                            "\nConnection to SignalR server established!");
                        _trackCommand = true;
                    }
                    else
                    {
                        _trackCommand = false;
                        throw new InvalidOperationException(
                            "cannot connect to SignalR server at " + HOST_URL);
                    } 
    
                    if (_trackCommand)
                    {
                        ed.WriteMessage("\nCommand tracking is on.");
                    }
                }
                catch (System.Exception ex)
                {
                    ed.WriteMessage("\nError: {0}", ex.Message);
 
                    _trackCommand = false;
                    _hubConnection = null;
                    _hubProxy = null;
                }
            }
            else
            {
                _hubConnection.Stop();
                _hubConnection.Dispose();
                _hubConnection = null;
                _hubProxy = null;
 
                ed.WriteMessage("\nCommand tracking is off.");
            }
 
            Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
        }
 
        #region private methods
 
        private static bool CreateHubConnection()
        {
            _hubConnection = new HubConnection(HOST_URL);
            _hubProxy = _hubConnection.CreateHubProxy("MyHub");
 
            bool connected = true;
 
            _hubConnection.Start().ContinueWith(task =>
            {
                if (!task.IsCompleted)
                {
                    connected = false;
                }
            }).Wait();
 
            return connected;
        }
 
        private static void AttachDocumentEventHandlers()
        {
            Document dwg = Application.DocumentManager.MdiActiveDocument;
            dwg.CommandWillStart += dwg_CommandWillStart;
 
            Application.DocumentManager.DocumentCreated += 
                DocumentManager_DocumentCreated;
            Application.DocumentManager.DocumentToBeDestroyed += 
                DocumentManager_DocumentToBeDestroyed;
        }
 
        private static void DocumentManager_DocumentToBeDestroyed(
            object sender, DocumentCollectionEventArgs e)
        {
            e.Document.CommandWillStart -= dwg_CommandWillStart;
        }
 
        private static void DocumentManager_DocumentCreated(
            object sender, DocumentCollectionEventArgs e)
        {
            e.Document.CommandWillStart += dwg_CommandWillStart;
        }
 
        private static void dwg_CommandWillStart(
            object sender, CommandEventArgs e)
        {
            if (!_trackCommand) return;
 
            Document dwg = Application.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;
            ed.WriteMessage("\nCommand {0} is about to start...", 
                e.GlobalCommandName.ToUpper());
 
            string dwgFileName = System.IO.Path.GetFileName(dwg.Name);
 
            SendMessageToSignalRServer(e.GlobalCommandName, dwgFileName);
        }
 
        private static void SendMessageToSignalRServer(
            string cmdName, string dwgFileName)
        {
            if (_hubConnection == nullreturn;
 
            string user = Environment.UserName;
            string computer = Environment.MachineName;
 
            SignalRCadData.AcadCommandTrack atrck = 
                new SignalRCadData.AcadCommandTrack()
                {
                    UserName = user,
                    ComputerName = computer,
                    CommandName = cmdName,
                    CmdExecTime = DateTime.Now,
                    DwgFileName=dwgFileName
                };
 
            //Call the hub method "RelayAcadMessage()" on 
            //SignalR server side
            _hubProxy.Invoke<SignalRCadData.AcadCommandTrack>(
                "RelayAcadMessage", atrck);
        }
 
        #endregion
    }
}
 
After the code was built successfully, I started run the projects in solution SignalR_Study first. Then I started solution "AcadSignalRStudy", which launches AutoCAD2014. after loading the addin DLL into AutoCAD, I entered the command "TrackCmd" to turn on monitoring to execution of AutoCAD commands. From this moment on, any command started can be watched in the monitoring web browser, until the command tracking process is turned off.

See this video clip showing how AutoCAD commands are monitored in real-time.


Final Thought

With the computing power moving to the Internet/cloud, more and more user applications run inside web browser instead of being native desktop application. Being so easily communicate an application inside an web browser by using SignalR is certainly a good thing to know and to use, when there is real business need. This article is just a scratch of it. While we AutoCAD programmers are still enjoying the ease and powerful AutoCAD .NET API when developing against one of the most important, traditional desktop application - AutoCAD, we should expand our programming knowledge/skill to embrace the coming tide of cloud based computing. ASP.NET SignalR is one of those things we could learn and get benefited from it, for now, at least.

Download the source code here.

Tuesday, February 25, 2014

Showing and Closing Modeless Form/Dialog Box With System.Windows.Forms.Form

When programming AutoCAD, there are 2 styles of dialog box can be shown: Modal Dialog Box and Modeless Dialog Box. Modal dialog box is usually used for collecting user inputs, while modeless dialog box is more focused on showing information. While the dialog box displayed, modal dialog box blocks user from direct interaction with AutoCAD (that is, user cannot interact with AutoCAD UI until the modal dialog box is dismissed. But it DOES NOT mean AutoCAD stops its processing until the modal dialog box is closed), modeless dialog box floats on top AutoCAD UI and user can still interact with AutoCAD UI.

With AutoCAD .NET API, one can use either Windows Form or WPF Window as dialog box. This discussion is limited to Windows Form, because it is still used far more often than WPF window.

System.Windows.Forms.Form class has 2 methods to show a form: ShowDialog() and Show(). The former shows a form as modal form and the latter shows a form as modeless form. Depending on which method is used to show a form, the form's closing behaviour is different (either by calling Form.Close(), or by clicking "x" button on the form), which I have seen is misunderstood quite often.

The difference is:
  • For modeless form, Form.Close() (or clicking the "x" button on the form. I'll not repeat this hereafter) also calls the Dispose() method;
  • For modal form, Form.Close() only hide the form, which is basically equivalent to setting the form's Visible property to False. The modal form can also be "closed" by calling Form.Hide(), by setting Form.DialogResult. That is, the modal form cannot be really closed unless Form.Dispose() is called explicitly.
In AutoCAD .NET API, one should use ShowModalDialog()/ShowModelessDialog() instead of Form.ShowDialog/Form.Show() to display modal/modeless dialog boxes. These 2 methods have the same behaviours in terms of how form/dialog box is closed.

When using modeless form in AutoCAD, one thing to be careful is how the form is opened and closed/hidden. The fact while the form is open AutoCAD's active document can be changed by user interaction makes things a bit complicated, if the information showing on the form is document specific. Here I am going to show some code dealing with modeless form in 2 different ways: creating a singleton form in entire AutoCAD session; or creating multiple forms (per document) and showing them accordingly.

Here is the Visual Studio 2012 solution, which includes 3 projects:





Project "DwgDataView" contains a Windows UserControl and Windows Form that can be used in AutoCAD addins:




This project also has a data model class and a AutoCAD utility class to get and hold drawing specific information.

The code follows.

Class DrawingData:

    1 namespace DwgDataView
    2 {
    3     public class DrawingData
    4     {
    5         public string CurrentDocName { set; get; }
    6         public string CurrentLayer { set; get; }
    7         public double CurrentDimScale { set; get; }
    8     }
    9 }

Class DrawingDataUtil:

    1 using System;
    2 using AcadApp = Autodesk.AutoCAD.ApplicationServices.Application;
    3 
    4 namespace DwgDataView
    5 {
    6     public class DrawingDataUtil
    7     {
    8         public static DrawingData GetCurrentDrawingData()
    9         {
   10             return new DrawingData
   11             {
   12                 CurrentDocName=AcadApp.DocumentManager.MdiActiveDocument.Name,
   13                 CurrentLayer=AcadApp.GetSystemVariable("CLAYER").ToString(),
   14                 CurrentDimScale=Convert.ToDouble(AcadApp.GetSystemVariable("DIMSCALE"))
   15             };
   16         }
   17     }
   18 }

Code of UserControl DrawingDataView:

    1 using System.Windows.Forms;
    2 
    3 namespace DwgDataView
    4 {
    5     public partial class DrawingDataView : UserControl
    6     {
    7         public DrawingDataView()
    8         {
    9             InitializeComponent();
   10         }
   11 
   12         public void SetDataView(DrawingData data)
   13         {
   14             txtDrawingName.Text = data.CurrentDocName;
   15             txtLayer.Text = data.CurrentLayer;
   16             txtDimScale.Text = data.CurrentDimScale.ToString();
   17         }
   18 
   19         public void ClearDataView()
   20         {
   21             txtDimScale.Text = "";
   22             txtDrawingName.Text = "";
   23             txtLayer.Text = "";
   24         }
   25     }
   26 }

Code of Form frmDrawing:

    1 using System;
    2 using System.Windows.Forms;
    3 
    4 namespace DwgDataView
    5 {
    6     public partial class frmDrawing : Form
    7     {
    8         //This property is only used for showing multiple modeless forms
    9         public bool ShowMe { set; get; }
   10 
   11         //This property is used to make the modeless form close differently:
   12         //hidden as closed, or disposed as closed
   13         public bool HiddenAsClosed { set; get; }
   14 
   15         public frmDrawing()
   16         {
   17             InitializeComponent();
   18 
   19             ShowMe = false;
   20             HiddenAsClosed = true;
   21         }
   22 
   23         public void SetDataView(DrawingData data)
   24         {
   25             ctlDataView.SetDataView(data);
   26         }
   27 
   28         public void ClearDataView()
   29         {
   30             ctlDataView.ClearDataView();
   31         }
   32 
   33         private void btnClose_Click(object sender, EventArgs e)
   34         {
   35             if (HiddenAsClosed)
   36             {
   37                 ShowMe = false;
   38                 this.Hide();
   39             }
   40             else
   41             {
   42                 this.Close();
   43             }
   44         }
   45 
   46         private void frmDrawing_FormClosing(object sender, FormClosingEventArgs e)
   47         {
   48             if (HiddenAsClosed)
   49             {
   50                 e.Cancel = true;
   51                 ShowMe = false;
   52                 this.Hide();
   53             }
   54         }
   55     }
   56 }

In the project SigletonModelessDialog, the modeless form will not be disposed (by calling Form.Dispose() or Form.Close()) once it is instantiated. User's interaction with the form can hide the form.

In this scenario, when the modeless form deals with per document data, we need to make sure the modeless form is tied to correct document, usually the active document, which can change when user works with AutoCAD while the modeless form is visible.

Here is code of project SingletonModelessDialog:

    1 using Autodesk.AutoCAD.ApplicationServices;
    2 using Autodesk.AutoCAD.EditorInput;
    3 using Autodesk.AutoCAD.Runtime;
    4 using AcadApp = Autodesk.AutoCAD.ApplicationServices.Application;
    5 
    6 using DwgDataView;
    7 
    8 [assembly: CommandClass(typeof(SingletonModelessDialog.MyCommands))]
    9 [assembly: ExtensionApplication(typeof(SingletonModelessDialog.MyCommands))]
   10 
   11 namespace SingletonModelessDialog
   12 {
   13     public class MyCommands : IExtensionApplication
   14     {
   15         private static frmDrawing _frm = null;
   16 
   17         public void Initialize()
   18         {
   19             DocumentCollection dwgCol = AcadApp.DocumentManager;
   20             dwgCol.DocumentBecameCurrent += dwgCol_DocumentBecameCurrent;
   21             Document dwg = AcadApp.DocumentManager.MdiActiveDocument;
   22             Editor ed = dwg.Editor;
   23 
   24             try
   25             {
   26                 ed.WriteMessage("\nInitializing {0}...", this.GetType().Name);
   27 
   28                 ed.WriteMessage("comleted\n");
   29             }
   30             catch (System.Exception ex)
   31             {
   32                 ed.WriteMessage("failed:\n{0}", ex.ToString());
   33             }
   34         }
   35 
   36         public void Terminate()
   37         {
   38             if (_frm != null)
   39             {
   40                 if (!_frm.IsDisposed) _frm.Dispose();
   41             }
   42         }
   43 
   44         private void dwgCol_DocumentBecameCurrent(
   45             object sender, DocumentCollectionEventArgs e)
   46         {
   47             if (_frm == null) return;
   48 
   49             if (_frm.Visible)
   50             {
   51                 //Reload drawing data to the form, when the form turns from
   52                 //invisible to visible. To make the code more efficient,
   53                 //we also check if the drawing information on the form points
   54                 //to the same drawing that became current, and only refresh
   55                 //information on the form when drawing is different
   56                 DrawingData data =
   57                     DwgDataView.DrawingDataUtil.GetCurrentDrawingData();
   58                 _frm.SetDataView(data);
   59             }
   60         }
   61 
   62         [CommandMethod("DwgDlg", CommandFlags.Session)]
   63         public static void RunMyCommand()
   64         {
   65             if (_frm == null)
   66             {
   67                 _frm = new frmDrawing();
   68             }
   69 
   70             DrawingData data =
   71                 DwgDataView.DrawingDataUtil.GetCurrentDrawingData();
   72             _frm.SetDataView(data);
   73             AcadApp.ShowModelessDialog(_frm);
   74         }
   75     }
   76 }

As the code shows. using singleton modeless dialog box, the form/dialog box is only instantiated once, and it stays in memory until AutoCAD session ends. When user closes the form, it becomes invisible, instead of being disposed.

See this video clip showing how the singleton modeless dialog behaves.

Then in the project MultiModelessDialog, The intention is to create a modeless form for each document when needed. The modeless form is also never disposed once created, only being set to visible/invisible accordingly.

Here is the code for project MultiModelessDialog:

    1 using System.Collections.Generic;
    2 using AcadApp = Autodesk.AutoCAD.ApplicationServices.Application;
    3 using Autodesk.AutoCAD.ApplicationServices;
    4 using Autodesk.AutoCAD.EditorInput;
    5 using Autodesk.AutoCAD.Runtime;
    6 using DwgDataView;
    7 
    8 [assembly: CommandClass(typeof(MultiModelessDialog.MyCommands))]
    9 [assembly: ExtensionApplication(typeof(MultiModelessDialog.MyCommands))]
   10 
   11 namespace MultiModelessDialog
   12 {
   13     public class MyCommands : IExtensionApplication
   14     {
   15         //Static member
   16         private static Dictionary<Document, frmDrawing> _modelessForms = null;
   17 
   18         //non-static member, will be created with each document
   19         private frmDrawing _frm = null;
   20 
   21         public void Initialize()
   22         {
   23             DocumentCollection dwgCol = AcadApp.DocumentManager;
   24             dwgCol.DocumentBecameCurrent += dwgCol_DocumentBecameCurrent;
   25             dwgCol.DocumentToBeDestroyed += dwgCol_DocumentToBeDestroyed;
   26 
   27             _modelessForms = new Dictionary<Document, frmDrawing>();
   28 
   29             Document dwg = AcadApp.DocumentManager.MdiActiveDocument;
   30             Editor ed = dwg.Editor;
   31 
   32             try
   33             {
   34                 ed.WriteMessage("\nInitializing {0}...", this.GetType().Name);
   35 
   36                 ed.WriteMessage("comleted\n");
   37             }
   38             catch (System.Exception ex)
   39             {
   40                 ed.WriteMessage("failed:\n{0}", ex.ToString());
   41             }
   42         }
   43 
   44         private void dwgCol_DocumentToBeDestroyed(
   45             object sender, DocumentCollectionEventArgs e)
   46         {
   47             //When a document is closed (to be destroyed by AutoCAD),
   48             //its corresponding form, if exists, will be disposed, and
   49             //the form's reference is removed from the static Dictionary
   50             if (_modelessForms.ContainsKey(e.Document))
   51             {
   52                 _modelessForms[e.Document].Dispose();
   53                 _modelessForms.Remove(e.Document);
   54             }
   55         }
   56 
   57         private void dwgCol_DocumentBecameCurrent(
   58             object sender, DocumentCollectionEventArgs e)
   59         {
   60             //Loop through the static Dictionary clooection to see whether
   61             //the current document has a corresponding form instantiated or not.
   62             //If the form exists and was not closed by user previously, then
   63             //bring it to visible. In the meantime, forms corresponding to other
   64             //documents are closed (set to invisible).
   65             foreach (KeyValuePair<Document, frmDrawing> item in _modelessForms)
   66             {
   67                 if (e.Document == item.Key)
   68                 {
   69                     if (item.Value.ShowMe)
   70                     {
   71                         item.Value.Visible = true;
   72 
   73                         //If necessary, we could refresh the data showing on
   74                         //the form
   75                         DrawingData data =
   76                             DwgDataView.DrawingDataUtil.GetCurrentDrawingData();
   77                         item.Value.SetDataView(data);
   78                     }
   79                     else
   80                         item.Value.Visible = false;
   81                 }
   82                 else
   83                 {
   84                     item.Value.Visible = false;
   85                 }
   86             }
   87         }
   88 
   89         public void Terminate()
   90         {
   91 
   92         }
   93 
   94 
   95         //non-static command method, when executed, each document will
   96         //instantiate a class "MyCommand" with a private member of frmDrawing
   97         [CommandMethod("DwgDlg")]
   98         public void RunMyCommand()
   99         {
  100             if (_frm == null)
  101             {
  102                 DrawingData data =
  103                     DwgDataView.DrawingDataUtil.GetCurrentDrawingData();
  104                 _frm = new frmDrawing();
  105                 _frm.SetDataView(data);
  106 
  107                 _modelessForms.Add(
  108                     AcadApp.DocumentManager.MdiActiveDocument, _frm);
  109             }
  110 
  111             _frm.ShowMe = true;
  112             AcadApp.ShowModelessDialog(_frm);
  113         }
  114     }
  115 }

Pay attention to the use of the non-static CommandMethod in this project, which ensures that the class MultiModelessDialog.MyCommand is instantiated for each document when the CommandMethod is called, thus one modeless form of frmDrawing type can be instantiated. For more information on "static" or "non-static" CommandMethod, see one of my old article here.

See this video clip showing how the multiple modeless dialogs behave.

In both cases of singleton form and multiple forms projects here the form or forms will never be disposed once opened. It simply changes its visibility as needed.

In the singleton case, the form simply be destroyed when AutoCAD session is closed. We can choose to add some code in IExtenstionApplication.Terminate() implementation to dispose the modeless form properly, if there is some complicated resources being used by the form and should be released nicely.

In the case of multiple modeless forms, the modeless form corresponding to each document is disposed when the document is to be closed.

Well, I could have chosen to actually dispose/close the modeless form when user clicks "Close" button or clicks the "x" on the form, instead of hiding the form. This may actually save some memory usage, if the form size is big (has a lots of controls, showing a lots of data, showing some images...). Then, this may results in some performance penalty: each time the form is to show, it has to be re-established from scratch, even user just simply switch the active document. Modern AutoCAD capable computer usually comes with huge amount of memory, and keeping a few moderately sized forms in memory with a few extra MB would not be a much of issue, in my opinion.

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.