It seems that one Autodesk employee that was lecturing at AU this
year wants to be a pot-stirrer, by recommending that managed API
programmers abort transactions when they're being used exclusively
for read-only purposes.
That squarely contradicts what I was told by one of the AutoCAD
architects who also happens to be the creator of the managed API,
which is that transactions should be committed, not aborted, even
when nothing has been changed, because aborting them has major
overhead (see test case below).
Below is the code I use to make comitting transactions implicit.
The code is centered around a class called the ReadOnlyTransaction.
ReadOnlyTransaction is Transaction that cannot be aborted. When a
regular Transaction is disposed, it will implicitly abort, if it
wasn't previously comitted.
When a ReadOnlyTransaction is disposed it will implicitly commit if
not previously comitted. You can use ReadOnlyTransaction in place of
a regular Transaction, when you are not modifying anything, and you
want to be sure the transaction is comitted regardless of anything,
including if an exception stops your code. You can omit the explicit
call to Commit() a ReadOnlyTransaction, because it does that for you
when it's disposed (so it should always be used with using(...), to
ensure that happens). The main benefit of using ReadOnlyTransaction
is that it allows you to exit a using() block at any point, without
having to call Commit() before you do, to ensure the transaction is
comitted. Another case where it can be useful, is where you prompt
for input while a transaction is active, and want to ensure that any
transparent view changes made by the user are not undone, which will
happen if you abort the transaction. If I need to access objects and
prompt for user input afterwards, I can start a ReadOnlyTransaction
for that, and if I later need to modify the database, then I'll start
a regular transaction for that.
The StartReadOnlyTransaction() extension methods of TransactionManager
simplify using the ReadOnlyTransaction. You can simply replace a call
to StartTransaction() with a call to StartReadOnlyTransaction(), and
omit the subsequent call to Commit(), and nothing else needs to change.
I've also posted a simple test case showing why aborting transactions
when there is nothing to roll back, is not a good idea, especially if
it is being done in frequently-called APIs that implicitly start and
end their own transactions internally.
using System;
using System.Linq;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Internal;
using AcApTransactionManager = Autodesk.AutoCAD.ApplicationServices.TransactionManager;
namespace Autodesk.AutoCAD.DatabaseServices
{
public class CustomTransaction : Transaction
{
public CustomTransaction( Transaction trans )
: base( trans.UnmanagedObject, trans.AutoDelete )
{
Interop.DetachUnmanagedObject( trans );
}
}
public class ReadOnlyTransaction : CustomTransaction
{
public ReadOnlyTransaction( Transaction trans )
: base( trans )
{
}
public override void Abort()
{
base.Commit();
}
protected override void Dispose( bool disposing )
{
if( disposing && base.AutoDelete )
base.Commit();
base.Dispose( disposing );
}
}
public static class TransactionManagerExtensionMethods
{
public static Transaction StartReadOnlyTransaction( this TransactionManager tm )
{
if( tm == null )
throw new ArgumentNullException
( "tm" ); return new ReadOnlyTransaction
( tm
.StartTransaction() ); }
public static Transaction StartReadOnlyTransaction( this AcApTransactionManager tm )
{
if( tm == null )
throw new ArgumentNullException
( "tm" ); return new ReadOnlyTransaction
( tm
.StartTransaction() ); }
}
}
Transaction Commit verses Abort test code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.ApplicationServices;
namespace Duh
{
public static class AbortThisExample
{
static int iterations = 1000;
[CommandMethod( "ABORT_THIS" )]
public static void AbortThisCommand()
{
Document doc = Application.DocumentManager.MdiActiveDocument;
PromptIntegerOptions peo
= new PromptIntegerOptions
( "\nIterations: " ); peo.AllowZero = false;
peo.AllowNegative = false;
peo.DefaultValue = iterations;
peo.UseDefaultValue = true;
PromptIntegerResult pir = doc.Editor.GetInteger( peo );
if( pir.Status != PromptStatus.OK )
return;
iterations = pir.Value;
Stopwatch st = Stopwatch.StartNew();
for( int i = 0; i < iterations; i++ )
{
using( Transaction tr = doc.TransactionManager.StartTransaction() )
{
BlockTable bt = (BlockTable) tr.GetObject(
doc.Database.BlockTableId,
OpenMode.ForRead, false, false );
tr.Abort();
}
}
st.Stop();
doc.Editor.WriteMessage( "\nTime to abort {0} transactions: {1} ms",
iterations, st.ElapsedMilliseconds );
st = Stopwatch.StartNew();
for( int i = 0; i < iterations; i++ )
{
using( Transaction tr = doc.TransactionManager.StartTransaction() )
{
BlockTable bt = (BlockTable) tr.GetObject(
doc.Database.BlockTableId,
OpenMode.ForRead, false, false );
tr.Commit();
}
}
st.Stop();
doc.Editor.WriteMessage( "\nTime to commit {0} transactions: {1} ms",
iterations, st.ElapsedMilliseconds );
}
}
}