Warning : Some change in Revit API and Revit behaviour makes this article obsolete with newer version of Revit. I advise you to install pyRevitMEP extension and study updated code : FormExternalEventHandler.pushbutton. For usage in your script I recommend to use my CustomizableEvent from pyRevitMEP library.
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.
where should i input those scripts ? The RevitPythonShell can not import from revitutils import selection, uidoc, doc and from scriptutils.userinput import WPFWindow ?
As said in the title. It has been designed to be used in pyRevit and is part or pyRevitMEP extension : http://pythoncvc.net/?page_id=123
scriptutils is a pyRevit library.
I’m working in c#, but I just wanted to write you and tell you how refreshing it was to read a simple, concise example of how to do this. The sample codes on the forum and autodesk website (and the SDK) are horrendously overcomplicated. If it wasn’t for this post, I would have just given up. Thanks for posting this.
I totally agree with you on SDK samples etc… I struggled for while before getting there. Thanks for your comment.
You are legend Cyril.! I’ve been jumping backwards and forwards between the examples you mentioned at the very top of your post.. Couldn’t understand the whole thing at all.. Found your blog and this particular post. Following your example in RPS rather than in pyRevit it all worked. Magic 😉 thanks heaps!
Hi Anton, Thanks for your comment !
The modeless WPF Form in the pyrevitMEP extension always crashes Revit when used.
It is supposed to be fixed in you use last pyRevit and pyRevitMEP version. Please [open an issue on Github](https://github.com/CyrilWaechter/pyRevitMEP/issues/new?assignees=&labels=bug&template=bug_report.md&title=%5BBUG%5D) following the template.
Thank you for this very useful article!
I am trying to create a WPF which waits for an input from the WPF before executing the external event.
E.G a checkbox boolean determines external placement
Would you have a similar simple example of such a thing?
I have dug around the internet but have had no luck.
Thank you again.
You had a look at [3D rotate](https://youtu.be/60Y_DJbIL5Y) ?
There is multiple type of input inside.
Hi there. Very sorry to bother you.
I had a working implementation of this some years ago. I’ve recently come across the need for a modeless form, and it doesn’t seem to be working any longer with pyRevit scripts.
I stumbled upon this article, and your implementation is very close to mine, so I copied this over, and this also does not work (Revit 2020, pyRevit 4.7.6). As strange as it seems, the modeless window seems to lose the ability to reference the global variables declared at the module level.
I added a warning message on top of the article :
It works with pyRevit 4.7.6. If it doesn’t help, try to deactivate rocket mode and tell me if it works else, please open an issue on github.
Also for use in a pyRevit script. Do not forget to add
__persistentengine__ = Trueas done in samples.