Is it possible to chain Idling events?
Say I have two functions, FuncA and FuncB. I want to run FuncA the first time revit idles, then run FuncB after FuncA is done running and revit idles again. Inside my FuncA handler, I am removing the handler to FuncA from UIApp.Idling, but I can not assign a new handler because I don't have a valid API context.
Is there a way for me to achieve my intended behavior?
NOTE: The reason I'm doing any of this at all is because of this referenced post. For some reason, Revit throws an internal exception if my FuncA and FuncB are called one after the other. So I am separating them with an idling event. Now I need to add another idling event before FuncA.
It doesn't sound like a good idea in general, however.
When Revit idles you want to run FunctionA when it idles again you want to run FunctionA but with different settings i.e. subscribe to FunctionA and change what FunctionA does based on what it did last. Then after a set number of things that FunctionA does unsubscribe. In the end you are waiting for the same event in-between doing tasks with the same function.
You can use static variable etc. to store stage of FunctionA.
Afaik, you cannot chain them directly, but you can easily achieve the same effect:
Depending on other factors, Revit may or may not execute other actions in between the two event handler calls, but their relative order should be preserved.
You say, I cannot assign a new handler because I don't have a valid API context.
Why not? Within the Idling event handler, I thought you do have a valid Revit API context.
Question: why do the actions A and B have to be executed separately? Can't you just execute them both within the same Idling event handler?
You should create a queue service for that, subscribing and unsubscribing Revit events in your App as static.
Something like this maybe helps: https://gist.github.com/ricaun/11b272c5bec46f05c3fe49525fcc3fdf
You can register the service in your IExternalApplication.
public class App : IExternalApplication
{
public static IdlingQueueService IdlingActionService { get; private set; }
public Result OnStartup(UIControlledApplication application)
{
IdlingActionService = new IdlingQueueService(application);
return Result.Succeeded;
}
public Result OnShutdown(UIControlledApplication application)
{
IdlingActionService?.Dispose();
return Result.Succeeded;
}
}
Then you could add some Action to run on the Idling Event, each one gonna run in a different Idling Event.
[Transaction(TransactionMode.Manual)]
public class Command : IExternalCommand
{
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elementSet)
{
App.IdlingActionService.Add((uiapp) => { System.Windows.MessageBox.Show("1"); });
App.IdlingActionService.Add((uiapp) => { System.Windows.MessageBox.Show("2"); });
return Result.Succeeded;
}
}
I hope this helps.
Thank you all for your responses. I have tried 3 different solutions all to no success.
public async void OnIdlingA(object sender, IdlingEventArgs e)
{
Autodesk.Revit.UI.UIApplication uiapp = sender as
Autodesk.Revit.UI.UIApplication;
Debug.Assert(null != uiapp,
"expected a valid Revit application instance");
if (uiapp != null)
{
uiapp.Idling -= OnIdlingA;
uiapp.Idling += new
EventHandler<Autodesk.Revit.UI.Events.IdlingEventArgs>
(OnIdlingB);
await FuncA();
}
}
If I step back from all the details of my implementation, my problem is really not too complicated. I just wish I could run the Execute function of IExternalCommand like below.
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
Object1 obj1 = new Object1(commandData)
await obj1.func1;
await obj1.func2;
obj1.func3; //THIS IS THE ONLY REVIT INTERACTION
return Autodesk.Revit.UI.Result.Succeeded;
}
This doesn't work because I can not make the Execute function async but this is the theory. Not being able to await func1 and func2 is what is causing me to dive so deep into idling events. Func1 and Func2 have nothing to do with Revit.
Func1 calls a web server to do authentication in a webview
Func2 calls a web server to download Revit files
Func3 opens those files and uses that data to manipulate the current document.
Thank you all for the guidance
You forgot to explain that you are using 'await' functions.
Revit does not like async stuff, and your code looks odd. How can you add an await without an async method...
To work with async and Revit use the Revit.Async package, so the package gonna manage the Idling stuff.
Sorry, I just edited my reply for clarity. I am not actually running the functions with await as shown. That is how I would do it if the Execute function could be made async.
Func1 and Func2 are async functions which is why I am/was trying to run them in idling events.
One thing that is important is that when an IExternalCommand is running the Idling is not gonna trigger until the command ends. The Idling means the Revit is not busy doing something and is ready to receive some user interaction.
I guess the best approach in your case should be to use Revit.Async package with something like this.
public async Task Execute()
{
await func1;
await func2;
await RevitTask.RunAsync(func3); //THIS IS THE ONLY REVIT INTERACTION
}
Using the RevitTask you can run the func3 in Revit context.
One correct, reliable and effective approach to address the situation you describe is to implement an external event:
https://thebuildingcoder.typepad.com/blog/about-the-author.html#5.28
In the event handler, simply run the code to execute func3.
All the rest has nothing to do with Revit and does not require the Revit API.
Therefore, you can execute it independently, externally, and simply raise the external event after func1 and func2 have completed successfully.
Oh dear. I just noticed your statement:
> Func1 and Func2 are async functions which is why I am/was trying to run them in idling events.
Well, that is just about the opposite of what you could or should do. Or at least it is not intended, afaict.
Revit and its API is single threaded. The Idling event handler is single threaded. So, as long as that event handler is busy executing, e.g., your func1 and func2, it will block Revit from doing anything else.
The purpose of the Idling event is to enable you to execute some action as soon as Revit has stopped being occupied with and therefore blocked by other tasks.
This confirms my preceding answer: it seems to me that an appropriate way to handle this would be to disconnect func1 and func2 from Revit and your add-in as much as possible, and let them trigger an external event when they are completed, to execute func3.
So what I ended up doing is below. It seems to work but would love some feedback if its unsafe:
This still feels like the original idling action is unnecessary and I should be able to just dive right into the async func1, but other than that this works!
@jeremy_tammik How would you recommend disconnecting Func1 and Func2? The functions are async in that they use async functions in them but I don't need them to run truly asynchronously. Blocking Revit is fine for them. At all points in my code base, I am awaiting the results of the async functions so that everything runs essentially synchronously. All of my problems are due to the fact that the Revit execute function can not be made "async" so I can not "await" func1 and func2.
I can't imagine that this is the first time this problem has been encountered. It's a pretty standard workflow assuming that other plugins need some sort of authentication. First send an HTTP request to a server, then following a successful response, do all the Revit stuff.
Yes, this is a standard situation, and I described the standard solution above. No need for async anything, just create an external event X, raise the event when func1 and func2 have completed, and execute func3 in the external event X handler. No Idling needed, no async needed.
Oh maybe I need to learn more about how async works in C# if its different than JS development.
I would expect that if run func1 and func2 without the await flag, func2 will start before func1 has completed and the external event will be raised before func1 and func2 are completed. This would not work for my code since I would be trying to run code before the authentication step (func1) is completed.
Have I misunderstood? Thanks
In the context of the Revit. API, I would recommend scrapping async completely. That will totally simplify things.
haha I wish, but I still need to send requests to external servers. I can't think of a way to do that without async.
Well, I did myself, many times, connecting desktop and cloud:
https://github.com/jeremytammik/FireRatingCloud
Not very long ago, async did not exist. Push and pull functionality was up and running for decades before async was invented in the form you know it:
https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history
As @ricaun has mentioned, it seems to be a highly matched case that Revit.Async is designed to solve.
The first time I came up with the idea of Revit.Async was in a cloud rfa management system. The main use case was to query the server about the information of an rfa file (your function 1), then download it (your function 2) and finally place it with Revit API (your function 3). I used a pattern similar to Revit.Async to chain them up and it worked like a charm.
I would suggest you to take a look at Revit.Async and you will not be disappointed.
Many thanks to everybody for the interesting discussion and valuable contributions, especially to Kennan for implementing and sharing Revit.Async. I summarised the discussion for posterity on the blog:
https://thebuildingcoder.typepad.com/blog/2022/10/can-you-avoid-chaining-idling.html#6
Thank you all for the suggestions and help!
Thank you @Kennan.Chen, I have Revit.Async in my code base but I don't think I am using it most effectively. Can you explain what you used for your original use case (so the same structure I would use)?
The part that I can't connect is how to enter the async function from the main IExternalCommands Execute function. The below is what I would have thought but I wasn't able to get this to work before so I had to instead add the runMyFunctions() as an idling event handler and then it worked.
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
runMyFunctions();
return Autodesk.Revit.UI.Result.Succeeded;
}
public async void runMyFunctions()
{
await func1();
await func2();
await RevitTask.RunAsync(uiapp2 => {func3();});
}
Again, thank you all for the great help and discussion. Really appreciate everyones insight and it has helped me along immensely.
You need to call RevitTask.Initialize(app) in a valid Revit API context to get Revit.Async ready to use.
Some valid Revit API contexts are:
IExternalCommand.Execute method
IExternalApplication.OnStartup method
IExternalEventHandler.Execute method
Revit API event handlers
IUpdater
In your case, you can initialize Revit.Async in IExternalCommand.Execute method.