managing transaction in a function initiated from a modeless dialog

managing transaction in a function initiated from a modeless dialog

Anonymous
Not applicable
2,463 Views
7 Replies
Message 1 of 8

managing transaction in a function initiated from a modeless dialog

Anonymous
Not applicable

I have made a modeless a revit external command which brings up a modeless dialog (as an instance of a separate class).

 

It is behaving fine in responding to clicks, etc and allowing me to modify the revit model.

However when I try to create and commit a transaction in this routine, I get an error.  I understand that createing a new transaction from this context seems to be a no-no.

 

If I invoke the routine as its own command, (with no dialog)  there is no problem with the transaction.

Can a dialog invoke a revit external command directly? (i.e similar to the way a ribbon button does).

 

So - my question is:

What is the normal way to

 - create a modeless dialog box

- a click on the modeless dialog box launches a routine which can create and commit transactions.

 

I am sure there must be a pretty standard way to do this.  I would appreciate a pointer to a good example, etc.

Thanks.

 

 

0 Likes
2,464 Views
7 Replies
Replies (7)
Message 2 of 8

Anonymous
Not applicable

I am having the same problem, and it has been driving crazy all day. Cannot figure the right way to do it.

I tried creating a new IExternalCommand an executing it with a button on a WPF window, but did not work either.

Passing the doc and trying to create the transaciton within the WPF.cs file wont work either. 

0 Likes
Message 3 of 8

Anonymous
Not applicable

 

For me a modal dialog is sufficient, https://msdn.microsoft.com/en-us/library/5w2h2y4y.aspx

Hope it helps, I know that you are trying to do a modeless one.

 

I've used Window.ShowDialog() rather than Window.Show() ... Now I know the difference.

I'll leave it here for future reference, sorry if it does not help you.

 

 

0 Likes
Message 4 of 8

Anonymous
Not applicable
Hi,

I was just looking at the SDK samples and found one called:

ModelessForm_ExternalEvent

It seems to mostly address this issue. I just started looking at it so I
can't say any more than that.



Good luck.
0 Likes
Message 5 of 8

Anonymous
Not applicable
I use an idling event handler to initiate a transaction and run code from a list of custom class objects previously posted to a queue. The classes used implement a custom interface that contains an Execute (Document) method and can also contain data such as a list of elementids to work with. Its kind of like a simplified version of IExternalCommand. Does the job nicely.
0 Likes
Message 6 of 8

jeremytammik
Autodesk
Autodesk

Dear Abba,

 

Thank you for your query.

 

Looking at the ModelessForm_ExternalEvent is the perfect answer to this question.

 

Lots more background information is provided in The Building Coder topic group on Idling and External Events for Modeless Access and Driving Revit from Outside:

 

http://thebuildingcoder.typepad.com/blog/about-the-author.html#5.28

 

You might want to start reading those posts from the end.

 

Scott, you mentioned your Idling event handler and custom task queue repeatedly in the past.

 

I would love to take a look at it!

 

Would you be willing to share it here with us?

 

Thank you!

 

By the way, I do not recommend using the Idling event except for one-off notifications, to get back into a valid Revit API context asap.

 

For continuous polling of events such as you describe, I rather recommend using an external event.

 

The disadvantage of the Idling event is the lack of control over the polling frequency. It is either 'once only' or 'as often as possible'. The latter hogs the CPUY and degrades performance.

 

With an external event driven by some external event or raised from a separate thread, you have total control over the polling frequency.

 

For an in-depth analysis and example, please refer to the FireRatingCloud sample:

 

https://github.com/jeremytammik/FireRatingCloud

 

Especially, the last few enhancements to implement the real-time BIM update:

 

http://thebuildingcoder.typepad.com/blog/2016/04/real-time-bim-update-with-fireratingcloud-2017.html

 

http://thebuildingcoder.typepad.com/blog/2016/04/real-time-bim-update-via-fireratingcloud-windows-cl...

 

I hope this helps.

 

Best regards,

 

Jeremy



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

Message 7 of 8

Anonymous
Not applicable

sure Jeremy, here you go:

 

First the command class interface:

public interface IAsyncCommand
{
	void Execute(Document dbDoc);
	bool ShouldStayActive();
}

the ShouldStayActive() method is used to iindicate whether to leave the command in the queue so that it is run everytime the idling event is called, As you said, this is not recommended and as a rule I always set it false, but I added the feature just in case I might want it one day and I like suprising myself in the future with things I already took care of in the past.

 

The Command manager is a static class that looks like this:

public static class AsyncCommandManager
{
	private static List<IAsyncCommand> CommandList = new List<IAsyncCommand>();
	private static bool IsRegistered { get; set; }

	public static void PostCommand(UIApplication uiApp, IAsyncCommand cmd)
	{
		CommandList.Add(cmd);
		RegisterIdlingEvent(uiApp);

	}
	
	private static void RegisterIdlingEvent(UIApplication uiApp)
	{
		if(!IsRegistered)
		{
			uiApp.Idling += new EventHandler<IdlingEventArgs>(Execute);
			IsRegistered = true;
		}
	}

	private static void UnregisterIdlingEvent(UIApplication uiApp)
	{
		if(IsRegistered)
		{
			uiApp.Idling -= new EventHandler<IdlingEventArgs>(Execute);
			IsRegistered = false;
		}
	}

	private static void Execute(object sender, IdlingEventArgs eventArgs)
	{
		UIApplication uiApp = sender as UIApplication;
		UIDocument uiDoc = uiApp.ActiveUIDocument;
		Document dbDoc = uiDoc.Document;
		
		try
		{
			if(CommandList.Count > 0)
			{
				// make a copy of the command list so that we can loop through the copy and modify the original while still inside the loop.
				List<IAsyncCommand> tempCommandList = CommandList.ToList();

				using(TransactionGroup transGroup = new TransactionGroup(dbDoc, "Asynchronous Idling Commands"))
				{
					transGroup.Start();

					foreach(IAsyncCommand cmd in tempCommandList)
					{
						cmd.Execute(dbDoc);
						if(!cmd.ShouldStayActive())
						{							
							CommandList.Remove(cmd);
						}
					}
					transGroup.Assimilate();
				}

				if(CommandList.Count == 0)
				{
					UnregisterIdlingEvent(uiApp);
				}

			}
		}

		catch(Autodesk.Revit.Exceptions.ExternalApplicationException e)
		{
			Debug.WriteLine("Exception Encountered (Application)\n" + e.Message + "\nStack Trace: " + e.StackTrace);
		}

		catch(Autodesk.Revit.Exceptions.OperationCanceledException e)
		{
			Debug.WriteLine("Operation cancelled\n" + e.Message);
		}

		catch(Exception e)
		{
			Debug.WriteLine("Exception Encountered (General)\n" + e.Message + "\nStack Trace: " + e.StackTrace);
		}
	}
}

the execute method creates a transaction group and it is expected that each command will create and manage thier own transactions.

I did it this way so that the undo list doesn't get flooded if many commands are processed in one event.

it has the disadvantage that an exception thrown in any of the commands will rollback all of the commands commited so far, I probably should have moved the try/catch blocks inside the transaction's using block then assimilated the transaction group inside a finally block. maybe another day when I get the time...

 

I originally designed this to get around the restriction that InstanceVoidCutUtils.AddInstanceVoidCut() can't be called from within a dynamic updater. so as here's the code for that as an example implimentation of IAsyncCommand:

 

public class AsyncInstanceVoidCut : IAsyncCommand
{
	private ElementId HostId { get; set; }
	private ElementId CuttingId { get; set; }
	private bool StayActive { get; set; }

	// hide the default constructor, it has no purpose.
	private AsyncInstanceVoidCut()
	{		
	}

	public AsyncInstanceVoidCut(ElementId hostId, ElementId cuttingId, bool stayActive = false)
	{
		HostId = hostId;
		CuttingId = cuttingId;
		StayActive = stayActive;
	}
	public bool ShouldStayActive()
	{
		return StayActive;
	}


	public void Execute(Document dbDoc)
	{
		if(HostId != ElementId.InvalidElementId && CuttingId != ElementId.InvalidElementId)
		{
			Element hostElem = dbDoc.GetElement(HostId);
			Element cuttingElem = dbDoc.GetElement(CuttingId);

			if(hostElem != null && cuttingElem != null)
			{
				using(Transaction trans = new Transaction(dbDoc, "OTIC_AddInstanceVoidCut"))
				{
					trans.Start();

					InstanceVoidCutUtils.AddInstanceVoidCut(dbDoc, hostElem, cuttingElem);

					trans.Commit();
				}
			}
		}
	}
}

to add a command to the queue you pass an instance of a class that implements IAsyncCommand to the AsyncCommandManager.PostCommand() method like this:

 

AsyncCommandManager.PostCommand(uiDoc.Application, new AsyncInstanceVoidCut(wallElem.Id, cuttingElem.Id));

the Command manager will then register its event handler if it isn't already registered and start processing anything that you add to the command queue. Once the command list is empty it will unregister itself so that it isn't chewing up CPU time for no reason.

 

Message 8 of 8

Anonymous
Not applicable
Thanks for your suggestion, Scott.

I would love to see a sample of code/pseudocode to get the idea more
clearly.

In the end, I was able to solve my problem using the techniques in a SDK
sample project on Modeless dialog events.



That example is a bit more complex than I needed but gave the elements I
needed, which seem to be as follow:



1. In the scope of the externalcommand class (not the modeless dialog),
declare an eventhandler and an event object.

2. The event object exposes whatever 'parameters' are necessary to make
things happen (in my particular case, a simple string is enough)

3. The transaction is created and committed within the scope of the
eventhandler's execute().

4. From the modeless dialog, the appropriate properties of the event
object are set, and the event.raise() is called.



That seems to allow the modeless dialog to initiate a routine which manages
transactions.



I would be keen to hear if this makes sense to more seasoned revit-meisters.
(Jeremy?)



Thanks
0 Likes