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.

1 comment:

  1. In fact, the reason of this behavior is simple: when you zoom or pan your drawing, some system variables like VIEWSIZE are changed. When you abort the transaction, they are restored to their initial state and the view comes back where it was when the command was started.

    ReplyDelete