Monday, February 27, 2017

Handling Document.UnknownCommand Event, 1 of 2

In one of my recent article (Custom On Demand Loading), I mentioned of handling Document.UnknownCommand could be used for on demand loading (last paragraph of that article). In my further dealing with UnknownCommand event, I also ran into an interesting issue. So, I decided to write down my experience on dealing with UnknownCommand to share with the community. I split the writing into 2 parts. This is the part 1: yet another on demand loading approach.

When thinking of loading application (DLLs, in the case of AutoCAD .NET add-in), the key is to find a suitable trigger moment when AutoCAD runs.When this trigger is pull off, a set of conditions/configurations should be examined to determine if particular applications/DLLs are already loaded in the AutoCAD process, and loaded them if necessary.

In my previous article, I wanted particular applications being loaded when the currently activated drawing is from specific project (project type, location, client... could be used as the criteria), then the trigger point is DocumentCollection.DocumentBecameCurrent event: the code would examine the drawing's project information to decide what applications should be available. Then if the applications have not been loaded, load them.

Now with handling Document.UnknownCommand we can have more generic on demand loading approach: whenever user enters a command from an application which has not been loaded, AutoCAD fires the UnknownCommand event. This allows our code in the event handler to use the unknown command name against a set of on demand loading configuration data to determine which applications are needed to be loaded. Here, we do not need to check if the application has already been loaded (otherwise, the UnknownCommand would not fire).

Here is a DLL project that is compiled as MyToolA.dll, in which 2 commands are defined:
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(MyToolA.Commands))]
[assemblyExtensionApplication(typeof(MyToolA.Commands))]
 
namespace MyToolA
{
    public class Commands : IExtensionApplication
    {
        public void Initialize()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
            ed.WriteMessage(
                "\nLoading MyToolA.dll...done.\n");
        }
 
        public void Terminate()
        {
            
        }
 
        [CommandMethod("Command_A1")]
        public static void RunCommandA1()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            ed.GetString(
                "\nThis \"Command_A1\" in DLL \"MyToolA.dll\"......" +
                "Press anykey to continue...");
            ed.WriteMessage("\n");
        }
 
        [CommandMethod("Command_A2")]
        public static void RunCommandA2()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            ed.GetString(
                "\nThis \"Command_A2\" in DLL \"MyToolA.dll\"......" +
                "Press anykey to continue...");
            ed.WriteMessage("\n");
        }
    }
}

Then here is another DLL project. compiled as MyToolB.ll, in which another 2 commands are defined:
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(MyToolB.Commands))]
[assemblyExtensionApplication(typeof(MyToolB.Commands))]
 
namespace MyToolB
{
    public class Commands : IExtensionApplication
    {
        public void Initialize()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
            ed.WriteMessage(
                "\nLoading MyToolB.dll is...done.\n");
        }
 
        public void Terminate()
        {
 
        }
 
        [CommandMethod("Command_B1")]
        public static void RunCommandA1()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            ed.GetString(
                "\nThis \"Command_B1\" in DLL \"MyToolB.dll\"......" +
                "Press anykey to continue...");
            ed.WriteMessage("\n");
        }
 
        [CommandMethod("Command_B2")]
        public static void RunCommandA2()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            ed.GetString(
                "\nThis \"Command_B2\" in DLL \"MyToolB.dll\"......" +
                "Press anykey to continue...");
            ed.WriteMessage("\n");
        }
    }
}

Now, I want these 2 DLLs to be loaded on demand, that is, when one of the commands defined in these 2 DLLs is called in AutoCAD, AutoCAD will load the DLL. So, I first came up with a simple set of auto-loading configuration, which is a Dictionary. That is, each Dictionary entry represents a DLL file name (entry's key) and an array of command names defined in the DLL (entry's value):
using System.Collections.Generic;
 
namespace HandlingUnknownCommand
{
    public class AutoLoaderSettings : Dictionary<stringstring[]>
    {
        public string GetDllByCommandName(string cmdName)
        {
            foreach (var item in this)
            {
                var cmds = item.Value;
                foreach(var cmd in cmds)
                {
                    if (cmd.ToUpper() == cmdName.ToUpper()) return item.Key;
                }
            }
 
            return null;
        }
 
        // A static factory method to generate a set of test AutoLoaderSetting
        public static AutoLoaderSettings CreateTestSettings()
        {
            var settings = new AutoLoaderSettings();
 
            settings.Add(
                "MyToolA.dll",
                new string[] { "Command_A1""Command_A2" });
 
            settings.Add(
                "MyToolB.dll",
                new string[] { "Command_B1""Command_B2" });
 
            return settings;
        }
    }
}

Here comes the auto-loader:
using System.Reflection;
 
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(HandlingUnknownCommand.MyAutoLoader))]
[assemblyExtensionApplication(typeof(HandlingUnknownCommand.MyAutoLoader))]
 
namespace HandlingUnknownCommand
{
    public class MyAutoLoader : IExtensionApplication 
    {
        private static bool _autoLoad = false;
        private static AutoLoaderSettings _settings = 
            AutoLoaderSettings.CreateTestSettings();
 
        public void Initialize()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                ed.WriteMessage("\nInitializing MyAutoLoader...");
                CadApp.DocumentManager.DocumentCreated += 
                    DocumentManager_DocumentCreated;
                foreach (Document doc in CadApp.DocumentManager)
                {
                    doc.UnknownCommand += Document_UnknownCommand;
                }
                _autoLoad = true;
                ed.WriteMessage("\nMyAutoLoader initialized successfully.");
            }
            catch(System.Exception ex)
            {
                _autoLoad = false;
                ed.WriteMessage(
                    "\nInitialzing MyAutoLoader failed:\n{0}\n", ex.Message);
            }
        }
 
        public void Terminate()
        {
 
        }
 
        private static void DocumentManager_DocumentCreated(
            object sender, DocumentCollectionEventArgs e)
        {
            e.Document.UnknownCommand += Document_UnknownCommand;
        }
 
        private static void Document_UnknownCommand(
            object sender, UnknownCommandEventArgs e)
        {
            if (!_autoLoad) return;
 
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var cmdName = e.GlobalCommandName;
            var dllFileName = _settings.GetDllByCommandName(cmdName);
            if (!string.IsNullOrEmpty(dllFileName))
            {
                try
                {
                    LoadAssemblyDll(dllFileName);
                    ed.WriteMessage(
                        "\nDll {0} loaded, command {1} resuming...",
                        dllFileName.ToUpper(), cmdName.ToUpper());
                }
                catch (System.Exception ex)
                {
                    ed.WriteMessage(
                        "\nAutoloading DLL failed: {0}\n", ex.Message);
                }    
            }
        } 
        
        private static void LoadAssemblyDll(string dllFileName)
        {
            string path = System.IO.Path.GetDirectoryName(
                Assembly.GetExecutingAssembly().Location);
            string dll = path + "\\" + dllFileName;
 
            if (!ExtensionLoader.IsLoaded(dll))
            {
                if (System.IO.File.Exists(dll))
                {
                    ExtensionLoader.Load(dll);
                }
                else
                {
                    throw new System.IO.FileNotFoundException(
                        "Cannot find DLL file: " + dllFileName + "!");
                }
            }
        } 
    }
}


Now, see this video clip to see how the code runs.

As we can see, the 2 Dlls that actually define the commands are not preloaded, yet we can enter the commands and let the custom AutoLoader load the Dlls. Another interesting thing is that once the custom AutoLoader loads the Dlls according to configuration, AutoCAD automatically resumes the command (previously unknown whent eh command is issued, but becoming known after the Dll is loaded) execution. An extr message at command line is generated by AutoCAD. See the highlight in this picture:



AutoCAD's this behaviour indicates that if there is any UnknowCommand event handler other than .NET API's built-in one, AutoCAD will try the command again after the custom event handler execution. This allows us to make a command purely be loaded on demand and provide a good user experience: a not loaded command is loaded and executed without interruption, except for the extra line of message at command line. Well, there is a little catch: AutoCAD's auto-command-completion might be a bit annoying, if you have a custom command that similar to an existing AutoCAD command and you have to enter the custom command at command line.

Anyway. simply handling UnknownCommand event would allow us to build a custom Dll/command auto-loading process quite easily.

I'll be talking another interesting issue on handling UnknownCommand event in next post.