Here are two important utilities, one eagerly awaited, the other a nice surprise: the updated REX and Structural analysis SDKs and a powerful async
and await
wrapper for the Revit API external event:
Last year, the Revit API discussion forum thread on an issue with SDK and Visual Studio pointed out that the Visual Studio templates provided with the REX SDK to make use of the Revit Structure Extensions had not yet been updated for Revit 2019.
That was fixed by the development team and an updated set of REX SDK Visual Studio templates for Revit Structure 2019 was shared in the forum and here on the blog.
The same situation arose again with Revit 2020, and here are the updated templates, samples and documentation for Revit 2020:
Response: The REX SDK installed and is now working!
I read all provided PDF documentation and did the Hello World exercise. I have some basic questions on how to get started with my specific projects and will create a separate post for that when I'm ready.
I haven't looked at the Structural Analysis SDK as we are going to link Revit with our own custom proprietary software. After reading about element mapping between databases and the content generator, I'm relieved to know that the REX SDK will likely reduce a lot of development time since those functionalities already exist.
Thank you for providing the updates! I'm glad to have begun my journey down this path.
Last month, we briefly looked at a
simple async
and await
external event wrapper.
Kennan Chen kindly reacted to that post in a comment, saying:
What a coincidence. I also implemented my own async/await external event wrapper these days. After some comparison with the one WhiteSharq provided, I found my implementation contains more useful functionality including wrapping async delegates and exposing core ability to define enhanced external events. It internally adopts an
ExternalEvent
creator, so the developers won't accidentally experience the context problems. Hope it can be helpful to the community.Please look at the Revit.Async GitHub repository showing how to use the task-based asynchronous pattern (TAP) to run Revit API code from any execution context. Also available via nuget.
Kennan's implementation does indeed look very complete and impressive with extensive documentation in both English and Chinese:
If you have ever encountered a Revit API exception saying, "Cannot execute Revit API outside of Revit API context", typically when you want to execute Revit API code from a modeless window, you may need this library to save your life.
A common solution for this exception is to wrap the Revit API code using IExternalEventHandler
and register the handler instance to Revit ahead of time to get a trigger (ExternalEvent
).
To execute the handler, just raise the trigger from anywhere to queue the handler to the Revit command loop.
But there comes another problem.
After raising the trigger, within the same context, you have no idea when the handler will be executed and it's not easy to get some result generated from that handler.
If you do want to make this happen, you have to manually yield the control back to the calling context.
This solution looks quite similar to the mechanism of "Promise" if you are familiar with JavaScript ES6.
Actually, we can achieve all the above logic by making use of task-based asynchronous pattern (TAP) which is generally known as Task<T>
in .NET.
By adopting Revit.Async, it is possible to run Revit API code from any context, because internally Revit.Async wraps your code automatically with IExternalEventHandler
and yields the return value to the calling context to make your invocation more natural.
If you are unfamiliar with the task-based asynchronous pattern (TAP), here is some useful material on it provided by Microsoft:
Here is a diagram comparing the Revit API external event mechanism with Revit.Async and screenshots of the two main parts:
Revit API external event
Revit.Async
[Transaction(TransactionMode.Manual)] public class MyRevitCommand : IExternalCommand { public static ExternalEvent SomeEvent { get; set; } public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements) { //Register MyExternalEventHandler ahead of time SomeEvent = ExternalEvent.Create(new MyExternalEventHandler()); var window = new MyWindow(); //Show modeless window window.Show(); return Result.Succeeded; } } public class MyExternalEventHandler : IExternalEventHandler { public void Execute(UIApplication app) { //Running some Revit API code here to handle the button click //It's complicated to accept argument from the calling context //and return value to the calling context var families = new FilteredElementCollector( app.ActiveUIDocument.Document) .OfType(typeof(Family)) .ToList(); //ignore some code } } public class MyWindow : Window { public MyWindow() { InitializeComponents(); } private void InitializeComponents() { Width = 200; Height = 100; WindowStartupLocation = WindowStartupLocation.CenterScreen; var button = new Button { Content = "Button", Command = new ButtonCommand(), VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center }; Content = button; } } public class ButtonCommand : ICommand { public bool CanExecute(object parameter) { return true; } public event EventHandler CanExecuteChanged; public void Execute(object parameter) { //Running Revit API code directly here will result in a //"Running Revit API outside of Revit API context" exception //Raise a predefined ExternalEvent instead MyRevitCommand.SomeEvent.Raise(); } }
[Transaction(TransactionMode.Manual)] public class MyRevitCommand : IExternalCommand { public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements) { //Always initialize RevitTask ahead of time within Revit API context RevitTask.Initialze(); var window = new MyWindow(); //Show modeless window window.Show(); return Result.Succeeded; } } public class MyWindow : Window { public MyWindow() { InitializeComponents(); } private void InitializeComponents() { Width = 200; Height = 100; WindowStartupLocation = WindowStartupLocation.CenterScreen; var button = new Button { Content = "Button", Command = new ButtonCommand(), CommandParameter = true, VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center }; Content = button; } } public class ButtonCommand : ICommand { public bool CanExecute(object parameter) { return true; } public event EventHandler CanExecuteChanged; public async void Execute(object parameter) { //.NET 4.5 supported keyword, use ContinueWith if using .NET 4.0 var families = await RevitTask.RunAsync( app => { //Run Revit API code here //Taking advantage of the closure created by the //lambda expression, we can make use of the argument //passed into the Execute method. //Let's assume it's a boolean indicating whether to // filter families that is editable if(parameter is bool editable) { return new FilteredElementCollector( app.ActiveUIDocument.Document) .OfType(typeof(Family)) .Cast() .Where(family => editable ? family.IsEditable : true) .ToList(); } return null; }); MessageBox.Show($"Family count: {families?.Count ?? 0}"); } }
Fed up with the weak IExternalEventHandler
interface?
Use the IGenericExternalEventHandler<TParameter,TResult>
interface instead.
It provides you with the ability to pass argument to a handler and receive result on complete.
It's always recommended to derive from the predefined abstract classes; they are designed to handle the argument passing and result returning part:
AsyncGenericExternalEventHandler<TParameter, TResult>
→ use to execute asynchronous logicSyncGenericExternalEventHandler<TParameter, TResult>
→ use to execute synchronize logic[Transaction(TransactionMode.Manual)] public class MyRevitCommand : IExternalCommand { public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements) { RevitTask.Initialize(); //Register SaveFamilyToDesktopExternalEventHandler ahead of time RevitTask.RegisterGlobal( new SaveFamilyToDesktopExternalEventHandler()); var window = new MyWindow(); //Show modeless window window.Show(); return Result.Succeeded; } } public class MyWindow : Window { public MyWindow() { InitializeComponents(); } private void InitializeComponents() { Width = 200; Height = 100; WindowStartupLocation = WindowStartupLocation.CenterScreen; var button = new Button { Content = "Save Random Family", Command = new ButtonCommand(), CommandParameter = true, VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center }; Content = button; } } public class ButtonCommand : ICommand { public bool CanExecute(object parameter) { return true; } public event EventHandler CanExecuteChanged; public async void Execute(object parameter) { var savePath = await RevitTask.RunAsync( async app => { try { var document = app.ActiveUIDocument.Document; var randomFamily = await RevitTask.RunAsync( () => { var families = new FilteredElementCollector(document) .OfClass(typeof(Family)) .Cast() .Where(family => family.IsEditable) .ToArray(); var random = new Random(Environment.TickCount); return families[random.Next(0, families.Length)]; }); //Raise your own handler return await RevitTask.RaiseGlobal< SaveFamilyToDesktopExternalEventHandler, Family, string>(randomFamily); } catch (Exception) { return null; } }); var saveResult = !string.IsNullOrWhiteSpace(savePath); MessageBox.Show( $"Family {(saveResult ? "" : "not ")}saved:\n{savePath}"); if (saveResult) { Process.Start(Path.GetDirectoryName(savePath)); } } } public class SaveFamilyToDesktopExternalEventHandler : SyncGenericExternalEventHandler { public override string GetName() { return "SaveFamilyToDesktopExternalEventHandler"; } protected override string Handle(UIApplication app, Family parameter) { //write sync logic here var document = parameter.Document; var familyDocument = document.EditFamily(parameter); var desktop = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); var path = Path.Combine(desktop, $"{parameter.Name}.rfa"); familyDocument.SaveAs(path, new SaveAsOptions {OverwriteExistingFile = true}); return path; } }
Impressive, isn't it?
Very many thanks to Kennan for this great implementation and explanation!
More details and full source in Kennan's Revit.Async GitHub repository.