I struggled for a while to make a modeless form. Why did I need it ? Because each time I was trying to get user to select object after WPF appear I was going out of Revit API thread and got this very common exception «Autodesk.Revit.Exceptions.InvalidOperationException: Attempting to create an ExternalEvent outside of a standard API execution». As Jeremy Tammik says :
One of the most frequently raised questions around the Revit API is how to drive Revit from outside, e.g., from a separate thread, a modeless dialogue, or a stand-alone executable.
I have read many examples on the subject. Most on them were in C#.
- Some post pointed out the ModelessDialog sample available in Revit SDK : it helps me to understand how it should behave but it was still a bit too complex for my C# knowledge.
- This Building Coder’s article showed me a very simple example
- This Daren Thomas article showed me an ironpython example but a bit too complex for me
- This Daren Thomas answer on stackoverflow helped me better understand the concept
- This AEC DevBlog article make me wonder «Is my code really working or I missed something and I was only lucky in situation I tested ?»
So I made a very simple form to make a very simple ExternalEventHandler sample as pyRevit script. It will help me and I hope it will help some hackers to struggle less than I did.
Let’s start with common import statement using built-in pyRevit utils :
# noinspection PyUnresolvedReferences from Autodesk.Revit.UI import IExternalEventHandler, ExternalEvent # noinspection PyUnresolvedReferences from Autodesk.Revit.DB import Transaction # noinspection PyUnresolvedReferences from Autodesk.Revit.Exceptions import InvalidOperationException from revitutils import selection, uidoc, doc from scriptutils.userinput import WPFWindow __doc__ = "A simple modeless form sample" __title__ = "Modeless Form" __author__ = "Cyril Waechter"
Then let’s write a simple function we want to execute modeless (here it just delete selected elements) :
# Simple function we want to run def delete_elements(): t = Transaction(doc, "Failing script") t.Start() for elid in uidoc.Selection.GetElementIds(): print elid doc.Delete(elid) t.Commit()
And now come the new magic thing that let you enter in a valid Revit API context. The «ExternalEvent» class with his «IExternalEventHandler» class :
# Create a subclass of IExternalEventHandler class SimpleEventHandler(IExternalEventHandler): """ Simple IExternalEventHandler sample """ # __init__ is used to make function from outside of the class to be executed by the handler. \ # Instructions could be simply written under Execute method only def __init__(self, do_this): self.do_this = do_this # Execute method run in Revit API environment. def Execute(self, uiapp): try: self.do_this() except InvalidOperationException: # If you don't catch this exeption Revit may crash. print "InvalidOperationException catched" def GetName(self): return "simple function executed by an IExternalEventHandler in a Form" # Now we need to make an instance of this handler. Moreover, it shows that the same class could be used to for # different functions using different handler class instances simple_event_handler = SimpleEventHandler(delete_elements) # We now need to create the ExternalEvent ext_event = ExternalEvent.Create(simple_event_handler)
Let’s do a simple form so easily created thanks to pyRevit in order to use our new toy :
# A simple WPF form used to call the ExternalEvent class ModelessForm(WPFWindow): """ Simple modeless form sample """ def __init__(self, xaml_file_name): WPFWindow.__init__(self, xaml_file_name) self.simple_text.Text = "Hello World" self.Show() def delete_click(self, sender, e): # This Raise() method launch a signal to Revit to tell him you want to do something in the API context ext_event.Raise() # Let's launch our beautiful and useful form ! modeless_form = ModelessForm("ModelessForm.xaml")
Here is the xaml code :
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Delete things:" Height="150" Width="300" ShowInTaskbar="False" Topmost="True" WindowStartupLocation="CenterScreen" ScrollViewer.VerticalScrollBarVisibility="Disabled" HorizontalContentAlignment="Center"> <StackPanel Margin="20" HorizontalAlignment="Stretch"> <TextBlock x:Name="simple_text" Text="" Grid.Column="0" HorizontalAlignment="Center" FontWeight="Bold"/> <Button Content="Delete selected elements" Height="30" Margin="10,10" Click="delete_click"/> </StackPanel> </Window>
Thanks a lot to all people mentioned in this article and linked article and stuffs.