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.




Wednesday, November 20, 2013

A Block with Auto-Pointing Leader

In drafting process, often user needs to create something like a label with a leader and points it to something in the drawing. The picture below is an example.




Also more often than not, depending on how crowded the drawing content is, user needs to move the label around. It would be better that when the label (be it consists of separate entities, or a block) moves, its pointing leader's tip can remain where it was, so that user would not have to move the label and then re-points the leader.

When I ran into a similar requirement from one of my AutoCAD programming project, the first idea jump out from my head is Overrule. For example, the label can be just a block without a leader line. Then I could use DrawableOverrule to draw a leader as the visual effect.

The problem with this approach is that the Overrule code must be loaded and enabled all the time. Or the label would not appear to have a leader pointing the point in interest.

In my real project, we settled with using a dynamic block for the label, which has a stretchable leader. Following is an example of a similar block:

Here is the video clip that shows how its leader can be stretched freely. The advantage of using this kind of block is that even if the code to auto-points the leader is not loaded and/does not works, the user can always manually drag the leader to make it point to where it is supposed to.

With this block, I figured, as long as I know where the point of interest, to which the leader points, I can always set the dynamic property's value (the X/Y value of the point parameter, named as "LeaderPoint" in this particular block).

So, the logic of setting the point parameter value is really simple: When the label block is inserted, the point of interest is saved with this block as XData, and then whenever the label block is changed (moved, stretched, rotated, scaled), the point parameter's value is recalculated and reset, thus the leader will remain pointing to the point of interest.

I went ahead trying to implement a TransformOverrule, thinking it would be fairly easy to do with just creating the overridden TransformBy() virtual method.

However, it turned out that the TransformOverrule was not the solution for 2 reasons:

1. The TransformBy() method seems not being triggered when user moves the block by select the block and drag the block by its grips;

2. AutoCAD crashes when the code is trying to set block reference's dynamic property value in the TransformBy() method. The crash is not not catch-able in my "try...catch..." code block, and if the code does not try to set the dynamic property value, then no crash. I just could not get over with this crashing issue and tend to think it is something inside the API, be it a bug or not.

The other drawback with using Overrule is, as I stated previously, the Overrule code has to be loaded all the time.

Therefore I gave up the Overrule idea and turned to handling events, like CommandWillStart, CommandEnded, and ObjectModified. The idea is that during the period from CommandWillStart to CommandEnded, the code will watch ObjectModified event if the command results in changes to the specific block references. The changed block reference's ObjectId is saved in a collection. After CommandEnded event, if the collection has ObjectId in it, extra code is executed to reset the block reference's dynamic property value (the point parameter that stretches the leader).

Here are the 2 helper classes going first:

Class XDataHelper, which embeds a Point3d value in the label block and read out the embedded Point3d value from the label block:

    1 using Autodesk.AutoCAD.DatabaseServices;
    2 using Autodesk.AutoCAD.Geometry;
    3 
    4 namespace BlockWithAutoPointingLeader
    5 {
    6     public static class XDataHelper
    7     {
    8         public static void SetPointToEntityXData(
    9             ObjectId entId, string appName, Point3d point)
   10         {
   11             RegisterXDataApp(appName, entId.Database);
   12 
   13             Database db = HostApplicationServices.WorkingDatabase;
   14 
   15             using (Transaction tran =
   16                 db.TransactionManager.StartOpenCloseTransaction())
   17             {
   18                 Entity ent = (Entity)tran.GetObject(entId, OpenMode.ForWrite);
   19 
   20                 ResultBuffer buffer = new ResultBuffer(
   21                             new TypedValue(
   22                                 (int)DxfCode.ExtendedDataRegAppName, appName),
   23                             new TypedValue(
   24                                 (int)DxfCode.ExtendedDataXCoordinate, point)
   25                             );
   26 
   27                 ent.XData = buffer;
   28 
   29                 tran.Commit();
   30             }
   31         }
   32 
   33         public static Point3d GetPointFromEntityXData(
   34             ObjectId entId, string appName)
   35         {
   36             Point3d point = new Point3d(
   37                 double.MinValue, double.MinValue, double.MinValue);
   38 
   39             Database db = HostApplicationServices.WorkingDatabase;
   40 
   41             using (Transaction tran =
   42                 db.TransactionManager.StartOpenCloseTransaction())
   43             {
   44                 Entity ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
   45 
   46                 point = GetPointFromEntityXData(ent, appName);
   47 
   48                 tran.Commit();
   49             }
   50 
   51             return point;
   52         }
   53 
   54         public static Point3d GetPointFromEntityXData(
   55             Entity entity, string appName)
   56         {
   57             Point3d point = new Point3d(
   58                 double.MinValue, double.MinValue, double.MinValue);
   59 
   60             ResultBuffer buffer = entity.GetXDataForApplication(appName);
   61 
   62             if (buffer != null)
   63             {
   64                 foreach (TypedValue tv in buffer)
   65                 {
   66                     if (tv.TypeCode ==
   67                         (short)DxfCode.ExtendedDataXCoordinate)
   68                     {
   69                         point = (Point3d)tv.Value;
   70                     }
   71                 }
   72             }
   73 
   74             return point;
   75         }
   76 
   77         private static void RegisterXDataApp(string appName, Database db)
   78         {
   79             using (Transaction tran = db.TransactionManager.StartTransaction())
   80             {
   81                 RegAppTable tbl = (RegAppTable)tran.GetObject(
   82                     db.RegAppTableId, OpenMode.ForRead);
   83 
   84                 if (!tbl.Has(appName))
   85                 {
   86                     tbl.UpgradeOpen();
   87                     RegAppTableRecord app = new RegAppTableRecord();
   88                     app.Name = appName;
   89 
   90                     tbl.Add(app);
   91                     tran.AddNewlyCreatedDBObject(app, true);
   92                 }
   93 
   94                 tran.Commit();
   95             }
   96         }
   97     }
   98 }

Class LeaderPointingHelper, which rests point parameter's value in the dynamic block reference:

    1 using System;
    2 using Autodesk.AutoCAD.DatabaseServices;
    3 using Autodesk.AutoCAD.Geometry;
    4 
    5 namespace BlockWithAutoPointingLeader
    6 {
    7     static class LeaderPointingHelper
    8     {
    9         public static void SnapLeaderToPoint(
   10             ObjectId blkId, string xDataAppName, string leaderPropName)
   11         {
   12             Database db = HostApplicationServices.WorkingDatabase;
   13 
   14             using (Transaction tran =
   15                 db.TransactionManager.StartTransaction())
   16             {
   17                 BlockReference bref = (BlockReference)
   18                     tran.GetObject(blkId, OpenMode.ForWrite);
   19                 SnapLeaderToPoint(bref, xDataAppName, leaderPropName);
   20                 tran.Commit();
   21             }
   22         }
   23 
   24         public static void SnapLeaderToPoint(
   25             BlockReference bref, string xDataAppName, string leaderPropName)
   26         {
   27             //Get point embedded with the block reference
   28             Point3d point = XDataHelper.GetPointFromEntityXData(
   29                 bref, xDataAppName);
   30             if (point.X == double.MinValue &&
   31                 point.Y == double.MinValue &&
   32                 point.Z == double.MinValue) return;
   33 
   34             //Get the pointing location's coordinate in terms of
   35             //the block's insertion point (e.g. set the block's
   36             //insertion point as the origin of a UCS and get the
   37             //coordinate of the pointing location in this UCS
   38             Point3d coordPoint = GetUscCoordinate(
   39                 point, bref.Rotation, bref.Position);
   40 
   41             //Set block's dynamic property - the strechable leader
   42             foreach (DynamicBlockReferenceProperty prop in
   43                 bref.DynamicBlockReferencePropertyCollection)
   44             {
   45                 if (prop.PropertyName ==
   46                     leaderPropName + " X")
   47                 {
   48                     prop.Value = coordPoint.X;
   49                 }
   50 
   51                 if (prop.PropertyName ==
   52                     leaderPropName + " Y")
   53                 {
   54                     prop.Value = coordPoint.Y;
   55                 }
   56             }
   57         }
   58 
   59         #region private methods
   60 
   61         private static Point3d GetUscCoordinate(
   62             Point3d inPoint, double ucsAngle, Point3d ucsOrigin)
   63         {
   64             double x, y, z;
   65 
   66             x = Math.Cos(ucsAngle);
   67             y = Math.Sin(ucsAngle);
   68             z = 0.0;
   69 
   70             Vector3d xVec = new Vector3d(x, y, z);
   71 
   72             x = 0.0 - Math.Sin(ucsAngle);
   73             y = Math.Cos(ucsAngle);
   74             z = 0.0;
   75 
   76             Vector3d yVec = new Vector3d(x, y, z);
   77             CoordinateSystem3d ucs =
   78                 new CoordinateSystem3d(ucsOrigin, xVec, yVec);
   79 
   80             Matrix3d mt = Matrix3d.AlignCoordinateSystem(
   81                 Point3d.Origin, Vector3d.XAxis,
   82                 Vector3d.YAxis, Vector3d.ZAxis,
   83                 ucs.Origin, ucs.Xaxis,
   84                 ucs.Yaxis, ucs.Zaxis);
   85 
   86             Point3d p = inPoint.TransformBy(mt.Inverse());
   87 
   88             return p;
   89         }
   90 
   91         #endregion
   92     }
   93 }

Here is the code that does the real work: class AutoPointingHandler:

    1 using System.Collections.Generic;
    2 using Autodesk.AutoCAD.ApplicationServices;
    3 using Autodesk.AutoCAD.DatabaseServices;
    4 
    5 namespace BlockWithAutoPointingLeader
    6 {
    7     public class AutoPointingHandler
    8     {
    9         private string _xDataAppName = "";
   10         private string _leaderPropName = "";
   11         private string _blockName = "";
   12         private bool _enabled = false;
   13         private bool _updating = false;
   14         private bool _handlerAttached = false;
   15         private bool _isMove = false;
   16 
   17         private List<ObjectId> _targetBlockIds = null;
   18 
   19         private Document _dwg;
   20 
   21         public AutoPointingHandler(
   22             string xDataAppName, string blockName, string leaderPropName)
   23         {
   24             _dwg = Application.DocumentManager.MdiActiveDocument;
   25             _xDataAppName = xDataAppName;
   26             _blockName = blockName;
   27             _leaderPropName = leaderPropName;
   28         }
   29 
   30         public bool Enabled
   31         {
   32             get { return _enabled; }
   33         }
   34 
   35         public void EnableAutoPointing(bool enable)
   36         {
   37             if (enable)
   38             {
   39                 if (!_enabled)
   40                 {
   41                     _dwg.CommandWillStart += dwg_CommandWillStart;
   42                     _enabled = true;
   43                 }
   44             }
   45             else
   46             {
   47                 if (_enabled)
   48                 {
   49                     _dwg.CommandWillStart -= dwg_CommandWillStart;
   50                     _enabled = false;
   51                 }
   52             }
   53         }
   54 
   55         private void dwg_CommandWillStart(object sender, CommandEventArgs e)
   56         {
   57             if (!_enabled) return;
   58             if (_updating) return;
   59 
   60             string cmdName = e.GlobalCommandName.ToUpper();
   61             if (cmdName.Contains("GRIP_POPUP") ||
   62                 cmdName.Contains("GRIP_STRETCH") ||
   63                 cmdName.Contains("MOVE") ||
   64                 cmdName.Contains("ROTATE") ||
   65                 cmdName.Contains("SCALE") ||
   66                 cmdName.Contains("STRETCH"))
   67             {
   68                 try
   69                 {
   70                     _dwg.Database.ObjectModified -= database_ObjectModified;
   71                     _dwg.CommandEnded -= dwg_CommandEnded;
   72                 }
   73                 catch { }
   74 
   75                 _dwg.Database.ObjectModified += database_ObjectModified;
   76                 _dwg.CommandEnded += dwg_CommandEnded;
   77                 _handlerAttached = true;
   78                 _targetBlockIds = new List<ObjectId>();
   79             }
   80         }
   81 
   82         private void dwg_CommandEnded(object sender, CommandEventArgs e)
   83         {
   84             if (_handlerAttached)
   85             {
   86                 _dwg.Database.ObjectModified -= database_ObjectModified;
   87                 _dwg.CommandEnded -= dwg_CommandEnded;
   88             }
   89 
   90             if (!_enabled) return;
   91             if (_updating) return;
   92 
   93             string cmdName = e.GlobalCommandName.ToUpper();
   94 
   95             if (cmdName.Contains("GRIP_POPUP") ||
   96                 cmdName.Contains("GRIP_STRETCH") ||
   97                 cmdName.Contains("MOVE") ||
   98                 cmdName.Contains("ROTATE") ||
   99                 cmdName.Contains("SCALE") ||
  100                 cmdName.Contains("STRETCH"))
  101             {
  102                 if (_targetBlockIds.Count>0)
  103                 {
  104                     _isMove = !cmdName.Contains("GRIP_POPUP");
  105 
  106                     //Since station(s) block has/have been modified
  107                     //recalculate/reset its dynamic property "LeaderPoint"
  108                     _updating = true;
  109                     foreach (var id in _targetBlockIds)
  110                     {
  111                         LeaderPointingHelper.SnapLeaderToPoint(
  112                             id, _xDataAppName, _leaderPropName);
  113                     }             
  114                     _updating = false;
  115 
  116                     _dwg.Editor.UpdateScreen();
  117                     _targetBlockIds = null;
  118                 }
  119             }
  120         }
  121 
  122         private void database_ObjectModified(object sender, ObjectEventArgs e)
  123         {
  124             if (!_enabled) return;
  125             if (_updating) return;
  126 
  127             if (!(e.DBObject is BlockReference)) return;
  128             if (e.DBObject.ObjectId.IsErased ||
  129                 e.DBObject.ObjectId.IsEffectivelyErased) return;
  130             if (_targetBlockIds == null) return;
  131 
  132             //Determine if the changed block refernce is
  133             //the target blockreference
  134             BlockReference bref = e.DBObject as BlockReference;
  135             if (bref.IsDynamicBlock)
  136             {
  137                 string blkName = "";
  138                 using (Transaction tran =
  139                     _dwg.TransactionManager.StartOpenCloseTransaction())
  140                 {
  141                     BlockTableRecord br = (BlockTableRecord)tran.GetObject(
  142                             bref.DynamicBlockTableRecord, OpenMode.ForRead);
  143                     blkName = br.Name;
  144                 }
  145 
  146                 if (blkName.ToUpper() == _blockName.ToUpper())
  147                 {
  148                     if (!_targetBlockIds.Contains(e.DBObject.ObjectId))
  149                     {
  150                         _targetBlockIds.Add(e.DBObject.ObjectId);
  151                     }
  152                 }
  153             }
  154         } 
  155     }
  156 }
 
 
Below is the code that insert the label block and enable the AutoPointingHandler. Note the the command methods are NOT static, so the command class is instantiated for each document when the commands is executed. This way, the AutoPointingHandler instance only hooks to events of its own drawing document.

    1 using System;
    2 using Autodesk.AutoCAD.ApplicationServices;
    3 using Autodesk.AutoCAD.DatabaseServices;
    4 using Autodesk.AutoCAD.EditorInput;
    5 using Autodesk.AutoCAD.Geometry;
    6 using Autodesk.AutoCAD.Runtime;
    7 
    8 [assembly: CommandClass(typeof(BlockWithAutoPointingLeader.MyCommands))]
    9 
   10 namespace BlockWithAutoPointingLeader
   11 {
   12     public class MyCommands
   13     {
   14         private const string XDATA_APPNAME = "AutoPointingBlock";
   15         private const string BLOCK_LEADER_PROPNAME = "LeaderPoint";
   16         private const string BLOCK_NAME = "PartNumber";
   17         private AutoPointingHandler _autoPointer = null;
   18 
   19         [CommandMethod("InsMyBlk")]
   20         public void RunMyCommand()
   21         {
   22             Document dwg = Application.DocumentManager.MdiActiveDocument;
   23             Editor ed = dwg.Editor;
   24 
   25             try
   26             {
   27                 int blkInserted = InsertAutoPointingBlock(dwg);
   28                 if (blkInserted > 0)
   29                 {
   30                     ed.WriteMessage("\n{0} block{1} inserted.",
   31                         blkInserted, blkInserted > 1 ? "s" : "");
   32 
   33                     EnsureAutoPoiningHandler();
   34                     _autoPointer.EnableAutoPointing(true);
   35                 }
   36             }
   37             catch (System.Exception ex)
   38             {
   39                 ed.WriteMessage("\nError: {0}", ex.Message);
   40             }
   41 
   42             Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
   43         }
   44 
   45         [CommandMethod("EnableAutoPointing")]
   46         public void EnableAutoPointingHandler()
   47         {
   48             Document dwg = Application.DocumentManager.MdiActiveDocument;
   49             Editor ed = dwg.Editor;
   50 
   51             EnsureAutoPoiningHandler();
   52             bool enabled = _autoPointer.Enabled;
   53 
   54             PromptKeywordOptions opt = new PromptKeywordOptions(
   55                 "\nAutoPointinghandler is currently " +
   56                 (enabled ? "enabled" : "disabled."));
   57             if (enabled)
   58             {
   59                 opt.Keywords.Add("oFf");
   60                 opt.Keywords.Default = "oFf";
   61             }
   62             else
   63             {
   64                 opt.Keywords.Add("oN");
   65                 opt.Keywords.Default = "oN";
   66             }
   67             opt.AppendKeywordsToMessage = true;
   68 
   69             PromptResult res = ed.GetKeywords(opt);
   70             if (res.Status == PromptStatus.OK)
   71             {
   72                 if (res.StringResult.ToUpper() == "ON")
   73                 {
   74                     _autoPointer.EnableAutoPointing(true);
   75                     ed.WriteMessage("\nAutoPointingHandler is now enabled.");
   76                 }
   77                 else
   78                 {
   79                     _autoPointer.EnableAutoPointing(false);
   80                     ed.WriteMessage("\nAutoPointingHandler is now disabled.");
   81                 }
   82             }
   83             else
   84             {
   85                 ed.WriteMessage("\n*Cancel*");
   86             }
   87 
   88             Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
   89         }
   90 
   91         private void EnsureAutoPoiningHandler()
   92         {
   93             if (_autoPointer == null)
   94             {
   95                 _autoPointer = new AutoPointingHandler(
   96                     XDATA_APPNAME, BLOCK_NAME, BLOCK_LEADER_PROPNAME);
   97             }
   98         }
   99 
  100         private int InsertAutoPointingBlock(Document dwg)
  101         {
  102             string blkLayer = "Layer1";
  103             int count = 0;
  104             while (true)
  105             {
  106                 Point3d lblPt;
  107                 Point3d blkPt;
  108                 if (PickLabelPoint(dwg.Editor,
  109                     "Pick point to be labelled:", out lblPt))
  110                 {
  111                     if (PickBlockPoint(dwg.Editor,
  112                         "Pick point for the label block", lblPt, out blkPt))
  113                     {
  114                         count++;
  115                         //Insert block
  116                         ObjectId brefId = InsertBlock(
  117                             dwg.Database, BLOCK_NAME, blkLayer, blkPt, count);
  118 
  119                         //Attach XData to the block refernce
  120                         XDataHelper.SetPointToEntityXData(
  121                             brefId, XDATA_APPNAME, lblPt);
  122 
  123                         //Snap block leader to point
  124                         LeaderPointingHelper.SnapLeaderToPoint(
  125                             brefId, XDATA_APPNAME, BLOCK_LEADER_PROPNAME);
  126 
  127                         dwg.Editor.UpdateScreen();
  128                     }
  129                     else
  130                     {
  131                         break;
  132                     }
  133                 }
  134                 else
  135                 {
  136                     break;
  137                 }
  138             }
  139 
  140             return count;
  141         }
  142 
  143         private bool PickLabelPoint(
  144             Editor ed, string msg, out Point3d point)
  145         {
  146             point = new Point3d();
  147             PromptPointOptions opt = new PromptPointOptions("\n" + msg);
  148             PromptPointResult res = ed.GetPoint(opt);
  149             if (res.Status == PromptStatus.OK)
  150             {
  151                 point = res.Value;
  152                 return true;
  153             }
  154             else
  155             {
  156                 return false;
  157             }
  158         }
  159 
  160         private bool PickBlockPoint(
  161             Editor ed, string msg, Point3d basePoint, out Point3d point)
  162         {
  163             point = new Point3d();
  164             PromptPointOptions opt = new PromptPointOptions("\n" + msg);
  165             opt.UseBasePoint = true;
  166             opt.BasePoint = basePoint;
  167             opt.UseDashedLine = true;
  168             PromptPointResult res = ed.GetPoint(opt);
  169             if (res.Status == PromptStatus.OK)
  170             {
  171                 point = res.Value;
  172                 return true;
  173             }
  174             else
  175             {
  176                 return false;
  177             }
  178         }
  179 
  180         private ObjectId InsertBlock(
  181             Database db, string blkName,
  182             string layerName, Point3d insPoint, int count)
  183         {
  184             ObjectId brefId = ObjectId.Null;
  185 
  186             using (Transaction tran =
  187                 db.TransactionManager.StartTransaction())
  188             {
  189                 BlockTable bt = (BlockTable)tran.GetObject(
  190                     db.BlockTableId, OpenMode.ForRead);
  191                 if (bt.Has(blkName))
  192                 {
  193                     BlockTableRecord bdef = (BlockTableRecord)
  194                         tran.GetObject(bt[blkName], OpenMode.ForRead);
  195 
  196                     BlockTableRecord model = (BlockTableRecord)tran.GetObject(
  197                         SymbolUtilityServices.GetBlockModelSpaceId(db),
  198                         OpenMode.ForWrite);
  199 
  200                     //Insert block
  201                     BlockReference bref = new BlockReference(
  202                         insPoint, bdef.ObjectId);
  203                     bref.SetDatabaseDefaults(db);
  204                     bref.Layer = layerName;
  205 
  206                     brefId = model.AppendEntity(bref);
  207                     tran.AddNewlyCreatedDBObject(bref, true);
  208 
  209                     //Add attribute
  210                     foreach (ObjectId id in bdef)
  211                     {
  212                         AttributeDefinition adef = tran.GetObject(
  213                             id, OpenMode.ForRead) as AttributeDefinition;
  214                         if (adef != null)
  215                         {
  216                             AttributeReference aref =
  217                                 new AttributeReference();
  218                             aref.SetAttributeFromBlock(
  219                                 adef, bref.BlockTransform);
  220 
  221                             if (adef.Tag.ToUpper() == "NO")
  222                             {
  223                                 aref.TextString =
  224                                     count.ToString().PadLeft(3, '0');
  225                             }
  226                             else
  227                             {
  228                                 aref.TextString = " ";
  229                             }
  230 
  231                             bref.AttributeCollection.AppendAttribute(aref);
  232                             tran.AddNewlyCreatedDBObject(aref, true);
  233                         }
  234                     }
  235                 }
  236                 else
  237                 {
  238                     throw new InvalidOperationException("Block \"" +
  239                     blkName + "\" not defined.");
  240                 }
  241 
  242                 tran.Commit();
  243             }
  244 
  245             return brefId;
  246         }
  247     }
  248 }

Watch this video clip to see the code in action.

A few things to note:

1. During the select-first operation of moving/rotating/scaling, the grip for dragging the leader's end point is not updated until the label block is deselected. I tried in code with Editor.Regen() without success. I could have try to remove it from the implied SelectionSet, but figured it is not a big deal.

2. While the AutoPointingHandler is enabled, when user drags the dynamic block's leader to point it to somewhere else, the leader will always go back. This could be good thing or bad thing. But in my case, this is by design, a good thing.

3. The key factor for this to work is to embed the point of interest with the block reference. If the point is changed, the block has to be re-inserted in order to have the changed point to be embedded with the block as XData.

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.