Friday, September 28, 2018

Showing Modeless Form with Help of PerDocumentClass

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

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

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

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

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

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

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

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

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


This video clip shows the code in action

Some Thought

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

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

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

Download source code here.