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.