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.