Sunday, December 22, 2013

An Issue With Using Ribbon Runtime API

I recently ran into an issue with Ribbon Runtime API, which an AutoCAD .NET programmer may also come across, if he/she uses Ribbon Runtime API to create ribbon tab/panel to start his/her command method.

Due to the infamous "Fiber" technology used in AutoCAD 2010 through AutoCAD 2014, most of us .NET programmers know that we need to set "Fiberworld" system variable to 0 in order to debug .NET project with AutoCAD in many, if not all, Visual Studio projects, in the expense of losing ribbon/menu usability. Because of this, the issue I ran into with Ribbon Runtime API went undetected from the tests of a few my projects. And when users reported that some of my commands did not work as expected, I was not able to reproduce what users described. Instead, I was looked into whether AutoCAD setup is good, whether OS is OK...which led to a few AutoCAD repairs/reinstalls, but nothing worked.

What the issue is, then? Here is what happens:

One of my command method, when executed, brings up a modal dialog box. There is a button "Pick >" on the form. If user clicks the button, the form hides, then user picks one or more entities in AutoCAD editor. User can also cancels he picking, of course. Once the picking is done/cancelled, the dialog form shows back with the picking result (if the picking is not cancelled) displayed. Pretty standard operation, eh? Yet, users reported that SOMETIMES, after picking, the dialog form flashed back and disappeared immediately. The code the follows after the dialog box being OKed/being cancelled (there are quite some code after the dialog box is closed to be executed) was also not executed and the command method was simply jumped to its end and the "try...catch..." block did not catch anything. Notice the capital "SOMETIMES"? The same user reported the command works sometimes, and then sometimes not. And whenever I tried with my development computer, it always works (why shouldn't it? It is just something I have done many times before. Really strange!).

Eventually, one of the users noticed, the command works when the command is started from a fully expanded ribbon item, and stops working if the ribbon is minimized (AutoCAD ribbon has 3 minimized states). This explains why I never ran into the issue, because I often have to set "Fiberworld" to 0 with my AutoCAD in order to do debugging (thus, ribbon in my AutoCAD often does not work), so I am used to enter command at command line to run my command methods. The ribbon item that starts the command method is included in a custom ribbon tab/panel that is specifically designed/developed for our custom CAD add-in applications and is dynamically generated by code using Ribbon Runtime API.

After finally being able to reproduce the issue myself, I went ahead to manually created a custom partial CUIx to build the same ribbon tab/panel/item that execute the same command method. With the ribbon built with CUIx, whether it is fully expanded or minimized, the execution of the command method is always succeeded as expected.

Obviously, the said issue only exists with ribbon created programmatically using Ribbon Runtime API. The code shown below reproduces the issue, followed by a video clip.

First a couple of utility classes.

Class MyRibbon, used for creating a ribbon tab, adding a ribbon panel with a button into the tab:

    1 using System;
    2 using System.Collections.Generic;
    3 using System.Linq;
    4 using System.Text;
    5 using Autodesk.Windows;
    6 using Autodesk.AutoCAD.ApplicationServices;
    7 using Autodesk.AutoCAD.Ribbon;
    8 
    9 namespace RibbonKillsDialog
   10 {
   11     public class MyRibbon
   12     {
   13         private const string TAB_TITLE = "Test Apps";
   14         private const string TAB_ID = "TestApps";
   15 
   16         private const string PANEL_TITLE = "Test Commands";
   17 
   18         public static void AddMyRibbon()
   19         {
   20             RibbonTab tab = CreateRibbonTab();
   21 
   22             BuildPanel(tab);
   23         }
   24 
   25         public static void RemoveMyRibbon()
   26         {
   27             RemoveRibbonTab();
   28         }
   29 
   30         #region private methods
   31 
   32         private static RibbonTab CreateRibbonTab()
   33         {
   34             Autodesk.Windows.RibbonControl ribbonControl =
   35                 RibbonServices.RibbonPaletteSet.RibbonControl;
   36 
   37             Autodesk.Windows.RibbonTab tab = null;
   38 
   39             //Find existing ribbon tab
   40             foreach (var t in ribbonControl.Tabs)
   41             {
   42                 if (t.Title.ToUpper() == TAB_TITLE.ToUpper() &&
   43                     t.Id.ToUpper() == TAB_ID.ToUpper())
   44                 {
   45                     tab = t;
   46                     break;
   47                 }
   48             }
   49 
   50             //If no existing tab found
   51             if (tab == null)
   52             {
   53                 tab = new Autodesk.Windows.RibbonTab();
   54                 tab.Title = TAB_TITLE;
   55                 tab.Id = TAB_ID;
   56 
   57                 ribbonControl.Tabs.Add(tab);
   58             }
   59 
   60             return tab;
   61         }
   62 
   63         private static void RemoveRibbonTab()
   64         {
   65             Autodesk.Windows.RibbonControl ribbonControl =
   66                 RibbonServices.RibbonPaletteSet.RibbonControl;
   67 
   68             foreach (var t in ribbonControl.Tabs)
   69             {
   70                 if (t.Title.ToUpper() == TAB_TITLE.ToUpper() &&
   71                     t.Id.ToUpper() == TAB_ID.ToUpper())
   72                 {
   73                     ribbonControl.Tabs.Remove(t);
   74                     break;
   75                 }
   76             }
   77         }
   78 
   79         private static void BuildPanel(RibbonTab tab)
   80         {
   81             RibbonPanel panel = null;
   82             RibbonPanelSource panelSource = null;
   83 
   84             foreach (var p in tab.Panels)
   85             {
   86                 if (p.Source.Title.ToUpper() == PANEL_TITLE.ToUpper())
   87                 {
   88                     panel = p;
   89                     panelSource = p.Source;
   90 
   91                     break;
   92                 }
   93             }
   94 
   95             if (panel == null)
   96             {
   97                 panelSource = new RibbonPanelSource();
   98                 panelSource.Title = PANEL_TITLE;
   99 
  100                 AddItemsToPanel(panelSource);
  101 
  102                 panel = new RibbonPanel();
  103                 panel.Source = panelSource;
  104 
  105                 tab.Panels.Add(panel);
  106             }
  107         }
  108 
  109         private static void AddItemsToPanel(RibbonPanelSource source)
  110         {
  111             RibbonButton btn = new RibbonButton();
  112             btn.Text = "Test dialog box";
  113 
  114             RibbonToolTip toolTip = new RibbonToolTip();
  115             toolTip.Title = "Test my dialog box";
  116             toolTip.Content = "Test modal dialog box' visibility change";
  117             toolTip.Command = "TestMyDialog";
  118             btn.ToolTip = toolTip;
  119             btn.CommandHandler = new MyRibbonCommandHandler();
  120             btn.CommandParameter = "._TESTMYDIALOG";
  121 
  122             source.Items.Add(btn);
  123         }
  124 
  125         #endregion
  126     }
  127 
  128     public class MyRibbonCommandHandler : System.Windows.Input.ICommand
  129     {
  130         public bool CanExecute(object parameter)
  131         {
  132             return true;
  133         }
  134 
  135         public event EventHandler CanExecuteChanged;
  136 
  137         public void Execute(object parameter)
  138         {
  139             RibbonCommandItem ribbonItem = parameter as RibbonCommandItem;
  140             if (ribbonItem != null)
  141             {
  142                 Document dwg =
  143                     Application.DocumentManager.MdiActiveDocument;
  144 
  145                 string cmdString =
  146                     ((string)ribbonItem.CommandParameter).TrimEnd();
  147                 if (!cmdString.EndsWith(";"))
  148                 {
  149                     cmdString = cmdString + " ";
  150                 }
  151 
  152                 dwg.SendStringToExecute(cmdString, true, false, true);
  153             }
  154         }
  155     }
  156 }

Class PickUtil, used for moving AutoCAD interaction (picking entities) away from dialog box' code behind:

    1 using Autodesk.AutoCAD.ApplicationServices;
    2 using Autodesk.AutoCAD.DatabaseServices;
    3 using Autodesk.AutoCAD.EditorInput;
    4 
    5 namespace RibbonKillsDialog
    6 {
    7     public class PickUtil
    8     {
    9         public static ObjectId PickEntity(
   10             System.Windows.Forms.Control modalDialog, string selectMessage)
   11         {
   12             Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
   13 
   14             ObjectId picked = ObjectId.Null;
   15 
   16             using (EditorUserInteraction inter =
   17                 ed.StartUserInteraction(modalDialog))
   18             {
   19                 PromptEntityOptions opt =
   20                     new PromptEntityOptions("\n" + selectMessage);
   21                 PromptEntityResult res = ed.GetEntity(opt);
   22                 if (res.Status == PromptStatus.OK)
   23                 {
   24                     picked = res.ObjectId;
   25                 }
   26             }
   27 
   28             return picked;
   29         }
   30 
   31         public static ObjectId PickEntity(string selectMessage)
   32         {
   33             Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
   34 
   35             ObjectId picked = ObjectId.Null;
   36 
   37             PromptEntityOptions opt =
   38                 new PromptEntityOptions("\n" + selectMessage);
   39             PromptEntityResult res = ed.GetEntity(opt);
   40             if (res.Status == PromptStatus.OK)
   41             {
   42                 picked = res.ObjectId;
   43             }
   44 
   45             return picked;
   46         }
   47     }
   48 }

Here is the code behind a dialog box' form (the form's design can be seen from the video clip):

    1 using System;
    2 using System.Windows.Forms;
    3 
    4 using Autodesk.AutoCAD.DatabaseServices;
    5 
    6 namespace RibbonKillsDialog
    7 {
    8     public partial class MyDialogBox : Form
    9     {
   10         public MyDialogBox()
   11         {
   12             InitializeComponent();
   13         }
   14 
   15         public string PickedId
   16         {
   17             get { return txtId.Text; }
   18         }
   19 
   20         private void ShowResult(ObjectId pickedId)
   21         {
   22             if (pickedId == ObjectId.Null)
   23                 txtId.Text = "";
   24             else
   25                 txtId.Text = pickedId.ToString();
   26         }
   27 
   28         private void btnPick1_Click(object sender, EventArgs e)
   29         {
   30             ObjectId id = PickUtil.PickEntity(this, "Select an entity:");
   31 
   32             ShowResult(id);
   33         }
   34 
   35         private void btnPick2_Click(object sender, EventArgs e)
   36         {
   37             this.Visible = false;
   38 
   39             ObjectId id = PickUtil.PickEntity("Select an entity:");
   40 
   41             ShowResult(id);
   42 
   43             this.Visible = true;
   44         }
   45 
   46         private void txtId_TextChanged(object sender, EventArgs e)
   47         {
   48             btnClose.Enabled = txtId.Text.Trim().Length > 0;
   49         }
   50 
   51         private void btnClose_Click(object sender, EventArgs e)
   52         {
   53             this.DialogResult = DialogResult.OK;
   54         }
   55     }
   56 }

Now, this is the command methods that create custom ribbon programmatically and does the work that shows dialog box:

    1 using Autodesk.AutoCAD.ApplicationServices;
    2 using Autodesk.AutoCAD.EditorInput;
    3 using Autodesk.AutoCAD.Runtime;
    4 
    5 [assembly: CommandClass(typeof(RibbonKillsDialog.MyCommands))]
    6 
    7 namespace RibbonKillsDialog
    8 {
    9     public class MyCommands
   10     {
   11         [CommandMethod("MyRibbonOn")]
   12         public static void TurnOnMyRibbonTab()
   13         {
   14             MyRibbon.AddMyRibbon();
   15         }
   16 
   17         [CommandMethod("MyRibbonOff")]
   18         public static void TurnOffMyRibbonTab()
   19         {
   20             MyRibbon.RemoveMyRibbon();
   21         }
   22 
   23         [CommandMethod("TestMyDialog")]
   24         public static void ShowMyDialog()
   25         {
   26             Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
   27 
   28             string id = "";
   29 
   30             try
   31             {
   32                 using (MyDialogBox dlg = new MyDialogBox())
   33                 {
   34                     System.Windows.Forms.DialogResult res =
   35                         Application.ShowModalDialog(dlg);
   36                     if (res == System.Windows.Forms.DialogResult.OK)
   37                     {
   38                         id = dlg.PickedId;
   39                     }
   40                 }
   41             }
   42             catch
   43             {
   44                 ed.WriteMessage("\nSomething was wrong...");
   45             }
   46             finally
   47             {
   48                 ed.WriteMessage("\nPicked entity: {0}",
   49                 string.IsNullOrEmpty(id) ? "None" : id);
   50             }
   51 
   52             Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
   53         }
   54     }
   55 }

In this video clip, as it shows, I first ran the "TestMyDialog" command from command line, as I usually do while programming AutoCAD. It worked as expected, of course. Then I made sure the ribbon works by checking "Fiberworld" was set to 1. Then I ran command "MyRibbonOn" to programmatically create my custom ribbon, and then start command "TestMyDialog" from normally displayed/expanded ribbon. The command also worked. After I minimized AutoCAD ribbon and then start "TestMyDialog" command from the minimized ribbon, the dialog box disappeared after the picking is completed. AutoCAD command line also does not show the messages by the 2 lines of code ed.WriteMessage("\n...) in the "catch..." and "finally..." block.

However, the command "TestMyDialog" would always execute correctly if from ribbon item that is create by CUIx.

Noticed that in the form's 2 "Pick >" buttons, my tried different ways to hide and show the dialog form: using EditorUserInteraction object and calling Form.Hide()/Form.Visible=False/True, just to prove that the issue discussed here is not affected by this difference of how form is hidden/shown.

This very issue exists with 2 AutoCAD versions that I have access to currently: AutoCAD 2012/2014. So, I can fairly be sure that it is the same with AutoCAD 2013.

In searching a solution for this issue, as an AutoCAD product license subscription custom, I requested a technical support from Autodesk and got a rather quick respond that provided a solution (thanks to Autodesk technical support team). It turned out the cure to this is fairly simple: setting focus back to AutoCAD editor after user prior to the dialog box being shown. That is, with AutoCAD 2013 or older, we need to call

Autodesk.AutoCAD.Internal.Util.SetFocusToDwgView();

and with AutoCAD 2014, we need to call

Autodesk.AutoCAD.ApplicationServices.Application.MainWindow.Focus();

The code change in the command class is like this (the change is in red):

    1 using Autodesk.AutoCAD.ApplicationServices;
    2 using Autodesk.AutoCAD.EditorInput;
    3 using Autodesk.AutoCAD.Runtime;
    4 
    5 [assembly: CommandClass(typeof(RibbonKillsDialog.MyCommands))]
    6 
    7 namespace RibbonKillsDialog
    8 {
    9     public class MyCommands
   10     {
   11         [CommandMethod("MyRibbonOn")]
   12         public static void TurnOnMyRibbonTab()
   13         {
   14             MyRibbon.AddMyRibbon();
   15         }
   16 
   17         [CommandMethod("MyRibbonOff")]
   18         public static void TurnOffMyRibbonTab()
   19         {
   20             MyRibbon.RemoveMyRibbon();
   21         }
   22 
   23         [CommandMethod("TestMyDialog")]
   24         public static void ShowMyDialog()
   25         {
   26             Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
   27 
   28             string id = "";
   29 
   30             Autodesk.AutoCAD.Internal.Utils.SetFocusToDwgView();
   31 
   32             try
   33             {
   34                 using (MyDialogBox dlg = new MyDialogBox())
   35                 {
   36                     System.Windows.Forms.DialogResult res =
   37                         Application.ShowModalDialog(dlg);
   38                     if (res == System.Windows.Forms.DialogResult.OK)
   39                     {
   40                         id = dlg.PickedId;
   41                     }
   42                 }
   43             }
   44             catch
   45             {
   46                 ed.WriteMessage("\nSomething was wrong...");
   47             }
   48             finally
   49             {
   50                 ed.WriteMessage("\nPicked entity: {0}",
   51                 string.IsNullOrEmpty(id) ? "None" : id);
   52             }
   53 
   54             Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
   55         }
   56     }
   57 }

Since custom ribbons created programmatically via Ribbon Runtime API are part of the AutoCAD customization in my office, the issue discussed here has to be dealt with. Though the solution is simple and easy enough, I have to be prepared that when I create a command method that may involve user interaction between dialog box and AutoCAD editor, I need to anticipate that the command methods may be executed from runtime-generated and minimized ribbon. That might mean that to make it safe, I'd better ALWAYS call

Autodesk.AutoCAD.Internal.Util.SetFocusToDwgView();

or

Autodesk.AutoCAD.ApplicationServices.Application.MainWindow.Focus();

in the command method before doing anything else, so that if this command method somehow ends up being used in programmatically generated ribbon, I do not have to going back to apply the workaround discussed here in future.

With AutoCAD 2015 being coming out in just a few months, I am hoping this issue will go away, but not holding my breath, now that I know the workaround.




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.