Sunday, August 18, 2013

A Simple Undoable Custom Command

When doing AutoCAD programming, you might very likely come across this situation: your code may have to manipulate objects in drawing, such as creating new entities, or transforming existing entities, or changing something like current layer, text style.... However, after all the changes are being made, you may want user to confirm the changes and give the user an option to drop/cancel the changes.

Since your own code causes the changes, surely you can right the code to reverse the changes back. But the code to reversing things back could be very tedious to write.

All AutoCAD users know the "UNDO" command (or simply press "Ctrl+Z") is very useful to reverse changes made by previous commands back.

When doing .NET programming, the Transaction class does provides a mechanism of undoing changes back by rolling back or aborting the working transaction. One of my previous post touched this topic here. However, if the process of changing is quite complicated and some changes must be first applied to the drawing database before it can be presented to user to decide whether the changes should stay or to be rolled back, then using Transaction as the mechanism of undoing in code might be quite difficult to implement. So, why not utilize AutoCAD's built-in "UNDO" command to make the coding easier?

I recently worked on a project for a drafting tool, which starts a custom command and then leads user to create a series of entities at the end of the process. Since the user is guided via command line prompt, based on previous steps, the entities created have to be added into drawing database in order to give user clear visual feedback on what have been done in the process. At the end of the process, I thought it would be better to allow user to decide he/she wants to cancel what the command has done so far. In this particular case, writing code to reverse back the changes are quite difficult, if not impossible. Well, I can let the command end and just tell user that if he/she still can execute UNDO command to reverse back the changes done by the custom command. However, it would be much better the custom command itself can undo the changes based on user interaction. Thus, I wrote some code and tried with satisfactory result, which is presented in this post.

Here what I did is to use AutoCAD built-in UNDO command to set an UNDO mark and to undo the changes back to the UNDO mark. However, the trick is how the call UNDO command to set UNDO mark before the main process in the custom command and how to call UNDO command to roll changes back when necessary.

The code presented here is pretty self-descriptive. So, no more explanation is needed. Here is the code:

First, this is a class to manipulate drawing database:

    1 using System.Windows.Forms;
    2 using Autodesk.AutoCAD.ApplicationServices;
    3 using Autodesk.AutoCAD.DatabaseServices;
    4 using Autodesk.AutoCAD.EditorInput;
    5 using Autodesk.AutoCAD.Geometry;
    6 
    7 namespace UndoableCommand
    8 {
    9     public class DoWorkTool
   10     {
   11         private Document _dwg;
   12 
   13         public DoWorkTool(Document dwg)
   14         {
   15             _dwg = dwg;
   16         }
   17 
   18         public bool CreateSomething()
   19         {
   20             int count = AddEntities();
   21 
   22             if (count > 0)
   23             {
   24                 string msg = string.Format("{0} circle{1} created." +
   25                     "\n\nDo you want to keep {2}?",
   26                     count, count > 1 ? "s" : "", count > 1 ? "them" : "it");
   27 
   28                 return MessageBox.Show(msg, "Do work tool question",
   29                     MessageBoxButtons.YesNo,
   30                     MessageBoxIcon.Question) == DialogResult.Yes;
   31             }
   32             else
   33             {
   34                 return true;
   35             }
   36         }
   37 
   38         //This method creates circles in a loop
   39         private int AddEntities()
   40         {
   41             int count = 0;
   42 
   43             using (Transaction tran =
   44                 _dwg.TransactionManager.StartTransaction())
   45             {
   46                 BlockTableRecord model = (BlockTableRecord)tran.GetObject(
   47                     SymbolUtilityServices.GetBlockModelSpaceId(_dwg.Database),
   48                     OpenMode.ForWrite);
   49 
   50                 while (true)
   51                 {
   52                     Point3d centrePt;
   53                     double radius;
   54                     if (!CircleCetreRadius(out centrePt, out radius)) break;
   55 
   56                     CreateCircle(centrePt, radius, model, tran);
   57                     tran.TransactionManager.QueueForGraphicsFlush();
   58                     _dwg.Editor.UpdateScreen();
   59 
   60                     count++;
   61                 }
   62 
   63                 tran.Commit();
   64             }
   65 
   66             return count;
   67         }
   68 
   69         private bool CircleCetreRadius(out Point3d centre, out double rad)
   70         {
   71             centre=new Point3d();
   72             rad=double.MinValue;
   73 
   74             PromptPointOptions pOpt = new PromptPointOptions(
   75                 "\nPick circle centre:");
   76             PromptPointResult pRes = _dwg.Editor.GetPoint(pOpt);
   77             if (pRes.Status != PromptStatus.OK) return false;
   78             centre = pRes.Value;
   79 
   80             PromptDoubleOptions dOpt = new PromptDoubleOptions(
   81                 "\nEnter circle radius");
   82             dOpt.AllowNegative = false;
   83             dOpt.AllowNone = false;
   84             dOpt.AllowZero = false;
   85             PromptDoubleResult dRes = _dwg.Editor.GetDouble(dOpt);
   86             if (dRes.Status != PromptStatus.OK) return false;
   87             rad = dRes.Value;
   88 
   89             return true;
   90         }
   91 
   92         private void CreateCircle(
   93             Point3d centrePt, double radius,
   94             BlockTableRecord model, Transaction tran)
   95         {
   96             Circle c = new Circle();
   97             c.Center = centrePt;
   98             c.Radius = radius;
   99             c.SetDatabaseDefaults(_dwg.Database);
  100 
  101             model.AppendEntity(c);
  102             tran.AddNewlyCreatedDBObject(c, true);  
  103         }
  104     }
  105 }

Then, here is the CommandClass that defines the "undoable" custom command MyCmd:

    1 using Autodesk.AutoCAD.ApplicationServices;
    2 using Autodesk.AutoCAD.EditorInput;
    3 using Autodesk.AutoCAD.Runtime;
    4 
    5 [assembly: CommandClass(typeof(UndoableCommand.MyCommands))]
    6 
    7 namespace UndoableCommand
    8 {
    9     public class MyCommands
   10     {
   11         private static bool _validCall = false;
   12 
   13         [CommandMethod("MyCmd")]
   14         public static void RunMyCommand()
   15         {
   16             Document dwg = Application.DocumentManager.MdiActiveDocument;
   17             Editor ed = dwg.Editor;
   18             dwg.CommandEnded += dwg_CommandEnded;
   19 
   20             //Set UNDO mark
   21             dwg.SendStringToExecute("UNDO M ", true, false, false);
   22         }
   23 
   24         static void dwg_CommandEnded(object sender, CommandEventArgs e)
   25         {
   26             if (e.GlobalCommandName.ToUpper() == "MYCMD")
   27             {
   28                 //Run my real command
   29                 Document dwg = Application.DocumentManager.MdiActiveDocument;
   30                 Editor ed = dwg.Editor;
   31 
   32                 ed.WriteMessage("\nUndo mark has been set.");
   33 
   34                 //Run the command that does real work with its result
   35                 //might be undone
   36                 _validCall = true;
   37                 dwg.SendStringToExecute("MyRealCmd ", true, false, false);
   38             }
   39         }
   40 
   41         [CommandMethod("MyRealCmd", CommandFlags.NoHistory)]
   42         public static void DoWork()
   43         {
   44             Document dwg = Application.DocumentManager.MdiActiveDocument;
   45             Editor ed = dwg.Editor;
   46 
   47             //Prevent this command being used without UNDO mark is set
   48             if (!_validCall)
   49             {
   50                 ed.WriteMessage(
   51                     "\ncommand \"MyRealCmd\" cannot be executed directly. " +
   52                     "Use command \"MyCmd\" instead.");
   53                 return;
   54             }
   55 
   56             //Do real work here and with return indicating
   57             //whether UNDO is required
   58             DoWorkTool work = new DoWorkTool(dwg);
   59             bool keep = work.CreateSomething();
   60 
   61             //Remove the commandEnded handler
   62             dwg.CommandEnded -= dwg_CommandEnded;
   63             _validCall = false;
   64 
   65             if (!keep)
   66             {
   67                 //Undo if it is needed
   68                 dwg.SendStringToExecute("UNDO B ", true, false, false);
   69             } 
   70         }
   71     }
   72 }

Go to this video clip to see the undoable command in action.


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.