Thursday, February 28, 2013

Using Application.ShowModalDialog()? Be Careful

When it comes to display a custom dialog box we developed in AutoCAD (assuming we use System.Windows.Forms.Form), we all know that Autodesk wants you use Application.ShowModalDialog/ShowModelessDialog() instead of Form.ShowDialog()/Show().

The documentation from ObjectARX SDK only cited that using Form.Show()/ShowDialog() can cause AutoCAD behaving unexpectedly. It also points out one of the BENEFIT of using Application.ShowModalDialog()/ShowModelessDialog() is that AutoCAD remembers the dialog box' location and size when it is closed.

Well, it may be a benefit in an AutoCAD work environment where nothing changes once a user is assigned to use that computer. For me, this benefit has been minor or big disasters quite a few times!

Here are 2 typical cases that made me not very happy.

Case One.

In one of my project, when I I doing my final test run before a demo, since I use dual-monitor computer (who still doesn't?) I left AutoCAD showing in one screen and the dialog box in the other when the dialog box is closed.

Next day for the demo, I connected my computer to the projector. Naturally, it only has one screen played on the wall. When demo started, I was very confidently start my application. Why shouldn't I? I just test it many times. The application is supposed to pop up a dialog box. To my surprise, as soon as I entered my command, nothing happens and AutoCAD does not responding. OK, somehow the damn Widows freezes AutoCAD for some reason. It happens, right? So, I went to Task Manager to kill the AutoCAD process and tried it again. The same thing. One can imagine how embarrassing it was with coworkers in whole office sitting in meeting room for your demo and you cannot get the damn thing run. It was a bit fortunate I had something else to show that day, so that I did not have to quit the demo with nothing being talked.

Of course, it was the unexpected surprise hit my mind too hard that I did not realize the AutoCAD appearing to be frozen was because the dialog box was shown off the screen (e.g. in a non-existent second screen, thanks to AutoCAD so diligently remembered the dialog box' previous location and restored it there, even there was no extra screen). Since it is modal dialog box, thus AutoCAD shown on the main screen (the only screen at the time) appeared frozen.

Many of the uses of my applications uses a laptop. They may run AutoCAD (and may applications) with the laptop only, or with the laptop docked at their desk with multiple monitors. When using multiple monitor, user tend to move dialog box to one screen and AutoCAD main window in another. So, user often runs into this situation and think my application somehow crashes AutoCAD and seek help from me.

The immediate cure for this issue, if you do know it is because of a dialog box showing off screen, is to press "ALT+SPACE" and then press "M" key, following by pressing arrow key to move the invisible dialog box, or by holding mouse left key down and moving it around, until the dialog box can be seen on the screen.

Case Two.

I was in a project development. There was a dialog box. After a debugging run, AutoCAD dutifully remembered the dialog box' location and its size. However, at some point in the development, I decided to change the form's size to add more controls. So, I rearranged the form's layout (making it larger than previously. Now I run the application again, AutoCAD insisted to show the form in its previous size, thus part of the form was cut of. Since the form's border was set "FixedSingle" and/or "FixedDialog", not "Sizeable", I could not resize the form and close with it being in correct size. therefore AutoCAD would keep showing the form partially. See picture below.

AutoCAD shows this:


While the form should be:


I figured it is not uncommon as CAD application developer that you design a form, do some debugging run and then you change the size of the same form. In this case, I really do not need AutoCAD remember the form's size. Well, I could rename the form, I can fool AutoCAD to think it is a new form, so that AutoCAD would not set its size to previously remembered size. But it is a bit silly each time you change a form's size, you need to rename it.

So, I tried change the form's border type to "Sizeable", so in next debugging run, I can resize the form to proper size and hope AutoCAD remembers it. Then I change the form's border type back to "FixedSingle/Fixed3d/FixedDialog" again and run the project, in hoping now AutoCAD should use the correct form size. To my surprise, AutoCAD still uses the size when the form was in the fixed border type.

OK, short of renaming my form (I really do not want to), the only way I can get my form shown correctly, is to set the form's size in Form.Shown event handler with code. Note, the code that set form's size only works when Form's Shown event fires and thereafter. If you place the code in the form's constructor or in the Form.Load event handler, AutoCAD still force its remembered size/location. Obviously, Autodesk does the form location/size restoration after the form is loaded but right before it is shown.

So, what I have learned from this Application.ShowModalDialog() issue?

1. It is a bug for letting a dialog box be restored in a off-screen location. When AutoCAD tries to restore the form's previous location, it should check if the remembered location is off all available screens, if yes, it should show the dialog box based on the form's "StartPosition" property in the primary screen.

2. If we want to override AutoCAD and set the form's location/size with our code, the code should be in Form.Shown event handler. But I do wish the Application.ShowModalDialog/ShowModelessDialog() had a overload method where we can set flag to ask AutoCAD not to restore previous location/size.

3. I tried to use code in the Form.Shown event handler to get my form shown correctly and then close the form in the hope that AutoCAD now remembers the correct form size. At this point I removed the code in Form.Shown event handler, hoping now AutoCAD would restore the form in correct size. I ran the application again. AutoCAD still shows the form in its originally remembered size again, Apparently, for each form, if the form's border is fixed type, AutoCAD only remember its size the first time when the form shows (but it does remember form location each time the form is closed). So I have to keep the size setting code in Form.Shown events, unless I rename the form, or clean up my Windows account profile, both I reluctant to do.

Sunday, February 17, 2013

Restoring a View with Aborted Transaction

In AutoCAD programming (or in general programming, for that matter), one of the best practices is that if your code changes some system status (such as system variables) just for your specific purpose, you should restore it back to its original status, if possible.

One of those things I had done many times before (quite long before, since I moved to use AutoCAD .NET API, when I worked with AutoLISP and VBA) was to create a temporary view before my code let user to do something in AutoCAD editor, usually it was to let user to zoom/pan the view or zoom/pan while selecting entities, and after that the code restore the view back and delete the temporary view. I was in this situation many times, so that I had LISP/VBA functions (to save a view and delete a view) in my tool box for reuse.

I also remembered it was emphasized in some popular AutoCAD user guide book (such as 'Master AutoCAD xxxx') that zooming it/out or panning too much that causes/requires 'regen' could be very slow/time consuming if the drawing size is big. Well, it was true considering the time when I was doing some drafting work with the computer of DOS/Windows 3.x/Windows 9x and 4MB/8MB memory. These book recommended to create view and save it before doing lots of zooming/panning, so that one can go back to the original view easily and quickly. I have hardly read any AutoCAD user guide book for many years since I stop doing drafting work, I am not sure if this is still recommended operation trick, but I'd not be surprised if it still is.

Anyway, back to the topic. I still find I am in the situation that I'd like to let the AutoCAD's current view look the same after running my code for something (mostly, it is to let user look at the entities in AutoCAD editor by hiding my modal dialog, or let user pick some entities, during which user need to zoom int/out or pan the view). So, my have reusable routines in my generic AutoCAD .NET tool box that does saving/restoring/deleting view (ViewTableRecord).

A question recently posted in Autodesk discussion group's .NET forum caught my attention: after the code asking user to pick entities, which results in zooming/panning, the view always automatically restored back to the status before the picking process started. It turned out it is due to the uncommitted (or aborted) Transaction that wrapped around the picking code.

As I admitted in the reply to that question, this is an interesting secrete of Transaction I had not noticed before: one starts a Transaction in order to get into individual entity for more information and somehow the Transaction grab current view information with it for some purpose behind the scene.

Without bothering to fully understand why, I thought at least I can use this to conveniently replace my "tedious" code of saving current view, restoring saved view and then deleting the saved view, and simply use an aborted Transaction to get the view back to it was after my code leads user zooming/panning in AutoCAD editor, assuming during the transaction, I only need to get read-only information from drawing database/entities (OpenMode.ForRead).

Here is the code that shows the situation: I want to user to pick some entities in the editor, but I'd like keep the view the same as before after picking is done. I used tow picking functions provided by Editor class: GetSelection() and GetEntity().

    1 using Autodesk.AutoCAD.ApplicationServices;
    2 using Autodesk.AutoCAD.DatabaseServices;
    3 using Autodesk.AutoCAD.EditorInput;
    4 using Autodesk.AutoCAD.Geometry;
    5 using Autodesk.AutoCAD.Runtime;
    6 using System;
    7 using System.Collections.Generic;
    8 using System.Linq;
    9 using System.Text;
   10 
   11 namespace RestoreView
   12 {
   13     public static class AcadUtils
   14     {
   15         public static ObjectId[] SelectObjectsOnScreen(Document dwg)
   16         {
   17             List<ObjectId> selected = new List<ObjectId>();
   18 
   19             using (Transaction tran = dwg.TransactionManager.StartTransaction())
   20             {
   21                 ObjectId[] objIds = UseGetSelection(dwg.Editor);
   22                 selected.AddRange(objIds);
   23                 tran.Abort();
   24             }
   25 
   26             dwg.Editor.GetString("\nPress any key to continue <Enter>");
   27 
   28             using (Transaction tran = dwg.TransactionManager.StartTransaction())
   29             {
   30                 ObjectId[] objIds = UseGetEntity(dwg.Editor);
   31                 selected.AddRange(objIds);
   32                 tran.Abort();
   33             }
   34 
   35             return selected.ToArray();
   36         }
   37 
   38         private static ObjectId[] UseGetSelection(Editor ed)
   39         {
   40             PromptSelectionResult res = ed.GetSelection();
   41             if (res.Status == PromptStatus.OK)
   42             {
   43                 return res.Value.GetObjectIds();
   44             }
   45             else
   46             {
   47                 return new ObjectId[0];
   48             }
   49         }
   50 
   51         private static ObjectId[] UseGetEntity(Editor ed)
   52         {
   53             List<ObjectId> lst = new List<ObjectId>();
   54 
   55             PromptEntityOptions opt =
   56                 new PromptEntityOptions("\nSelect a LINE or a CIRCLE:");
   57             opt.SetRejectMessage("\nInvalid pick: must be either LINE or CIRCLE");
   58             opt.AddAllowedClass(typeof(Line), true);
   59             opt.AddAllowedClass(typeof(Circle), true);
   60 
   61             while (true)
   62             {
   63                 PromptEntityResult res = ed.GetEntity(opt);
   64                 if (res.Status != PromptStatus.OK) break;
   65 
   66                 if (lst.Contains(res.ObjectId))
   67                 {
   68                     ed.WriteMessage("\nDuplicate pick.");
   69                 }
   70                 else
   71                 {
   72                     Entity ent = (Entity)res.ObjectId.GetObject(OpenMode.ForWrite);
   73                     ent.Highlight();
   74 
   75                     lst.Add(res.ObjectId);
   76                     ed.WriteMessage("\n{0} object{1} selected.",
   77                         lst.Count, lst.Count > 1 ? "s" : "");
   78                 }
   79             }
   80 
   81             return lst.ToArray();
   82         }
   83     }
   84 }

Here is command class to run the code

    1 using Autodesk.AutoCAD.ApplicationServices;
    2 using Autodesk.AutoCAD.DatabaseServices;
    3 using Autodesk.AutoCAD.Runtime;
    4 
    5 [assembly: CommandClass(typeof(RestoreView.MyCommands))]
    6 
    7 namespace RestoreView
    8 {
    9     public class MyCommands
   10     {
   11         [CommandMethod("MySelect")]
   12         public static void RunMyCommand()
   13         {
   14             Document dwg = Application.DocumentManager.MdiActiveDocument;
   15 
   16             ObjectId[] ids = AcadUtils.SelectObjectsOnScreen(dwg);
   17 
   18             dwg.Editor.WriteMessage("\nSelected objects: {0}", ids.Length);
   19 
   20             Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
   21         }
   22     }
   23 }

Running this code, in terms of restoring view to the status before picking is started, it is to my satisfactory: with only one extra line code (using (Transaction tran = ....){} (Transaction.Abort() can be omitted, but I like being explicit on this: committed, or aborted), I achieve the goal to keep the view unchanged after executing some code that very likely make the view changes. It is a lot easier than the way I used to do: saving current view, and later restore it back and then delete the saved view.

Oh, as side effect, I also noticed another thing I have never noticed before: Entity.Highlight() can be called with entity that is opened for read-only. In .NET API code practice so far, whenever I need to highlight an entity, I just took it for grant that the entity is opened for write. It looks like Entity.Highlight() does not involve the current Transaction.

Another issue you would notice by running this code: when selecting with Editor.GetEntity(), I call Entity.Highlight() on each picked entity to give user visual clue of which entity has been already selected. However, if the Transaction is done (aborted, in this case), although the view is restored back it what it was before, the entities highlighted by the code remain highlighted. This proves my guess just in above paragraph.

Depending on what you would do with the selected entities by code shown here, you can decide if you want the entities remaining highlighted or not. In my code sample here, I decide that I do not need them remaining highlighted, so I modified code to unhighlight the entities before the code returns. Here is the modified code:

    1 using Autodesk.AutoCAD.ApplicationServices;
    2 using Autodesk.AutoCAD.DatabaseServices;
    3 using Autodesk.AutoCAD.EditorInput;
    4 using Autodesk.AutoCAD.Geometry;
    5 using Autodesk.AutoCAD.Runtime;
    6 using System;
    7 using System.Collections.Generic;
    8 using System.Linq;
    9 using System.Text;
   10 
   11 namespace RestoreView
   12 {
   13     public static class AcadUtils
   14     {
   15         public static ObjectId[] SelectObjectsOnScreen(Document dwg)
   16         {
   17             List<ObjectId> selected = new List<ObjectId>();
   18 
   19             using (Transaction tran = dwg.TransactionManager.StartTransaction())
   20             {
   21                 ObjectId[] objIds = UseGetSelection(dwg.Editor);
   22                 selected.AddRange(objIds);
   23                 tran.Abort();
   24             }
   25 
   26             dwg.Editor.GetString("\nPress any key to continue <Enter>");
   27 
   28             using (Transaction tran = dwg.TransactionManager.StartTransaction())
   29             {
   30                 ObjectId[] objIds = UseGetEntity(dwg.Editor);
   31                 selected.AddRange(objIds);
   32                 tran.Abort();
   33             }
   34 
   35             return selected.ToArray();
   36         }
   37 
   38         private static ObjectId[] UseGetSelection(Editor ed)
   39         {
   40             PromptSelectionResult res = ed.GetSelection();
   41             if (res.Status == PromptStatus.OK)
   42             {
   43                 return res.Value.GetObjectIds();
   44             }
   45             else
   46             {
   47                 return new ObjectId[0];
   48             }
   49         }
   50 
   51         private static ObjectId[] UseGetEntity(Editor ed)
   52         {
   53             List<ObjectId> lst = new List<ObjectId>();
   54 
   55             PromptEntityOptions opt =
   56                 new PromptEntityOptions("\nSelect a LINE or a CIRCLE:");
   57             opt.SetRejectMessage("\nInvalid pick: must be either LINE or CIRCLE");
   58             opt.AddAllowedClass(typeof(Line), true);
   59             opt.AddAllowedClass(typeof(Circle), true);
   60 
   61             while (true)
   62             {
   63                 PromptEntityResult res = ed.GetEntity(opt);
   64                 if (res.Status != PromptStatus.OK) break;
   65 
   66                 if (lst.Contains(res.ObjectId))
   67                 {
   68                     ed.WriteMessage("\nDuplicate pick.");
   69                 }
   70                 else
   71                 {
   72                     Entity ent = (Entity)res.ObjectId.GetObject(OpenMode.ForWrite);
   73                     ent.Highlight();
   74 
   75                     lst.Add(res.ObjectId);
   76                     ed.WriteMessage("\n{0} object{1} selected.",
   77                         lst.Count, lst.Count > 1 ? "s" : "");
   78                 }
   79             }
   80 
   81             Unhighlight(lst.ToArray());
   82 
   83             return lst.ToArray();
   84         }
   85 
   86         private static void Unhighlight(ObjectId[] ids)
   87         {
   88             foreach (ObjectId id in ids)
   89             {
   90                 Entity ent = (Entity)id.GetObject(OpenMode.ForRead);
   91                 ent.Unhighlight();
   92             }
   93         }
   94     }
   95 }

I do not know if there would be some implications by using an aborted transaction to restore a view back. But in the situation I tend to use this approach (restoring a view back after asking user to pick some entities in most cases), it seems a simple and harmless way to do it.