Wednesday, October 3, 2018

Executing Command from PaletteSet

This article is inspired by the question post in Autodesk's .NET discussion forum here. There could be different solutions to that question and I thought it would be better to put forth mine with actual code provided, which might be too long to post as reply in the discussion forum. So, I decided to post it here for better readability.

When using PaletteSet as UI to allow user to interact with AutoCAD (i.e. letting AutoCAD do particular processing), it is common practice to use Document.SendStringToExecute() to call AutoCAD command, either built-in one, or custom-built one. PaletteSet is a modeless UI, floating on top of AutoCAD window and user can freely change the focus between AutoCAD window, or the PaletteSet window. This would cause issue when a command is active with AutoCAD (usually, it is in the middle of the command, waiting for user input) and user goes to the PaletteSet to trigger another command, as described in the question posted in the discussion forum, such as clicking a button to call a command to insert a block. In this case, the interaction with PaletteSet either results in AutoCAD command line showing error message; or nothing happens at AutoCAD command line - the active command is still waiting to be either completed, or cancelled.

One way to handle this issue is always test if there is active command in PaletteSet' user interaction event handler first, and only goes ahead when there is no active command.

The other approach is whenever PaletteSet's user interaction even is triggered, always cancel any active command first, just like we usually do with any menu/toolbar/ribbon item macro: prefixing it with "^C^C" to cancel possible active command before the macro is called.

Obviously the latter approach would be desirable in most cases and and more compliant with AutoCAD conventions.

So, here is my solution to the question posted in the forum aforementioned.

Firstly I created a custom PaletteSet. One should ALWAYS derive custom PaletteSet from Autodesk.AutoCAD.Windows.PaletteSet class. DO NOT directly use PaletteSet class without subclass it.

Following is the code of the Palette (System.Windows.Forms.UserControl), which simply has 2 buttons; each button's Tag property is given a valid block name; that means, clicking on each button would trigger a block inserting command. Since the UI is very simple, I only show it code behind here:

using System;
using System.Windows.Forms;
 
namespace SendCommandFromPaletteSet
{
    public partial class BlockPalatte : UserControl
    {
 
        public BlockPalatte()
        {
            InitializeComponent();
        }
 
        public event BeginBlockInsertingEventHandler BeginBlockInserting;
 
        // the 2 buttons' Click event is wired to this event handler
        private void ButtonClick(object sender, EventArgs e)
        {
            var tag = ((Control)sender).Tag;
 
            if (tag!=null)
            {
                var blkName = tag.ToString();
                if (!string.IsNullOrEmpty(blkName))
                {
                    BeginBlockInserting?.Invoke(
                        sender, new BeginBlockInsertingEventArgs(blkName));
                }
            }
        }
    }
}

Here is the class MyBlockPaletteSet:

using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Windows;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace SendCommandFromPaletteSet
{
    public class MyBlockPaletteSet : PaletteSet
    {
        private BlockPalatte _blkPalette;
        private bool _doInsertion = false;
        public string CurrentBlockName { private setget; }
 
        public MyBlockPaletteSet() : base(
            "My Block PaletteSet""BlkPs"new Guid("B33AE81D-0FBB-49EA-83D2-62D667EEDDCA"))
        {
            this.Style = PaletteSetStyles.ShowCloseButton |
                 PaletteSetStyles.UsePaletteNameAsTitleForSingle |
                  PaletteSetStyles.Snappable;
 
            this.MinimumSize = new System.Drawing.Size(400, 400);
            this.KeepFocus = true;
 
            _blkPalette = new BlockPalatte();
            Add("Blocks", _blkPalette);
 
            _blkPalette.BeginBlockInserting += BlkPalette_BeginBlockInserting;
        }
 
        private void BlkPalette_BeginBlockInserting(object sender, BeginBlockInsertingEventArgs e)
        {
            if (!string.IsNullOrEmpty(e.BlockName))
            {
                var dwg = CadApp.DocumentManager.MdiActiveDocument;
                CurrentBlockName = e.BlockName;
 
                var cmdActive = Convert.ToInt32(CadApp.GetSystemVariable("CMDACTIVE"));
                if (cmdActive>0)
                {
                    dwg.CommandCancelled += Dwg_CommandCancelled;
 
                    _doInsertion = true;
                    dwg.SendStringToExecute("\x03\x03"falsetruefalse);
                }
                else
                {
                    DoBlockInsert(dwg);
                } 
            }
        }
 
        private void Dwg_CommandCancelled(object sender, CommandEventArgs e)
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
 
            if (_doInsertion)
            {
                dwg.CommandCancelled -= Dwg_CommandCancelled;
                _doInsertion = false;
                DoBlockInsert(dwg);
            }
        }
 
        private void DoBlockInsert(Document dwg)
        {
            CadApp.MainWindow.Focus();
            dwg.SendStringToExecute("InsBlk "truefalsefalse);
        }
    }
 
    public class BeginBlockInsertingEventArgs : EventArgs
    {
        public string BlockName { private setget; }
        public BeginBlockInsertingEventArgs(string blkName)
        {
            BlockName = blkName;
        }
    }
 
    public delegate void BeginBlockInsertingEventHandler(object sender, BeginBlockInsertingEventArgs e);
}

As the code shows, the user interaction event (clicking the buttons) in the Palette is bubbled to the custom PaletteSet, where the actual command execution is called (via SendStringToExecute()). Also, custom PaletteSet conveys the user input information (what block is to insert).

The trick of dealing the issue raised in aforementioned question is to test if there is active command with current active document by examine system variable "CMDACTIVE". If no, go ahead to send command to execution; if yes, hook up the CommandCancelled event of active drawing, and then send "^C^C" as command to cancel active command, what ever it is, and then set a flag to indicate new command is waiting to be executed. Therefore, the Command_Cancelled event handler would be triggered and pending command from user interaction with Palette is sent to execution.

Following is the command class that shows the custom PaletteSet and does the actual work: inserting multiple blocks in a loop whenever user clicks a button in the Palette. To simplify the code, I only have code to let user to pick insertion point in a loop until user either cancels the loop, or click the button in the Palette, which cancels the active point picking loop. Here is the code:

using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(SendCommandFromPaletteSet.Commands))]
 
namespace SendCommandFromPaletteSet
{
    public class Commands
    {
        private static MyBlockPaletteSet _blkPs = null;
 
        [CommandMethod("BlkPs"CommandFlags.Session)]
        public static void RunCommand()
        {
            if (_blkPs==null)
            {
                _blkPs = new MyBlockPaletteSet();
            }
 
            _blkPs.Visible = true;
        }
 
        [CommandMethod("InsBlk"CommandFlags.NoHistory )]
        public static void InsertBlock()
        {
            if (_blkPs == null ||
                !_blkPs.Visible) return;
 
            var blkName = _blkPs.CurrentBlockName;
                InsertBlock(blkName);
        }
 
        #region private methods
 
        private static void InsertBlock(string blkName)
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var count = 0;
            while(true)
            {
                if (!PickInsertionPoint(ed, out Point3d pt))
                {
                    break;
                }
                else
                {
                    count++;
                    ed.WriteMessage($"Inserting block \"{blkName}\" #{count}...");
                }
            }
        }
 
        private static bool PickInsertionPoint(Editor ed, out Point3d pt)
        {
            pt = Point3d.Origin;
 
            var res = ed.GetPoint("\nSelect block position:");
            if (res.Status== PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }
 
        #endregion
    }
}

Watch this video clip for the code in action. As the video clip shows, when there is active command waiting for user input, be it AutoCAD built-in command, or a custom command, or the command started by the PaletteSet, whenever user clicks a button in the PaletteSet, the active command is cancelled, and whatever command tied to the button-click starts.



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.