Revit API Forum
Welcome to Autodesk’s Revit API Forums. Share your knowledge, ask questions, and explore popular Revit API topics.
cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

Combine Multiple Transactions into One Undo

11 REPLIES 11
SOLVED
Reply
Message 1 of 12
ZoltanFerenczy
4180 Views, 11 Replies

Combine Multiple Transactions into One Undo

I am creating an application that must do the following steps:

 

  1. Load a FamilySymbol from a file.
  2. Get the FamilyManager from the Family document and access some parameters.
  3. Place an instance of the FamilySymbol into the project.

 

In order to load the FamilySymbol into the project, I have to have a transaction running. Then, in order to open the Family document, the transaction has to be closed.  In order to place an instance of the component, a transaction has to be running.

 

This causes the application to create two undo points - one for each transaction.

 

Is there a way to combine the two transactions into a single undo point so the entire action of the command can be undone?

 

I am using the Manual TransactionMode.

11 REPLIES 11
Message 2 of 12

Hello Zoltan Ferenczy,

 

I am glad you’ve asked. Yes, it can certainly be done. It is what Transaction Groups are for. The steps are simple:

 

  1. Start a transaction group. Give it a name.
  2. Do your first transaction.
  3. Open the family document.
  4. Load the family into the project
  5. Close the transaction group by calling Assimilate method

 

The last operation will merge all transaction that have been committed within the group into just one transaction. It will bear the name of the transaction group.

 

When working with transaction group, make sure you scope them by the “using” block, just like you would with transactions.

 

I hope this helps. There is more info if needed in the RevitAPI.chm file. Simply lookup the TransactionGroup class.

Arnošt Löbel
Message 3 of 12

Ok,  Let me make sure I'm doing this correctly when dealing with exeptions.

 

When the TransactionGroup is rolled back, all of the commited and uncommited Transactions inside it are rolled back?

 

When a TransactionGroup is commited or assimilated, all of the Transactions inside of it must already be commited?

 

 

using (TransactionGroup transGroup = new TransactionGroup(document))

{

   transGroup.Start("Transaction Group");

 

   using (Transaction firstTrans = new Transaction(document))

   {

      try

      {

         firstTrans.Start("First Transaction");

 

         // do some stuff

 

         firstTrans.Commit();

      }

      catch

      {

         transGroup.Rollback(); // <-- We do not have to roll back firstTrans?

 

         return Result.Failed;

      }

   }

 

   using (Transaction secondTrans = new Transaction(document))

   {

      try

      {

         secondTrans.Start("Second Transaction");

 

         // do some stuff

 

         secondTrans.Commit();

      }

      catch

      {

         transGroup.Rollback(); // <-- We do not have to roll back secondTrans?

 

         return Result.Failed;

      }

   }

 

   transGroup.Assimilate();

 

   return Result.Succeeded;

}

 

Message 4 of 12

Yes ** 2.

 

There is no need to roll back in the catch block, beacuse Commit has not been called if you land there.

 

Correct, Arnošt?

 

By the way, you can make your code more readable by using the 'Insert Code' button.

 

Cheers,

 

Jeremy



Jeremy Tammik
Developer Technical Services
Autodesk Developer Network, ADN Open
The Building Coder

Message 5 of 12
arnostlobel
in reply to: jeremytammik

Hello Zoltan,

 

Regarding to your two questions:

 

  1. Yes and No, actually. Your statement is almost correct except one part, which I highlight here: “ ...all of the committed and uncommitted Transactions...”. The thing is that you cannot close (that is either Commit, RollBack, or Assimilate) a transaction group while there is an uncommitted transaction still open. All transactions that start inside a transaction group must be either properly committed or rolled back before any of the aforementioned methods can be called upon the transaction group object.

 

  1. Yes, basically what I have just stated above. All transactions inside a group must be either committed or rolled back. If you try to call any of the transaction-group closing methods while there is still a transaction open (in the same document), you will get an exception.

 

As for you code snippet, it is quite all right, except for two details, one more important than the other. I’ll start with that one:

 

  1. Indeed, as Jeremy already mentioned, you do not have to Roll Back the open transaction in the catch blocks, because it will be rolled back automatically upon leaving its using block. However, in your case it can in fact be even dangerous to call RollBack in the catch. The problem is that you may actually (in theory) get an exception from the Start method too, and if you do, you would most certainly get another one when attempting to roll the transaction back. That would be an exception thrown while exception handling, and that is never a good thing. Thus, if you want to call RollBack as you do, you need to test whether the transaction has actually started. (There is a method for that or you could test the status.)

 

  1. This second problem is less dangerous, but could lead to unexpected results. (Unexpected as your code goes, I mean.) A programmer should anticipate that committing a transaction does not need to succeed. It can fail and it is not all that uncommon, in fact. There are several possible reasons for such an outcome, one of which is a failed regeneration. When that happens and your transaction actually fails, it is most likely that you would not like to continue with opening another transaction which might only work if the previous transaction succeeded. Ignoring the results of transactions can lead to a spiral of errors and failures that might be rather challenging to untangle.

 

  1. Lastly, a very minor thingy, that is not a problem, but could make your code simpler. You do not have to have instantiate a new transaction for each and every transaction. You can re-start an existing one instead; simply give it a proper name in the Start method.

 

Putting all the above comments into action, I’ve rewritten your simple snippet as follows:

 

using (TransactionGroup transGroup = new TransactionGroup(document))
{
   using (Transaction trans = new Transaction(document))
   {
      try
      {
         transGroup.Start("Action");

         trans.Start("First Transaction");
 
         // do some stuff
 
         if (trans.Commit() != TransactionStatus.Committed)
         {
            return Result.Failed;
         }

         trans.Start("Second Transaction");
 
         // do some more stuff
 
         trans.Commit();
 
         if (trans.Commit() != TransactionStatus.Committed)
         {
            return Result.Failed;
         }

         transGroup.Assimilate();
      }
      catch
      {
         return Result.Failed;
      }
   }
   return Result.Succeeded;
}

 

Cheers!

Arnošt Löbel
Message 6 of 12
jeremytammik
in reply to: arnostlobel

Dear Arnošt,

 

Thank you very much once again for such a complete and exhaustive answer.

 

Yet again, I captured it for future reference on The Building Coder:

 

http://thebuildingcoder.typepad.com/blog/2015/02/using-transaction-groups.html

 

Cheers,

 

Jeremy



Jeremy Tammik
Developer Technical Services
Autodesk Developer Network, ADN Open
The Building Coder

Message 7 of 12

Thank you Arnošt and Jeremy; this has been very helpfull.

 

One more question:

 

In the same logic as checking that Transaction.Commit() has returned TransactionStatus.Committed, is it a good idea to check that Transaction.Start() has returned TransactionStatus.Started?  Is there a reason why it wouldn't?

 

 

Message 8 of 12

Hello Zoltan,

 

I have been asked that very question many times in the past. My answer is that even though it is indeed possible in theory that the Start method returns a status other then Started, it is rather unlikely that it actually happens, if is it all possible in the public API. (Note: We, Revit programmers use the same classes internally and for us it is a tiny bit more likely that such a situation may happen.) In most situations that come to my mind the Start method would raise an exception if it is not possible (or permitted) to start a transaction, or any one of the three transaction-phase objects for that matter.

 

And since I am back on this particular case, there is one small detail I could have changed in the snippet I wrote in my post above. I could have explicitly roll back the transaction group in the two places where I return with a failure due to a transaction failing to commit. Although this explicit closing of the group is not necessary (for it will be closed implicitly upon leaving its using block), I do tend to write my code that way. The reason is more style related; I simply prefer to make it clear to anyone who comes across my code that I knew what I was doing (here, I am aware that the group must be rolled back.)

Arnošt Löbel
Message 9 of 12
jlpgy
in reply to: arnostlobel

Hi Arnost Lobel:

I have read your reply, and looked into the TransactionGroup.Assimilate() method in help document .chm file.

I found that this method takes no parameters.

What  I want to ask it:

  • How do I know how many transactions, and which ones are assimilated into group?
  • What if I start a transaction before starting a group ( in one API context), and then call Assimilate(), will this ahead started one also assimilated into group?
  • Another interesting thing is that I found that I can leave API context (external event. Execute() method) with the transaction group still open. See my codes:
public void Execute(RvtUiApplication app)
		{
			if(WpfTarget.Transactions.ContainsKey(this.GetName()))
			{
				Transaction = WpfTarget.Transactions[GetName()];
				if(Transaction == null)
				{
					Transaction = new Transaction(WpfTarget.CmdVars.DbDoc, GetName());
					WpfTarget.Transactions[GetName()] = this.Transaction;
				}
			}
			else
			{
				WpfTarget.Transactions.Add(GetName(), new Transaction(WpfTarget.CmdVars.DbDoc, GetName()));
			}

			WpfTarget.TransGroup.Start();

			Transaction.Start();

			Level.Create(WpfTarget.CmdVars.DbDoc, 30);

			Transaction.Commit();

			WpfTarget.TransGroup.Assimilate();

		}

Just ignore the head part of this Execute() method, they are for my own WPF window. Notice that before leaving this external event handler context, I called only Assimilated(), but not Rollback() or Commit().

I ran these codes without debugging, and Revit threw no errors.

 

Tested in API ver 2018. And if running in VS debugger, and single step out of this method, there goes a page like this:

图像 3.png

 

Does it mean that I can 'modelessly' use TransactionGroup?Robot Happy

 

Also expecting Jeremy to have a discussion about this. 🙂

 

 

单身狗;代码狗;健身狗;jolinpiggy@hotmail.com
Message 10 of 12
Anonymous
in reply to: jlpgy

Now this is rather funny, I think. I have found this post accidentally while googling up something else which had my name in it. I was surprised to see a new question to a very old post I had participated in, and even more I was surprised to find the question unanswered. I do not work for Autodesk anymore, but since the transaction API was kind of a favorite of mine when I worked on the API, I kind of feel somehow obligated to answer such questions. I mean – I would not look for them purposely, of course, but if I come across one, I’ll answer it if I can.

 

Julong.Lin, to your questions:

 

  1. You would not know how many transactions would be assimilated in a transaction group unless you keep a count of the committed transactions by yourself (though I do not know why you would need to do that). A transaction groups simply assimilates (when assimilated) all transactions that started and were committed inside the group.
  2. The previous answer sort of also answers your second question – you cannot start a transaction group while there is a transaction still open. The TransactionGroup.Start method would throw an exception in such a situation. To test whether there is a transaction open you can use the Document.IsModifiable method.
  3. To your third question – it should not be possible to leave your command context with either a transaction or transaction group still open. Well, technically you can do that, of course, but if you do, Revit will roll them back for you and you would lose all changes made in your external command. The internal implementation might have changed in the last three years, I suppose, but it used to be the following way: Before an external command was launched, Revit would start an internal transaction group. Then the command was executed and upon returning from it, be that a natural exit or an exception, Revit would test whether there were any transactions or transaction groups open. If there are, Revit would force them to be rolled back and then it would follow with rolling back the internal transaction group which started before the external command was invoked. This is all for safety reasons; Revit is cautious about external commands that forgot to close all their transactions and transaction groups.

I hope it make sense what I wrote.

 

I also have one comment about your code. One thing that is not quite clear to me is what happens if your container of transactions does not have an entry for this.GetName(); The code would proceed to the Else block in which a new transaction is instantiated and added to your container of Transactions. However, the member this.Transaction is not updated, which seem strange with respect to the rest of your code. After you leave your if-else block and you start your transaction group you then call Transaction.start() – which transaction would that be? Do you have a global Transaction instance? I assume you do, because in this particular case your this.Transaction would not be set to the new transaction you instantiated inside the Else block. My guess is that you would start and commit a different transaction (if there was one instantiated before), which would have a different name. Of course, you would not notice that if you successfully assimilated the transaction group, which is probably why that detail escaped you.

 

Arnošt Löbel

Message 11 of 12
jeremytammik
in reply to: Anonymous

Dear Arnošt,

 

Thank you very much for jumping in and picking this up once again!

 

I am happy to hear from you again and took this opportunity to immediately promote your answer to a new post on The Building Coder:

 

https://thebuildingcoder.typepad.com/blog/2018/11/more-on-transaction-groups-and-assimilation.html

 

I hope you are having a great time and enjoying life post-Revit!



Jeremy Tammik
Developer Technical Services
Autodesk Developer Network, ADN Open
The Building Coder

Message 12 of 12
jlpgy
in reply to: Anonymous

@Anonymous

Hi! Thank you for your answer, which is quite clear.

Sorry for this rather delayed reply. Have not seen your fresh answer is this forum for a while, so I thought it might sink 🙂

What I asked was really amateur. In the recent several months, I strictly stick to the Revit API best practise, which means always closing a Transiction before leaving a safe API context.

 

Thank you again, and gald to hear from you again in this Forum.

单身狗;代码狗;健身狗;jolinpiggy@hotmail.com

Can't find what you're looking for? Ask the community or share your knowledge.

Post to forums  

Autodesk DevCon in Munich May 28-29th


Rail Community