AutoCAD .NET API provides Transaction as the main mechanism to manipulate AutoCAD DBObject and Entity instances when opening, reading, writing, notifying, committing, aborting, closing, and disposing them.
The transaction mechanism in AutoCAD .NET API has a clear use pattern. Let us take the scenario of creating a new entity for example. The following steps are common and good practices:
• Starting a transaction (through calling the TransactionManager.StartTransaction() method from a Database, Document, or possibly somewhere else);
• Opening the container (represented by the BlockTable) of the drawing space (Model Space or Paper Space) and the block definitions (from which those AutoCAD INSERTs are created) for READ (indicated by the OpenMode.ForRead enumeration value);
• Finding the particular drawing space (e.g. ModelSpace) or a specific block definition, which is represented by a BlockTableRecord entry regardless, and opening it for WRITE (indicated by the OpenMode.ForWrite enumeration value);
• Creating the new Entity (e.g. Circle) and setting its necessary properties;
• Appending the entity to the drawing space or block definition (i.e. the API term BlockTableRecord) through calling its AppendEntity() method;
• Adding the entity to the transaction as well through calling the AddNewlyCreatedDBObject() method against the transaction itself;
• Committing the transaction through calling the Commit() method of the transaction instance;
• Finally disposing of the transaction through either calling the Dispose() method of the transaction explicitly or wrapping all the above operations into a using statement to let the .NET Framework do the work for us automatically no matter whether everything goes well or something goes wrong.
Here is a typical use for such a situation:
Using tr As Transaction = db.TransactionManager.StartTransaction()
Dim bt As BlockTable = TryCast(tr.GetObject(db.BlockTableId, OpenMode.ForRead), BlockTable)
Dim btr As BlockTableRecord = TryCast(tr.GetObject(bt(BlockTableRecord.ModelSpace), OpenMode.ForWrite), BlockTableRecord)
For index As Integer = 0 To 4999
Using cir As New Circle()
cir.Center = New Point3d(index / 10.0, 0.0, 0.0)
cir.Radius = 5.0
btr.AppendEntity(cir)
tr.AddNewlyCreatedDBObject(cir, True)
End Using
Next
tr.Commit()
End Using
Now we know what AutoCAD .NET Transaction is and what steps we generally should follow as far as entity creation is concerned. However, these are not the major concern of this article. It will be talking about something deep, much deeper actually, Transaction and Performance.
Supposing we would like to create thousands of entities (e.g. circles) into the same Model Space of the same Database, what shall we do as far as transaction is concerned?
Most of times, people would have already made a method to help create such an entity (Circle here):
Public Shared Sub CreateCircle(center As Point3d, radius As Double)
Dim db As Database = HostApplicationServices.WorkingDatabase
Using tr As Transaction = db.TransactionManager.StartTransaction()
Dim bt As BlockTable = TryCast(tr.GetObject(db.BlockTableId, OpenMode.ForRead), BlockTable)
Dim btr As BlockTableRecord = TryCast(tr.GetObject(bt(BlockTableRecord.ModelSpace), OpenMode.ForWrite), BlockTableRecord)
Using cir As New Circle()
cir.Center = center
cir.Radius = radius
btr.AppendEntity(cir)
tr.AddNewlyCreatedDBObject(cir, True)
End Using
tr.Commit()
End Using
End Sub
As can be noticed, the circle creation is wrapped into a transaction. Then it seems natural to reuse the above code and add some more code to create the thousands of circles:
<CommandMethod("CreateCircles2")> _
Public Shared Sub CreateCircles2_Method()
Dim ed As Editor = MgdAcApplication.DocumentManager.MdiActiveDocument.Editor
Dim begin As DateTime = DateTime.Now
For index As Integer = 0 To 4999
Dim cen As New Point3d(index / 10.0, 20.0, 0.0)
CreateCircle(cen, 5.0)
Next
Dim elapsed As TimeSpan = DateTime.Now.Subtract(begin)
ed.WriteMessage("Time elapsed in CreateCircles2: {0}" & vbLf, elapsed.TotalMilliseconds)
End Sub
It looks structural, reusable and readable, doesn’t it?
It certainly does and really is. However, it sacrifices the performance, which is not the major concern many times but surely is many others.
Let us look at another way to do the same thing:
<CommandMethod("CreateCircles1")> _
Public Shared Sub CreateCircles1_Method()
Dim db As Database = HostApplicationServices.WorkingDatabase
Dim ed As Editor = MgdAcApplication.DocumentManager.MdiActiveDocument.Editor
Dim begin As DateTime = DateTime.Now
Using tr As Transaction = db.TransactionManager.StartTransaction()
Dim bt As BlockTable = TryCast(tr.GetObject(db.BlockTableId, OpenMode.ForRead), BlockTable)
Dim btr As BlockTableRecord = TryCast(tr.GetObject(bt(BlockTableRecord.ModelSpace), OpenMode.ForWrite), BlockTableRecord)
For index As Integer = 0 To 4999
Using cir As New Circle()
cir.Center = New Point3d(index / 10.0, 0.0, 0.0)
cir.Radius = 5.0
btr.AppendEntity(cir)
tr.AddNewlyCreatedDBObject(cir, True)
End Using
Next
tr.Commit()
End Using
Dim elapsed As TimeSpan = DateTime.Now.Subtract(begin)
ed.WriteMessage("Time elapsed in CreateCircles1: {0}" & vbLf, elapsed.TotalMilliseconds)
End Sub
As can be noticed, all the circle creations are wrapped into a single transaction this time. Except for this point, all other operations are the same.
What good is it?
It improves the performance. Let us run the two commands in the same environment and compare the elapsed time to get a concrete clear idea:
Command: CreateCircles1
Time elapsed in CreateCircles1: 577.201
Command: CREATECIRCLES2
Time elapsed in CreateCircles2: 1154.402
As can be seen, it took twice time for the second command to do the same thing as the first one. If we change the order of the command running, we may notice bigger difference.
Is it a big deal that half more second is consumed here?
Probably not in this simple sample case, but in most many reality ones it likely will be, considering the fact that many other operations may be also involved in between such as reading ids of layers and line types and assigning them to the circles, transactions may be nested and such like.
Therefore, it is a good practice to use transactions as less as possible and wrap operations as many as possible into a single transaction since transactions are expensive especially nested ones, if the performance is the most concern.
Of course, that brings more coding work and needs better design. Then another good practice is suggested here, creating two different signatures of each entity creation method with one wrapping everything in a complete transaction and the other accepting an additional transaction pointer. Here is the improved version of the first command:
<CommandMethod("CreateCircles1_New")> _
Public Shared Sub CreateCircles1_New_Method()
Dim db As Database = HostApplicationServices.WorkingDatabase
Dim ed As Editor = MgdAcApplication.DocumentManager.MdiActiveDocument.Editor
Dim begin As DateTime = DateTime.Now
Using tr As Transaction = db.TransactionManager.StartTransaction()
For index As Integer = 0 To 4999
CreateCircle(tr, New Point3d(index / 10.0, 0.0, 0.0), 5.0)
Next
tr.Commit()
End Using
Dim elapsed As TimeSpan = DateTime.Now.Subtract(begin)
ed.WriteMessage("Time elapsed in CreateCircles1_New: {0}" & vbLf, elapsed.TotalMilliseconds)
End Sub
Public Shared Sub CreateCircle(tr As Transaction, center As Point3d, radius As Double)
Dim db As Database = HostApplicationServices.WorkingDatabase
Dim bt As BlockTable = TryCast(tr.GetObject(db.BlockTableId, OpenMode.ForRead), BlockTable)
Dim btr As BlockTableRecord = TryCast(tr.GetObject(bt(BlockTableRecord.ModelSpace), OpenMode.ForWrite), BlockTableRecord)
Using cir As New Circle()
cir.Center = center
cir.Radius = radius
btr.AppendEntity(cir)
tr.AddNewlyCreatedDBObject(cir, True)
End Using
End Sub
If we run the new command, we will find it has the same good performance as the old one:
Command: CreateCircles1_New
Time elapsed in CreateCircles1_New: 577.201
Better, we can reuse the code to redefine the helper method that has not the Transaction argument:
Public Shared Sub CreateCircle2(center As Point3d, radius As Double)
Dim db As Database = HostApplicationServices.WorkingDatabase
Using tr As Transaction = db.TransactionManager.StartTransaction()
CreateCircle(tr, center, radius)
tr.Commit()
End Using
End Sub
In this way, all purposes can be met in balance. Code still looks concise enough, readable, maintainable, and structural, and performance has been taken into account as well. Of course, the same ideas can be extended to cover other scenarios such as non-current databases, XREF databases and even long transactions.
The code snippets as mentioned above have been put together and appeneded below for convenience.
<CommandMethod("CreateCircles2")> _
Public Shared Sub CreateCircles2_Method()
Dim ed As Editor = MgdAcApplication.DocumentManager.MdiActiveDocument.Editor
Dim begin As DateTime = DateTime.Now
For index As Integer = 0 To 4999
Dim cen As New Point3d(index / 10.0, 20.0, 0.0)
CreateCircle(cen, 5.0)
Next
Dim elapsed As TimeSpan = DateTime.Now.Subtract(begin)
ed.WriteMessage("Time elapsed in CreateCircles2: {0}" & vbLf, elapsed.TotalMilliseconds)
End Sub
Public Shared Sub CreateCircle(center As Point3d, radius As Double)
Dim db As Database = HostApplicationServices.WorkingDatabase
Using tr As Transaction = db.TransactionManager.StartTransaction()
Dim bt As BlockTable = TryCast(tr.GetObject(db.BlockTableId, OpenMode.ForRead), BlockTable)
Dim btr As BlockTableRecord = TryCast(tr.GetObject(bt(BlockTableRecord.ModelSpace), OpenMode.ForWrite), BlockTableRecord)
Using cir As New Circle()
cir.Center = center
cir.Radius = radius
btr.AppendEntity(cir)
tr.AddNewlyCreatedDBObject(cir, True)
End Using
tr.Commit()
End Using
End Sub
<CommandMethod("CreateCircles1")> _
Public Shared Sub CreateCircles1_Method()
Dim db As Database = HostApplicationServices.WorkingDatabase
Dim ed As Editor = MgdAcApplication.DocumentManager.MdiActiveDocument.Editor
Dim begin As DateTime = DateTime.Now
Using tr As Transaction = db.TransactionManager.StartTransaction()
Dim bt As BlockTable = TryCast(tr.GetObject(db.BlockTableId, OpenMode.ForRead), BlockTable)
Dim btr As BlockTableRecord = TryCast(tr.GetObject(bt(BlockTableRecord.ModelSpace), OpenMode.ForWrite), BlockTableRecord)
For index As Integer = 0 To 4999
Using cir As New Circle()
cir.Center = New Point3d(index / 10.0, 0.0, 0.0)
cir.Radius = 5.0
btr.AppendEntity(cir)
tr.AddNewlyCreatedDBObject(cir, True)
End Using
Next
tr.Commit()
End Using
Dim elapsed As TimeSpan = DateTime.Now.Subtract(begin)
ed.WriteMessage("Time elapsed in CreateCircles1: {0}" & vbLf, elapsed.TotalMilliseconds)
End Sub
<CommandMethod("CreateCircles1_New")> _
Public Shared Sub CreateCircles1_New_Method()
Dim db As Database = HostApplicationServices.WorkingDatabase
Dim ed As Editor = MgdAcApplication.DocumentManager.MdiActiveDocument.Editor
Dim begin As DateTime = DateTime.Now
Using tr As Transaction = db.TransactionManager.StartTransaction()
For index As Integer = 0 To 4999
CreateCircle(tr, New Point3d(index / 10.0, 0.0, 0.0), 5.0)
Next
tr.Commit()
End Using
Dim elapsed As TimeSpan = DateTime.Now.Subtract(begin)
ed.WriteMessage("Time elapsed in CreateCircles1_New: {0}" & vbLf, elapsed.TotalMilliseconds)
End Sub
Public Shared Sub CreateCircle(tr As Transaction, center As Point3d, radius As Double)
Dim db As Database = HostApplicationServices.WorkingDatabase
Dim bt As BlockTable = TryCast(tr.GetObject(db.BlockTableId, OpenMode.ForRead), BlockTable)
Dim btr As BlockTableRecord = TryCast(tr.GetObject(bt(BlockTableRecord.ModelSpace), OpenMode.ForWrite), BlockTableRecord)
Using cir As New Circle()
cir.Center = center
cir.Radius = radius
btr.AppendEntity(cir)
tr.AddNewlyCreatedDBObject(cir, True)
End Using
End Sub
Public Shared Sub CreateCircle2(center As Point3d, radius As Double)
Dim db As Database = HostApplicationServices.WorkingDatabase
Using tr As Transaction = db.TransactionManager.StartTransaction()
CreateCircle(tr, center, radius)
tr.Commit()
End Using
End Sub
The leading edge AutoCAD .NET Addin Wizard (AcadNetAddinWizard) provides various project wizards, item wizards, coders and widgets to help program AutoCAD .NET addins.
Recent Comments