I received this question by email last week:
Is it ever required to use more than one transaction per program?
The simple answer is that you mostly only need one transaction active per command: you shouldn't leave a transaction active outside of a command, as this is likely to cause problems at some point, and within your own command one transaction is typically enough to do what you want.
That said, the transaction mechanism inside AutoCAD has some pretty cool nesting capabilities that make it very flexible and a great way to manage sets of database operation and to roll them back should they no longer be necessary.
In this post we're going to look at some code which uses some nested transactions and allows the user to choose whether to commit or abort each one.
Here's what the ObjectARX Developer's Guide says about "Nesting Transactions":
Transactions can be nested—that is, you can start one transaction inside another and end or abort the recent transaction. The transaction manager maintains transactions in a stack, with the most recent transaction at the top of the stack. When you start a new transaction using AcTransactionManager::startTransaction(), the new transaction is added to the top of the stack and a pointer to it is returned (an instance of AcTransaction). When someone calls AcTransactionManager::endTransaction() or AcTransactionManager::abortTransaction(), the transaction at the top of the stack is ended or aborted.
When object pointers are obtained from object IDs, they are always associated with the most recent transaction. You can obtain the recent transaction using AcTransactionManager::topTransaction(), then use AcTransaction::getObject() or AcTransactionManager::getObject() to obtain a pointer to an object. The transaction manager automatically associates the object pointers obtained with the recent transaction. You can use AcTransaction::getObject() only with the most recent transaction.
When nested transactions are started, the object pointers obtained in the outer containing transactions are also available for operation in the innermost transaction. If the recent transaction is aborted, all the operations done on all the objects (associated with either this transaction or the containing ones) since the beginning of the recent transaction are canceled and the objects are rolled back to the state at the beginning of the recent transaction. The object pointers obtained in the recent transaction cease to be valid once it's aborted.
If the innermost transaction is ended successfully by calling AcTransactionManager::endTransaction(), the objects whose pointers were obtained in this transaction become associated with the containing transaction and are available for operation. This process is continued until the outermost (first) transaction is ended, at which time modifications on all the objects are committed. If the outermost transaction is aborted, all the operations on all the objects are canceled and nothing is committed.
While this was written for ObjectARX, it also applies (with some minor changes to the terminology) to the world of .NET programming in AutoCAD.
Here's the idea... we're going to have an outer transaction, within which we're going to create a grid of filled circles (hatches, in fact). We're then going to nest a number of transactions (for now only at one level deep, although there's nothing stopping us nesting more deeply), which modify the colour of various items in the grid.
Here's another way of looking at the transaction, hierarchically:
- [Outer] Create a grid of filled circles
- [Nested 1] Make all these circles red
- [Nested 2] Make alternate lines yellow
- [Nested 3] Draw a magenta sine wave on the grid
At the end of each transaction, the user will be given the choice to commit or abort it.
Here's the C# code:
using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
namespace Transactionality
{
public class Commands
{
[CommandMethod("nt")]
public void NestedTransactions()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
// Our outermost transaction starts by creating
// a load of circles, the ids of which are in
// a collection we then pass around
ObjectIdCollection ids =
CreateLotsOfCircles(tr, db, 0.5, 1.2, 30);
tr.TransactionManager.QueueForGraphicsFlush();
Transaction tr2 =
tr.TransactionManager.StartTransaction();
using (tr2)
{
// Our first nested transaction turns the
// circles red
ChangeColor(tr2, ids, 1);
tr2.TransactionManager.QueueForGraphicsFlush();
CommitOrAbort(
ed,
tr2,
"transaction to make the circles red"
);
}
Transaction tr3 =
tr.TransactionManager.StartTransaction();
using (tr3)
{
// Our second nested transaction turns the
// circles yellow
ObjectIdCollection alternates =
new ObjectIdCollection();
for (int i = 0; i < ids.Count; i++)
{
if (i % 2 == 0)
alternates.Add(ids[i]);
}
ChangeColor(tr3, alternates, 2);
tr3.TransactionManager.QueueForGraphicsFlush();
CommitOrAbort(
ed,
tr3,
"transaction to make alternate circles yellow"
);
}
Transaction tr4 =
tr.TransactionManager.StartTransaction();
using (tr4)
{
// Our third nested transaction draws a sine wave
// on the grid of circles
SineWave(tr4, ids, 6);
tr4.TransactionManager.QueueForGraphicsFlush();
CommitOrAbort(
ed,
tr4,
"transaction to draw a magenta sine wave");
}
CommitOrAbort(
ed,
tr,
"top-level transaction"
);
}
}
// Helper function to handle the user-input around
// the decision to commit/abort a particular
// transaction (and then the actual commit/abort
// operation)
private void CommitOrAbort(
Editor ed,
Transaction tr,
string desc
)
{
PromptKeywordOptions pko =
new PromptKeywordOptions(
"\nCommit or abort the " + desc + "?"
);
pko.AllowNone = true;
pko.Keywords.Add("Commit");
pko.Keywords.Add("Abort");
pko.Keywords.Default = "Commit";
PromptResult pkr =
ed.GetKeywords(pko);
if (pkr.StringResult == "Abort")
{
tr.Abort();
}
else
{
tr.Commit();
}
}
// Create a grid of filled circles (well, actually
// hatches) based on the information provided
private ObjectIdCollection CreateLotsOfCircles(
Transaction tr,
Database db,
double radius,
double offset,
int numOnSide
)
{
ObjectIdCollection ids =
new ObjectIdCollection();
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
bt[BlockTableRecord.ModelSpace],
OpenMode.ForWrite
);
for (int i = 0; i < numOnSide; i++)
{
for (int j = 0; j < numOnSide; j++)
{
// To get a filled circle we're going to create a
// circle and then hatch it with the "solid" pattern
// Start with the hatch itself...
Hatch hat = new Hatch();
hat.SetDatabaseDefaults();
hat.SetHatchPattern(
HatchPatternType.PreDefined,
"SOLID"
);
ObjectId id = btr.AppendEntity(hat);
tr.AddNewlyCreatedDBObject(hat, true);
ids.Add(id);
// Now we create the loop, which we make db-resident
// (appending a transient loop caused problems, so
// we're going to use the circle and then erase it)
Circle cir = new Circle();
cir.Radius = radius;
cir.Center =
new Point3d(i * offset, j * offset, 0);
ObjectId lid = btr.AppendEntity(cir);
tr.AddNewlyCreatedDBObject(cir, true);
// Have the hatch use the loop we created
ObjectIdCollection loops =
new ObjectIdCollection();
loops.Add(lid);
hat.AppendLoop(HatchLoopTypes.Default, loops);
hat.EvaluateHatch(true);
// Now we erase the loop
cir.Erase();
}
}
return ids;
}
// Loop through a list of objects and change their colour
private void ChangeColor(
Transaction tr,
ObjectIdCollection ids,
int col
)
{
foreach (ObjectId id in ids)
{
Entity ent =
tr.GetObject(id, OpenMode.ForRead)
as Entity;
if (ent != null)
{
if (!ent.IsWriteEnabled)
ent.UpgradeOpen();
ent.ColorIndex = col;
}
}
}
// Draw a sine wave on a grid of objects
private void SineWave(
Transaction tr,
ObjectIdCollection ids,
int col
)
{
// Assume that we're working with a square grid
int numOnSide =
(int)Math.Sqrt(ids.Count);
// Loop along the x axis
for (int i = 0; i < numOnSide; i++)
{
// Get a result between -1 and 1
double res =
Math.Sin(2 * Math.PI * i / (numOnSide - 1));
// Normalise to between 0 and 1
res = (res + 1) / 2;
// Get the corresponding index in the collection
int j = (int)(res * numOnSide),
idx = i * numOnSide + j;
// Open and modify the appropriate "y" object
Entity ent =
tr.GetObject(
ids[idx],
OpenMode.ForWrite
) as Entity;
ent.ColorIndex = col;
}
}
}
}
Here's what happens as we run the NT command, committing at each step:
Command: NT
Commit or abort the transaction to make the circles red? [Commit/Abort] <Commit>: [Enter]
Commit or abort the transaction to make alternate circles yellow? [Commit/Abort] <Commit>: [Enter]
Commit or abort the transaction to draw a magenta sine wave? [Commit/Abort] <Commit>: [Enter]
Commit or abort the top-level transaction? [Commit/Abort] <Commit>: [Enter]
Now let's see what happens if choose to abort some of the transactions as we run through the NT command...
[Abort the one to make the grid red, which means the circles start off being black]
[Abort the one to make the grid alternately yellow]
[Abort both the above-mentioned transactions]
Whenever you abort the outer-most transaction, everything will disappear, of course.
Hopefully this gives you some idea of how nested transactions might be useful to you, allowing different sets of operations to be applied during one of your command based on changing circumstances or user-input.
This "graphing" concept is quite fun, and one I think I'm going to plat around with a little more. There's definite scope for making this more of a pipelining application, which passes the data from function to function - a perfect application of F#, in fact. Hmmm. :-)