XAML UI unresponsive until focus is switched to other window

Firstly, I am not sure if this is the correct place to ask this question as it might not be the fault of the Revit API, or pyRevit, but, it is in that context. Secondly, buckle up ‘cause this is going to require a lot of reading.

I have a modeless XAML UI with Ironpython code-behind. The purpose of the tool is to find orphaned & misplaced room tags and allow the user to fix them. This is what the UI looks like:

As you can see, the ListBoxes populate with any errant room tags in the model. The user can then click on one of these elements and ShowElements() is called on it.

Everything works fine for the most part, except when ShowElements() is called on a room tag that is not in the active view. Normally, Revit displays a popup dialog saying “There is no open view that shows any of the highlighted elements. Searching through the closed views to find a good view could take a long time. Continue?”

The user clicks “Ok” to show the element, then the Revit UI and modeless window both become totally unresponsive. In this state, I cannot even close or drag the windows. I click aggravatedly many times all over the Revit window and nothing happens. I click all over the modeless window and nothing happens. Until…

I click back into the ListBox. The ListBox seems to reacquire focus and the Revit UI and modeless window start functioning again. After experimentation, I find that function is also restored when switching focus to any other window besides Revit or my modeless window.

At this point, I am fairly sure that the Revit pop-up dialog is triggering the loss of function because I do not experience the problem when calling ShowElements() on an element that is already visible in the active view. In those cases, no dialog pops up and everything functions perfectly.

To solve this, I attempted to refocus the modeless XAML window using this code after the dialog:

ctypes.windll.user32.SetForegroundWindow(self.as_foreground_window)

This appears to correctly bring the window to the foreground, which I can tell because the window title is no longer grayed-out, but appears black, like the window is activated. Both windows are still unresponsive though, so I figure setting the window to foreground is not a useful solution.

Next, I tried to programmatically dismiss the dialog with an event handler. After researching how to do this, I was quite successful. I am effectively suppressing the dialog, but still, the UI is unresponsive until switching focus. Below is how I call ShowElements() and dismiss the dialog:

def show_element(element):
	if element :
		global uidoc
		dialogEventHandler = WinEventHandler[DialogBoxShowingEventArgs](dismiss_dialog)
		uiapp.DialogBoxShowing += dialogEventHandler
		uidoc.ShowElements(element)
		uiapp.DialogBoxShowing -= dialogEventHandler

If you look at the modeless UI image above, you will see the “Next Tag” and “Previous Tag” buttons. These call ShowElements() on the next tag in the list using the same methods, but the loss of responsiveness does not occur when clicking them. This further leads me to believe the issue is related to the ListBox specifically and its interaction with Revit’s pop-up dialog. Here are the methods that are called by external events triggered by the XAML UI when the ListBox selection gets changed and when it gets focus:

		def listbox_entire_model_selection_change(self):
			button_name = 'listbox_entire_model_selection_change'
			selected_index = self.listbox_entire_model.SelectedIndex
			if selected_index > -1:
				self.current_tag = self.display.model[selected_index].elem
				if len(self.display.model) > 0:
					show_element(self.current_tag)
				self.refresh_listboxes_and_quantities()
				self.current_tag_index = selected_index
				p('{} succeeded'.format(button_name))
			else: p('selected_index not > -1. Likely no list items in listbox.')
		
		def listbox_entire_model_got_focus(self):
			self.listbox_entire_model_selection_change()

I have also tried using the Focus() and Activate() methods to refocus the modeless window, but these have seemingly done nothing. I know that when I switched focus manually, function returned, so I tried programmatically switching focus. I told the window to focus a different ListBox, then re-focus on the original ListBox. This restores function, but since I have an event triggering when the ListBox gets focus, the modeless window falls into an endless loop of repeatedly switching focus over and over. I suppose I could try to find some hacky way of programmatically switching focus to some other window to restore function, but I figured I would ask here first to see if anybody knows what’s actually causing the issue.

Feel free to ask for more context, but the code-behind is 614 lines, so I figure it would be rude to post the entire thing. Thanks in advance for any advice!

Apologies in advance (this unfortunately isn’t an attempt at an answer) since I have very little experience with the sort of stuff you’re trying but it seemed really cool what you’re working on and wanted to learn more.

I’m assuming “modeless XAML UI” refers to the entire box shown and ListBox is a component in that UI. But are you using a PyRevit Listbox or something from .NET forms?

I don’t have much experience in .NET and PyRevit forms but I’d love to try it out and repro it (if you have a small code example + small Revit project) (maybe via DM if you don’t want to share more publicly) - but totally understand if that’d be difficult! I’m not sure how state management works in your XAML code (more familiar with web frameworks) but there is probably some way set state explicitly when you’re manually triggering the hacky refocus so that the second event does not trigger.

Good luck anyway, and I’m excited to follow along this thread!

Thanks for the interest in the thread!

I thought it would be difficult to create a minimum reproducible example, but it didn’t take that long. Unfortunately, it’s still 351 lines. There are some interesting things you can learn from it, such as how to make a modeless UI for pyRevit using XAML, how to drive it with Ironpython codebehind, how to dismiss dialogs programmatically, etc. It may not be the cleanest code you’ve ever seen, though, and there may be some vestigial code that no longer applies to this minimum example.

It consists of 3 files, the first being the bundle YAML file. I think the only crucial part is setting the engine to persistent. If you do not set it to persistent, the modeless window will crash Revit, if I remember correctly. Normally, you can set the engine to persistent within your script, but I remember finding out that it only worked when set in the bundle file:

engine:
  persistent: true

title: "Fix Room\nTags"

tooltip: Finds errant room tags and allows the user to fix them.

author: Frank Loftus

Then, the Ironpython codebehind (.py file) containing the event handlers and API interactions:

# dependencies
import clr
clr.AddReference('System.Windows.Forms')
clr.AddReference('IronPython.Wpf')

# find the path of ui.xaml
from pyrevit import UI
from pyrevit import script
xamlfile = script.get_bundle_file('ui.xaml')

# import WPF creator and base Window
import wpf
from System import Windows
from System import EventHandler as WinEventHandler

import sys

import ctypes

clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import ViewType, Transaction, ElementId, FilteredElementCollector, BuiltInCategory, BuiltInParameter, Element, RevitLinkInstance
from Autodesk.Revit.DB.Architecture import RoomTag, Room
from Autodesk.Revit.UI import UIDocument, Selection, TaskDialog, TaskDialogResult, IExternalEventHandler, ExternalEvent
from Autodesk.Revit.UI.Events import DialogBoxShowingEventArgs, TaskDialogShowingEventArgs
from Autodesk.Revit.UI.UIDocument import GetOpenUIViews, ActiveView, ShowElements
from Autodesk.Revit.Exceptions import InvalidOperationException

from pyrevit import revit
from pyrevit.forms import WPFWindow

from System.Collections.Generic import List

# Global variables____________________________________
script_name = 'Fix Room Tags'
GLOBAL_DEBUG = True

def p(*to_print): # Toggleable print function. Turn on to debug.
	if GLOBAL_DEBUG:
		for item in to_print:
			print item

#______________SETUP DOC_____________________
uiapp = __revit__
uidoc = __revit__.ActiveUIDocument
doc = __revit__.ActiveUIDocument.Document

def dismiss_dialog (sender, event_args):
	id_ok = 1
	try:
		p('type(event_args):', type(event_args))
		if isinstance(event_args, TaskDialogShowingEventArgs):
			p('dismissing dialog {}. Cancellable: {}'.format(event_args.DialogId, event_args.Cancellable))
			result = event_args.OverrideResult(id_ok)
			p('dismissed:', result)
	except Exception as ex:
		import sys
		print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(ex).__name__, ex)
		print(sys.exc_info()[-1].tb_next.tb_lineno)
		print(sys.exc_info()[-1].tb_next.tb_next.tb_lineno)
		print(sys.exc_info()[-1].tb_next.tb_next.tb_next.tb_lineno)
		print(sys.exc_info()[-1].tb_next.tb_next.tb_next.tb_next.tb_lineno)
		print(sys.exc_info()[-1].tb_next.tb_next.tb_next.tb_next.tb_next.tb_lineno)

def show_element(element):
	if element :
		global uidoc
		dialogEventHandler = WinEventHandler[DialogBoxShowingEventArgs](dismiss_dialog)
		uiapp.DialogBoxShowing += dialogEventHandler
		uidoc.ShowElements(element)
		uiapp.DialogBoxShowing -= dialogEventHandler

# Create a subclass of IExternalEventHandler
class EventHandler(IExternalEventHandler):
	def __init__(self, func_to_run):
		self.func = func_to_run
	
	def Execute(self, uiapp):
		try:
			self.func()
		except Exception as ex:
			import sys
			# Hacky way to catch exceptions within Execute() and print them for debugging:
			print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(ex).__name__, ex)
			print(sys.exc_info()[-1].tb_next.tb_lineno)
			print(sys.exc_info()[-1].tb_next.tb_next.tb_lineno)
			print(sys.exc_info()[-1].tb_next.tb_next.tb_next.tb_lineno)
			print(sys.exc_info()[-1].tb_next.tb_next.tb_next.tb_next.tb_lineno)
			print(sys.exc_info()[-1].tb_next.tb_next.tb_next.tb_next.tb_next.tb_lineno)

	def GetName(self):
		return "simple function executed by an IExternalEventHandler in a Form"


class ListItem:
	def __init__(self, elem):
		# Initial properties
		self.elem            = elem
		self.elem_id         = elem.Id
		self.elem_id_str     = self.elem_id.ToString()
		self.status          = self.status
		self.owner_view_id   = elem.OwnerViewId
		self.owner_view      = doc.GetElement(self.owner_view_id)
		self.owner_view_name = self.owner_view.Name
		self.room_number     = self.room.Number if self.room else ''
		self.room_name       = self.room.get_Parameter(BuiltInParameter.ROOM_NAME).AsString() if self.room else 'No Room'
		
	@property
	def status(self):
		if self.elem.IsOrphaned:
			return '(Orphaned)'
		elif not self.elem.IsInRoom:
			return '(Misplaced)'
		else:
			return False
	
	@property
	def room(self):
		if not self.elem.IsOrphaned:
			if self.elem.IsTaggingLink:
				linked_element_id         = self.elem.TaggedRoomId.LinkedElementId
				link_instance_id          = self.elem.TaggedRoomId.LinkInstanceId
				link_instance             = doc.GetElement(link_instance_id)
				link_doc                  = link_instance.GetLinkDocument()
				linked_room               = link_doc.GetElement(linked_element_id)
				return linked_room
			else:
				tagged_local_room = doc.GetElement(self.elem.TaggedLocalRoomId)
				return tagged_local_room
		else: p('tag is orphaned. cannot find tagged room.')
	
	@property
	def display_string(self):
		return '{}, {}, {} {}, View: {}'.format(self.status, self.elem_id_str, self.room_name, self.room_number, self.owner_view_name)


class Display:
	def __init__(self, form, model=None, view=None):
		self.form = form
		self.model = model
		self.view = view
	
	def refresh_model(self):
		self.model = self.form.list_items_in_model
		self.form.listbox_entire_model.ItemsSource = [list_item.display_string for list_item in self.model]
	
	def set_model(self, list_items):
		self.model = list_items
		self.form.listbox_entire_model.ItemsSource = [list_item.display_string for list_item in list_items]
	
try:
	# WPF form used to call the ExternalEvents
	class ModelessForm(WPFWindow):
		def __init__(self, xaml_file_name):
			WPFWindow.__init__(self, xaml_file_name)
			
			self.current_tag_index      = 0
			self.recentered_list_item   = None
			self.recentered_list_items  = None
			self.current_tag            = None
			self.display                = Display(self)
			
			# Create instance of handler, then instance of event for each button/command
			self.listbox_entire_model_selection_change_handler = EventHandler(self.listbox_entire_model_selection_change)
			self.listbox_entire_model_selection_change_event_instance = ExternalEvent.Create(self.listbox_entire_model_selection_change_handler)
			
			self.listbox_entire_model_got_focus_handler = EventHandler(self.listbox_entire_model_got_focus)
			self.listbox_entire_model_got_focus_event_instance = ExternalEvent.Create(self.listbox_entire_model_got_focus_handler)

			self.next_tag_handler = EventHandler(self.next_tag)
			self.next_tag_event_instance = ExternalEvent.Create(self.next_tag_handler)

			self.previous_tag_handler = EventHandler(self.previous_tag)
			self.previous_tag_event_instance = ExternalEvent.Create(self.previous_tag_handler)
			
			# Populate ListBox with RoomTags from model
			self.display.refresh_model()
			
			# Populate item quantities
			self.textblock_quantity_in_model.Text = self.quantity_in_model_str
			
			# Show the modeless window
			self.show_self()
		
		
		# Properties
		@property
		def active_ui_view(self):
			return uidoc.ActiveView
		
		@property
		def room_tags_in_model(self):
			result = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_RoomTags).WhereElementIsNotElementType()
			p('room_tags_in_model:', result)
			if result: return result
			else: p('No room tags in model.')
		
		@property
		def room_tag_owner_view_ids_in_model_set(self):
			if self.room_tags_in_model:
				result = set([tag.OwnerViewId for tag in self.room_tags_in_model])
				p('room_tag_owner_view_ids_in_model_set:', result)
				return result
			else: p('No room tags in model. Cannot make room_tag_owner_view_ids_in_model_set.')
		
		@property
		def room_tags_visible_in_model(self):
			room_tags_visible_in_model = List[Element]()
			if self.room_tag_owner_view_ids_in_model_set:
				for owner_view_id in self.room_tag_owner_view_ids_in_model_set:
					room_tags_visible_in_view = FilteredElementCollector(doc, owner_view_id).OfCategory(BuiltInCategory.OST_RoomTags).WhereElementIsNotElementType().ToElements()
					room_tags_visible_in_model.AddRange(room_tags_visible_in_view)
				p('room_tags_visible_in_model:', [tag.Id.ToString() for tag in room_tags_visible_in_model])
				return room_tags_visible_in_model
			else: p('No room_tag_owner_view_ids_in_model_set. Cannot make room_tags_visible_in_model')
		
		@property
		def orphaned_room_tags_in_model(self):
			result = [room_tag for room_tag in self.room_tags_in_model if room_tag.IsOrphaned]
			p('orphaned_room_tags_in_model:', result)
			return result
		
		@property
		def misplaced_room_tags_in_model(self):
			result = [room_tag for room_tag in self.room_tags_in_model if not room_tag.IsInRoom]
			p('misplaced_room_tags_in_model:', result)
			return result
		
		@property
		def orphaned_or_misplaced_room_tags_in_model(self):
			result = [room_tag for room_tag in self.room_tags_in_model if room_tag.IsOrphaned or not room_tag.IsInRoom]
			p('orphaned_or_misplaced_room_tags_in_model:', [elem.Id for elem in result])
			return result
		
		@property
		def list_items_in_model(self):
			result = [ListItem(room_tag) for room_tag in self.room_tags_in_model if room_tag.IsOrphaned or not room_tag.IsInRoom]
			p('orphaned_or_misplaced_room_tags_in_model:', [list_item.elem.Id for list_item in result])
			return result
		
		@property
		def display_strings_in_model(self):
			return [list_item.display_string for list_item in self.display.model]
		
		@property
		def quantity_in_model(self):
			result = len(self.orphaned_or_misplaced_room_tags_in_model)
			p('quantity_in_model:', result)
			return result
		
		@property
		def quantity_in_model_str(self):
			return str(self.quantity_in_model)
		
		@property
		def tag_ids_in_model(self):
			result = [tag.Id for tag in self.orphaned_or_misplaced_room_tags_in_model]
			p('tag_ids_in_model:', result)
			return result
		
		# Methods
		def regain_focus(self): #### useless?
			ctypes.windll.user32.SetForegroundWindow(self.as_foreground_window)
		
		def show_self(self):
			self.Show()
			# Set self.as_foreground_window, which can be used to place the modeless window in the foreground:
			self.as_foreground_window = ctypes.windll.user32.GetForegroundWindow()
			p('foreground window:', self.as_foreground_window)
		
		
		# Functions to run with external event
		def listbox_entire_model_selection_change(self):
			button_name = 'listbox_entire_model_selection_change'
			selected_index = self.listbox_entire_model.SelectedIndex
			if selected_index > -1:
				self.current_tag = self.display.model[selected_index].elem
				if len(self.display.model) > 0:
					show_element(self.current_tag)
				self.refresh_listboxes_and_quantities()
				self.current_tag_index = selected_index
				p('{} succeeded'.format(button_name))
			else: p('selected_index not > -1. Likely no list items in listbox.')
		
		def listbox_entire_model_got_focus(self):
			self.listbox_entire_model_selection_change()
		
		def refresh_listboxes(self):
			self.display.refresh_model()
		
		def refresh_quantities(self):
			self.textblock_quantity_in_model.Text = self.quantity_in_model_str
		
		def refresh_listboxes_and_quantities(self):
			self.refresh_listboxes()
			self.refresh_quantities()
		
		def next_tag(self):
			button_name = 'next_tag'
			if self.current_tag_index < len(self.display.model) - 1:
				self.current_tag_index += 1
				p('Current Tag Index: ', self.current_tag_index)
				show_element(self.display.model[self.current_tag_index].elem)
				self.refresh_listboxes_and_quantities()
				p('{} succeeded'.format(button_name))
			else: p('There is no next tag to show.')

		def previous_tag(self):
			button_name = 'previous_tag'
			if self.current_tag_index >= 1 < len(self.display.model):
				self.current_tag_index -= 1
				p('Current Tag Index: ', self.current_tag_index)
				show_element(self.display.model[self.current_tag_index].elem)
				self.refresh_listboxes_and_quantities()
				p('{} succeeded'.format(button_name))
			else: p('There is no previous tag to show.')
	
		
		# Events to call from xaml UI
		def close_window(self, sender, args):
			self.Close()

		def listbox_entire_model_selection_change_event(self, sender, args):
			self.listbox_entire_model_selection_change_event_instance.Raise()
		
		def listbox_entire_model_got_focus_event(self, sender, args):
			self.listbox_entire_model_got_focus_event_instance.Raise()

		def next_tag_event(self, sender, args):
			self.next_tag_event_instance.Raise()

		def previous_tag_event(self, sender, args):
			self.previous_tag_event_instance.Raise()

	
	# Instantiate the form
	modeless_form = ModelessForm("ui.xaml")

except Exception as ex:
	import sys
	# Hacky way to catch exceptions and print them for debugging:
	print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(ex).__name__, ex)
	print(sys.exc_info()[-1].tb_next.tb_lineno)
	print(sys.exc_info()[-1].tb_next.tb_next.tb_lineno)
	print(sys.exc_info()[-1].tb_next.tb_next.tb_next.tb_lineno)
	print(sys.exc_info()[-1].tb_next.tb_next.tb_next.tb_next.tb_lineno)
	print(sys.exc_info()[-1].tb_next.tb_next.tb_next.tb_next.tb_next.tb_lineno)

Lastly, the XAML file created with the help of Visual Studio. I am using a WPF window rather than pyRevit forms. The events which are defined in the codebehind get called from this XAML file:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		xmlns:av="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:Collections="clr-namespace:System.Collections;assembly=mscorlib" mc:Ignorable="av"
		Title="Fix Room Tags" ResizeMode="NoResize" SizeToContent="WidthAndHeight" Background="WhiteSmoke" BorderBrush="#FF89AF98" Topmost="True">
	<StackPanel x:Name="main_stackpanel" Margin="10">
		<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
			<StackPanel Orientation="Vertical" Margin="0,0,10,0">
                <StackPanel Orientation="Horizontal" Margin="0,0,0,0">
					<TextBlock Text="Errant tags in entire model:" Padding="1,0,5,0" Margin="0,0,0,5" />
                    <TextBlock x:Name="textblock_quantity_in_model" Text="#" Padding="5,0,5,0" Margin="0,0,0,5" />
                </StackPanel>
                <ListBox x:Name="listbox_entire_model" GotFocus="listbox_entire_model_got_focus_event" SelectionChanged="listbox_entire_model_selection_change_event" Margin="0,0,0,10" Height="100" />
            </StackPanel>
			<StackPanel Orientation="Vertical">
                <StackPanel Orientation="Horizontal" Margin="0,0,0,0"/>
            </StackPanel>
		</StackPanel>
		<StackPanel Orientation="Horizontal" Margin="0,10,0,0" HorizontalAlignment="Right">
            <Button x:Name="previous_tag_button" Content="Previous Tag" Height="35" Click="previous_tag_event" Background="#FF89AF98" Foreground="#FF071E42" FontSize="20" BorderThickness="0,0,0,4" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="0,0,10,0" Padding="5,1,5,1" VerticalContentAlignment="Top" >
				<Button.BorderBrush>
					<SolidColorBrush Color="#FF609274" Opacity="1"/>
				</Button.BorderBrush>
			</Button>
            <Button x:Name="next_tag_button" Content="Next Tag" Height="35" Click="next_tag_event" Background="#FF89AF98" Foreground="#FF071E42" FontSize="20" BorderThickness="0,0,0,4" VerticalAlignment="Bottom" HorizontalAlignment="Right" Padding="5,1,5,1" VerticalContentAlignment="Top" >
				<Button.BorderBrush>
					<SolidColorBrush Color="#FF609274" Opacity="1"/>
				</Button.BorderBrush>
			</Button>
		</StackPanel>
	</StackPanel>
</Window>

Throw these 3 files into a pyRevit pushbutton and you should be able to test it out. I work in MEP, so typically the rooms are from a linked architectural model and I tag them in the MEP host model. When an architect deletes a room, the tag becomes orphaned and shows a question mark.

To test this yourself, create a simple model with some rooms to represent the architectural model. Then link it into a second model which represents the MEP model. Tag the rooms in the MEP model and save. Then open the architectural model, delete some rooms, then save it. Reload the link in the MEP model. When the command is run, these orphaned tags will populate into the ListBox. Clicking on the ListBox items will show the selected tag, allowing the user to make a decision about how to fix it.

Things to test:

Notice that clicking on a room tag in a closed view opens the view, shows the tag, then produces the unresponsive UI error, which affects both Revit and the modeless window. When showing a tag in an already open view, the UI does not become unresponsive.

In this unresponsive state, Revit and the modeless window cannot be interacted with, moved, or closed until focus is switched to some other window. Responsiveness also returns when clicking back in the ListBox, or using the previous/next tag buttons. Therefore, I believe the issue is triggered by some interaction between the ListBox and the popup dialog from Revit (even though the dialog is being dismissed) since the previous/next tag buttons do not cause unresponsiveness.

Like I said, there may be a way to regain responsiveness by programmatically switching focus to some other window, then switching back, but that would be kind of hacky, so I would prefer to do it more correctly. If anyone out there knows a lot about WPF, I would appreciate some advice on how to manage focus between UI elements, child windows, and main windows. Thanks to all for reading!

Hi @Frank_L
I still don’t have an answer for you, but I have some unsolicited advice:

  • you’re wrapping and entire class in a try/except block, but it’s not a good practice. You should usually enclose the bare minimum of lines that you expect to raise an exception. In your case, just wrapping the modeless_form creation is enough
  • speaking of exceptions, in the except your try8ng to emulate what oyrevit already does by itself, outputting the exception and the stack trace on the pyrevit output window. You can simplify your code by removing the try/except blocks and let pyrevit handle them for you
  • pyrevit also imports many things for you, you don’t need to clr.AddReference and import from Autodesk… check the quick start guide for more info
  • also, from pyrevit import revit gives you doc = revit.doc
  • if you use the pyrevit logger, you can then do logger l.debug instead of printing and having to handle the GLOBAL_DEBUG variable; debug messages are shown when you Ctrl+click the script button

Thank you @sanzoghenzo for offering advice.

Allow me to respond to some of your comments:

you’re wrapping and entire class in a try/except block

Yes, I had some reason for doing this earlier in development, but then I never cleaned it up. Thanks for pointing that out.

your try8ng to emulate what oyrevit already does by itself, outputting the exception and the stack trace on the pyrevit output window.

I find that pyRevit does not always output the complete stack trace, especially for the Execute() method of IUpdaters and IExternalEventHandlers. I don’t understand the details, but outputting the stack trace that way is the only way I know how to see the correct line number of the exceptions inside Execute(). Otherwise pyRevit seems only to output the line where it was called from earlier in the stack.

pyrevit also imports many things for you, you don’t need to clr.AddReference and import from Autodesk… check the quick start guide for more info

In the quick start guide, I think it says “many objects are already loaded.” I tried commenting out my references and just importing pyrevit.revit. It seems that not every API reference I need is included. Since I don’t know what is imported, I guess I prefer to just import everything individually. I go back and forth between pyRevit and Dynamo, so it helps me to remember the API better if I explicitly import each individual object. In that case, I should remove the reference to pyrevit.revit. (I’m not sure if that’s valid.)

if you use the pyrevit logger, you can then do logger l.debug instead of printing and having to handle the GLOBAL_DEBUG variable; debug messages are shown when you Ctrl+click the script button

I know pyRevit has a logger, but showing debug messages with Ctrl+click shows a TON of debug messages that have nothing to do with my code, so I opted not to use it because it takes a significant amount of time to print all that. It’s good to keep in mind for the future, though, if I need to use it.

Thanks again for your comments!

1 Like

Well, I still have not solved the original issue, but I found what should have been an obvious workaround. The pop-up dialog that triggers the unresponsiveness can be avoided by opening the element’s owner view before showing the element. That way, there is no need to dismiss the dialog in the first place.

For anyone following who wants to use this code, here are the files:

bundle.yaml file:

engine:
  persistent: true

title: "Fix Room\nTags"

tooltip: Finds errant room tags and allows the user to fix them.

author: Frank Loftus

script.py file:

# dependencies
import clr
clr.AddReference('System.Windows.Forms')
clr.AddReference('IronPython.Wpf')

# Import from pyrevit
from pyrevit import UI
from pyrevit import script
from pyrevit.forms import WPFWindow
from pyrevit.revit import Transaction as pyrevit_transaction
##from pyrevit.revit import doc # Alternative way to access doc through pyrevit
xamlfile = script.get_bundle_file('ui.xaml') # find the path of ui.xaml

# import WPF creator and base Window
import wpf
from System import Windows
from System import EventHandler as WinEventHandler

# import python system module
import sys

'''
# Import ctypes for access to SetForegroundWindow(). No longer used.
import ctypes
'''

# Import Revit API objects
clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import ViewType, Transaction, ElementId, FilteredElementCollector, BuiltInCategory, BuiltInParameter, Element, RevitLinkInstance
from Autodesk.Revit.DB.Architecture import RoomTag, Room
from Autodesk.Revit.UI import UIDocument, Selection, TaskDialog, TaskDialogResult, IExternalEventHandler, ExternalEvent
from Autodesk.Revit.UI.Events import DialogBoxShowingEventArgs, TaskDialogShowingEventArgs
from Autodesk.Revit.UI.UIDocument import GetOpenUIViews, ActiveView, ShowElements
from Autodesk.Revit.Exceptions import InvalidOperationException

# Import .NET List
from System.Collections.Generic import List

# Global variables
script_name = 'Fix Room Tags'
GLOBAL_DEBUG = False

# Toggleable print function. Turn on to debug.
def p(*to_print):
	if GLOBAL_DEBUG:
		for item in to_print:
			print item

# Setup doc
uiapp = __revit__
uidoc = __revit__.ActiveUIDocument
doc = __revit__.ActiveUIDocument.Document

def dismiss_dialog (sender, event_args): #### No longer used.
	id_ok = 1
	try:
		p('type(event_args):', type(event_args))
		if isinstance(event_args, TaskDialogShowingEventArgs):
			p('dismissing dialog {}. Cancellable: {}'.format(event_args.DialogId, event_args.Cancellable))
			result = event_args.OverrideResult(id_ok)
			p('dismissed:', result)
	except Exception as ex:
		# Hacky way to catch and print exceptions within:
		print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(ex).__name__, ex)
		print(sys.exc_info()[-1].tb_next.tb_lineno)
		print(sys.exc_info()[-1].tb_next.tb_next.tb_lineno)
		print(sys.exc_info()[-1].tb_next.tb_next.tb_next.tb_lineno)
		print(sys.exc_info()[-1].tb_next.tb_next.tb_next.tb_next.tb_lineno)
		print(sys.exc_info()[-1].tb_next.tb_next.tb_next.tb_next.tb_next.tb_lineno)

def show_element_OBSOLETE(element): #### No longer used
	if element:
		global uidoc
		# Use a windows EventHandler to dismiss the pop-up dialog upon ShowElements()
		dialog_event_handler = WinEventHandler[DialogBoxShowingEventArgs](dismiss_dialog)
		uiapp.DialogBoxShowing += dialog_event_handler
		uidoc.ShowElements(element)
		uiapp.DialogBoxShowing -= dialog_event_handler

def show_element(element):
	if element:
		global uidoc
		# Acquire tag's OwnerView and open it before ShowElements() to avoid pop-up dialog
		active_view = uidoc.ActiveView
		owner_view_id = element.OwnerViewId
		if active_view.Id != owner_view_id:
			owner_view = doc.GetElement(owner_view_id)
			uidoc.ActiveView = owner_view
		uidoc.ShowElements(element)

# Create a subclass of IExternalEventHandler
class EventHandler(IExternalEventHandler):
	def __init__(self, func_to_run):
		self.func = func_to_run
	
	def Execute(self, uiapp):
		try:
			self.func()
		except Exception as ex:
			# Hacky way to catch and print exceptions within Execute():
			print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(ex).__name__, ex)
			print(sys.exc_info()[-1].tb_next.tb_lineno)
			print(sys.exc_info()[-1].tb_next.tb_next.tb_lineno)
			print(sys.exc_info()[-1].tb_next.tb_next.tb_next.tb_lineno)
			print(sys.exc_info()[-1].tb_next.tb_next.tb_next.tb_next.tb_lineno)
			print(sys.exc_info()[-1].tb_next.tb_next.tb_next.tb_next.tb_next.tb_lineno)

	def GetName(self):
		return "simple function executed by an IExternalEventHandler in a Form"


class ListItem:
	def __init__(self, elem):
		# Initial properties
		self.elem            = elem
		self.elem_id         = elem.Id
		self.elem_id_str     = self.elem_id.ToString()
		self.status          = self.status
		self.owner_view_id   = elem.OwnerViewId
		self.owner_view      = doc.GetElement(self.owner_view_id)
		self.owner_view_name = self.owner_view.Name
		self.room_number     = self.room.Number if self.room else ''
		self.room_name       = self.room.get_Parameter(BuiltInParameter.ROOM_NAME).AsString() if self.room else 'No Room'
		
	@property
	def status(self):
		if self.elem.IsOrphaned:
			return '(Orphaned)'
		elif not self.elem.IsInRoom:
			return '(Misplaced)'
		else:
			return False
	
	@property
	def room(self):
		if not self.elem.IsOrphaned:
			if self.elem.IsTaggingLink:
				linked_element_id         = self.elem.TaggedRoomId.LinkedElementId
				link_instance_id          = self.elem.TaggedRoomId.LinkInstanceId
				link_instance             = doc.GetElement(link_instance_id)
				link_doc                  = link_instance.GetLinkDocument()
				linked_room               = link_doc.GetElement(linked_element_id)
				return linked_room
			else:
				tagged_local_room = doc.GetElement(self.elem.TaggedLocalRoomId)
				return tagged_local_room
		else: p('tag is orphaned. cannot find tagged room.')
	
	@property
	def display_string(self):
		return '{}, {}, {} {}, View: {}'.format(self.status, self.elem_id_str, self.room_name, self.room_number, self.owner_view_name)


class Display:
	def __init__(self, form, model=None, view=None):
		self.form = form
		self.model = model
		self.view = view
	
	def refresh_model(self):
		self.model = self.form.list_items_in_model
		self.form.listbox_entire_model.ItemsSource = [list_item.display_string for list_item in self.model]
	
	def refresh_view(self):
		self.view = self.form.list_items_in_view
		self.form.listbox_current_view.ItemsSource = [list_item.display_string for list_item in self.view]
	
	def set_model(self, list_items):
		self.model = list_items
		self.form.listbox_entire_model.ItemsSource = [list_item.display_string for list_item in list_items]
	
	def set_view(self, list_items):
		self.view = list_items
		self.form.listbox_current_view.ItemsSource = [list_item.display_string for list_item in list_items]

# WPF form used to call the ExternalEvents
class ModelessForm(WPFWindow):
	def __init__(self, xaml_file_name):
		WPFWindow.__init__(self, xaml_file_name)
		
		self.current_tag_index      = 0
		self.recentered_list_item   = None
		self.recentered_list_items  = None
		self.current_tag            = None
		self.display                = Display(self)
		
		# Create instance of handler, then instance of event for each button/command
		self.refresh_window_handler = EventHandler(self.refresh_window)
		self.refresh_window_event_instance = ExternalEvent.Create(self.refresh_window_handler)

		self.listbox_current_view_selection_change_handler = EventHandler(self.listbox_current_view_selection_change)
		self.listbox_current_view_selection_change_event_instance = ExternalEvent.Create(self.listbox_current_view_selection_change_handler)
		
		self.listbox_current_view_got_focus_handler = EventHandler(self.listbox_current_view_got_focus)
		self.listbox_current_view_got_focus_event_instance = ExternalEvent.Create(self.listbox_current_view_got_focus_handler)

		self.recenter_selected_tag_handler = EventHandler(self.recenter_selected_tag)
		self.recenter_selected_tag_event_instance = ExternalEvent.Create(self.recenter_selected_tag_handler)

		self.delete_selected_tag_handler = EventHandler(self.delete_selected_tag)
		self.delete_selected_tag_event_instance = ExternalEvent.Create(self.delete_selected_tag_handler)

		self.listbox_entire_model_selection_change_handler = EventHandler(self.listbox_entire_model_selection_change)
		self.listbox_entire_model_selection_change_event_instance = ExternalEvent.Create(self.listbox_entire_model_selection_change_handler)
		
		self.listbox_entire_model_got_focus_handler = EventHandler(self.listbox_entire_model_got_focus)
		self.listbox_entire_model_got_focus_event_instance = ExternalEvent.Create(self.listbox_entire_model_got_focus_handler)

		self.recenter_all_tags_in_view_handler = EventHandler(self.recenter_all_tags_in_view)
		self.recenter_all_tags_in_view_event_instance = ExternalEvent.Create(self.recenter_all_tags_in_view_handler)

		self.delete_all_orphans_in_view_handler = EventHandler(self.delete_all_orphans_in_view)
		self.delete_all_orphans_in_view_event_instance = ExternalEvent.Create(self.delete_all_orphans_in_view_handler)

		self.delete_room_tags_not_visible_in_any_view_handler = EventHandler(self.delete_room_tags_not_visible_in_any_view)
		self.delete_room_tags_not_visible_in_any_view_event_instance = ExternalEvent.Create(self.delete_room_tags_not_visible_in_any_view_handler)

		self.next_tag_handler = EventHandler(self.next_tag)
		self.next_tag_event_instance = ExternalEvent.Create(self.next_tag_handler)

		self.previous_tag_handler = EventHandler(self.previous_tag)
		self.previous_tag_event_instance = ExternalEvent.Create(self.previous_tag_handler)
		
		# Populate ListBox with RoomTags from model
		self.display.refresh_model()
		self.display.refresh_view()
		
		# Populate item quantities
		self.textblock_quantity_in_model.Text = self.quantity_in_model_str
		self.textblock_view_name.Text         = self.active_ui_view.Name
		self.textblock_quantity_in_view.Text  = self.quantity_in_view_str
		
		# Show modeless window
		self.show_self()
	
	
	# Properties
	@property
	def active_ui_view(self):
		return uidoc.ActiveView
	
	@property
	def link_instances(self): #### Not used currently.
		result = FilteredElementCollector(doc).OfClass(RevitLinkInstance)
		p('linked_models:', result)
		return result
	
	@property
	def link_documents(self): #### Not used currently.
		if self.link_instances:
			result = [instance.GetLinkDocument() for instance in self.link_instances]
			result_cleaned = [document for document in result if document is not None]
			p('link_documents:', result_cleaned)
			return result_cleaned
		else: p('No link instances. Cannot create link_documents.')
	
	@property
	def link_titles(self): #### Not used currently.
		if self.link_documents:
			result = [document.Title for document in self.link_documents]
			p('link_titles:', result)
			return result
		else: p('No link documents. Cannot create link_titles.')
	
	@property
	def room_tags_in_model(self):
		result = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_RoomTags).WhereElementIsNotElementType()
		p('room_tags_in_model:', result)
		if result: return result
		else: p('No room tags in model.')
	
	@property
	def room_tag_owner_view_ids_in_model_set(self):
		if self.room_tags_in_model:
			result = set([tag.OwnerViewId for tag in self.room_tags_in_model])
			p('room_tag_owner_view_ids_in_model_set:', result)
			return result
		else: p('No room tags in model. Cannot make room_tag_owner_view_ids_in_model_set.')
	
	@property
	def room_tags_visible_in_model(self):
		room_tags_visible_in_model = List[Element]()
		if self.room_tag_owner_view_ids_in_model_set:
			for owner_view_id in self.room_tag_owner_view_ids_in_model_set:
				room_tags_visible_in_view = FilteredElementCollector(doc, owner_view_id).OfCategory(BuiltInCategory.OST_RoomTags).WhereElementIsNotElementType().ToElements()
				room_tags_visible_in_model.AddRange(room_tags_visible_in_view)
			p('room_tags_visible_in_model:', [tag.Id.ToString() for tag in room_tags_visible_in_model])
			return room_tags_visible_in_model
		else: p('No room_tag_owner_view_ids_in_model_set. Cannot make room_tags_visible_in_model')
	
	@property
	def room_tag_ids_not_visible_in_any_view(self):
		room_tags_in_model = self.room_tags_in_model
		if room_tags_in_model:
			room_tag_ids_in_model = [tag.Id.IntegerValue for tag in room_tags_in_model]
			room_tag_ids_in_model_set = set(room_tag_ids_in_model)
			p('room_tags_in_model_set:', [tag_id for tag_id in room_tag_ids_in_model_set])
			if self.room_tags_visible_in_model:
				room_tags_visible_in_model_list = self.room_tags_visible_in_model
				room_tag_ids_visible_in_model = [tag.Id.IntegerValue for tag in room_tags_visible_in_model_list]
				room_tag_ids_visible_in_model_set = set(room_tag_ids_visible_in_model)
				set_diff = room_tag_ids_in_model_set.difference(room_tag_ids_visible_in_model_set)
				result = [ElementId(tag_id_string) for tag_id_string in set_diff]
				p('room_tags_not_visible_in_any_view:', [tag_id.IntegerValue for tag_id in result])
				return result
			else: p('no room tags visible in model. cannot make room_tags_not_visible_in_any_view')
		else: p('no room tags in model. cannot make room_tags_not_visible_in_any_view')
	
	@property
	def room_tags_in_view(self):
		result = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_RoomTags).WhereElementIsNotElementType().OwnedByView(self.active_ui_view.Id)
		p('room_tags_in_view:', result)
		return result
	
	@property
	def orphaned_room_tags_in_model(self):
		result = [room_tag for room_tag in self.room_tags_in_model if room_tag.IsOrphaned]
		p('orphaned_room_tags_in_model:', result)
		return result
	
	@property
	def misplaced_room_tags_in_model(self):
		result = [room_tag for room_tag in self.room_tags_in_model if not room_tag.IsInRoom]
		p('misplaced_room_tags_in_model:', result)
		return result
	
	@property
	def orphaned_or_misplaced_room_tags_in_model(self):
		result = [room_tag for room_tag in self.room_tags_in_model if room_tag.IsOrphaned or not room_tag.IsInRoom]
		p('orphaned_or_misplaced_room_tags_in_model:', [elem.Id for elem in result])
		return result
	
	@property
	def list_items_in_model(self):
		result = [ListItem(room_tag) for room_tag in self.room_tags_in_model if room_tag.IsOrphaned or not room_tag.IsInRoom]
		p('orphaned_or_misplaced_room_tags_in_model:', [list_item.elem.Id for list_item in result])
		return result
	
	@property
	def display_strings_in_model(self):
		return [list_item.display_string for list_item in self.display.model]
	
	@property
	def quantity_in_model(self):
		result = len(self.orphaned_or_misplaced_room_tags_in_model)
		p('quantity_in_model:', result)
		return result
	
	@property
	def quantity_in_model_str(self):
		return str(self.quantity_in_model)
	
	@property
	def tag_ids_in_model(self):
		result = [tag.Id for tag in self.orphaned_or_misplaced_room_tags_in_model]
		p('tag_ids_in_model:', result)
		return result
	
	@property
	def recentered_tag_ids(self):
		return [tag.Id for tag in self.recentered_tags]
	
	@property
	def recentered_list_item_ids(self):
		return [list_item.elem.Id for list_item in self.recentered_list_items]
	
	@property
	def recentered_display_strings(self):
		return [list_item.display_string for list_item in self.recentered_list_items]
	
	@property
	def orphaned_in_view(self):
		result = [room_tag for room_tag in self.room_tags_in_view if room_tag.IsOrphaned]
		p('orphaned_in_view:', [elem.Id for elem in result])
		return result
	
	@property
	def misplaced_in_view(self):
		result = [room_tag for room_tag in self.room_tags_in_view if not room_tag.IsInRoom]
		p('misplaced_in_view:', [elem.Id for elem in result])
		return result
	
	@property
	def orphaned_or_misplaced_room_tags_in_view(self):
		result = [room_tag for room_tag in self.room_tags_in_view if room_tag.IsOrphaned or not room_tag.IsInRoom]
		p('orphaned_or_misplaced_room_tags_in_view:', [elem.Id for elem in result])
		return result
	
	@property
	def list_items_in_view(self):
		result = [ListItem(room_tag) for room_tag in self.room_tags_in_view if room_tag.IsOrphaned or not room_tag.IsInRoom]
		p('orphaned_or_misplaced_room_tags_in_view:', [list_item.elem.Id for list_item in result])
		return result
	
	@property
	def display_strings_in_view(self):
		return [list_item.display_string for list_item in self.display.view]
	
	@property
	def tag_ids_in_current_view(self):
		result = [tag.Id for tag in self.orphaned_or_misplaced_room_tags_in_view]
		p('tag_ids_in_current_view:', result)
		return result
	
	@property
	def quantity_in_view(self):
		result = len(self.orphaned_or_misplaced_room_tags_in_view)
		p('quantity_in_view:', result)
		return result
	
	@property
	def quantity_in_view_str(self):
		return str(self.quantity_in_view)
	
	# Methods
	'''
	def regain_focus(self): #### No longer used.
		ctypes.windll.user32.SetForegroundWindow(self.as_foreground_window)
	'''
	
	def show_self(self):
		self.Show()
		'''
		# Set self as foreground window. No longer used.
		self.as_foreground_window = ctypes.windll.user32.GetForegroundWindow()
		p('foreground window:', self.as_foreground_window)
		'''
	
	def recenter_tag(self, tag):
		if not tag:  p('no input tag. cannot recenter_tag()')
		else: # If tag exists:
			if tag.IsOrphaned: p('tag is orphaned. cannot recenter_tag()')
			else: # If tag can be recentered:
				if tag.IsTaggingLink:
					linked_element_id         = tag.TaggedRoomId.LinkedElementId
					link_instance_id          = tag.TaggedRoomId.LinkInstanceId
					link_instance             = doc.GetElement(link_instance_id)
					link_instance_transform   = link_instance.GetTransform()
					link_doc                  = link_instance.GetLinkDocument()
					linked_room               = link_doc.GetElement(linked_element_id)
					room_location             = linked_room.Location
					tag.Location.Point        = link_instance_transform.OfPoint(room_location.Point)
					self.recentered_list_item = ListItem(tag)
				else: # If tag is tagging local room in host model:
					tagged_local_room         = doc.GetElement(tag.TaggedLocalRoomId)
					room_location             = tagged_local_room.Location
					tag.Location.Point        = room_location.Point
					self.recentered_list_item = ListItem(tag)
	
	
	# Methods to call via external event
	def refresh_window(self):
		button_name = 'refresh_window'
		
		new_window = ModelessForm("ui.xaml")
		self.Close()
		
		p('{} succeeded'.format(button_name))
	
	def listbox_entire_model_selection_change(self):
		button_name = 'listbox_entire_model_selection_change'
		selected_index = self.listbox_entire_model.SelectedIndex
		if selected_index > -1:
			self.current_tag = self.display.model[selected_index].elem
			if len(self.display.model) > 0:
				show_element(self.current_tag)
			self.refresh_listboxes_and_quantities()
			self.current_tag_index = selected_index
			p('{} succeeded'.format(button_name))
		else: p('selected_index not > -1. Likely no list items in listbox.')
	
	def listbox_entire_model_got_focus(self):
		self.listbox_entire_model_selection_change()
	
	def listbox_current_view_selection_change(self):
		button_name = 'listbox_current_view_selection_change'
		selected_index = self.listbox_current_view.SelectedIndex
		if selected_index > -1:
			self.current_tag = self.display.view[selected_index].elem
			if len(self.display.view) > 0:
				show_element(self.current_tag)
				elems_in_model = [item.elem_id for item in self.display.model]
				self.current_tag_index = elems_in_model.index(self.current_tag.Id)
			p('{} succeeded'.format(button_name))
		else: p('selected_index not > -1. Likely no list items in listbox.')
	
	def listbox_current_view_got_focus(self):
		self.listbox_current_view_selection_change()
	
	def recenter_selected_tag(self):
		button_name = 'recenter_selected_tag'
		if self.current_tag:
			if not self.current_tag.IsOrphaned:
				with pyrevit_transaction("Recenter room tag"):
					self.recenter_tag(self.current_tag)
				show_element(self.recentered_list_item.elem)
			p('{} succeeded'.format(button_name))
		else: p('no current_tag. cannot recenter_selected_tag()')

	def refresh_listboxes(self):
		self.display.refresh_model()
		self.display.refresh_view()
	
	def refresh_quantities(self):
		self.textblock_quantity_in_model.Text = self.quantity_in_model_str
		self.textblock_quantity_in_view.Text  = self.quantity_in_view_str
	
	def refresh_active_view_text(self):
		self.textblock_view_name.Text         = self.active_ui_view.Name
	
	def refresh_listboxes_and_quantities(self):
		self.refresh_listboxes()
		self.refresh_quantities()
		self.refresh_active_view_text()
	
	def delete_selected_tag(self):
		if self.current_tag:
			button_name = 'delete_selected_tag'
			p('deleting selected_tag {}'.format(self.current_tag.Id))
			with pyrevit_transaction("Delete room tag"):
				doc.Delete(self.current_tag.Id)
			self.current_tag = None
			self.refresh_listboxes_and_quantities()
			p('{} succeeded'.format(button_name))
		else: p('no current tag. cannot delete_selected_tag()')

	def recenter_all_tags_in_view(self):
		misplaced_in_view = self.misplaced_in_view
		if misplaced_in_view:
			button_name = 'recenter_all_tags_in_view'
			recentered_list_items = []
			with pyrevit_transaction("Recenter all room tags in view"):
				for tag in misplaced_in_view:
					self.recenter_tag(tag)
					recentered_list_items.append(ListItem(tag))
			self.recentered_list_items = recentered_list_items
			self.display.set_view(self.recentered_list_items)
			if self.display.view: show_element(self.display.view[0].elem)
			p('{} succeeded'.format(button_name))
		else: p('no misplaced tags in view. cannot recenter_all_tags_in_view()')

	def delete_all_orphans_in_view(self):
		button_name = 'delete_all_orphans_in_view'
		with pyrevit_transaction("Delete orphaned room tags in view"):
			for tag in self.orphaned_in_view:
				doc.Delete(tag.Id)
		self.refresh_listboxes_and_quantities()
		p('{} succeeded'.format(button_name))
	
	def delete_room_tags_not_visible_in_any_view(self):
		button_name = 'delete room tags not visible in any view'
		if self.room_tag_ids_not_visible_in_any_view:
			with pyrevit_transaction("Delete room tags not visible in any view"):
				for tag_id in self.room_tag_ids_not_visible_in_any_view:
					doc.Delete(tag_id)
			self.refresh_listboxes_and_quantities()
			p('{} succeeded'.format(button_name))
		else: p('No self.room_tag_ids_not_visible_in_any_view. Cannot delete_room_tags_not_visible_in_any_view()')

	def next_tag(self):
		button_name = 'next_tag'
		if self.current_tag_index < len(self.display.model) - 1:
			self.current_tag_index += 1
			p('Current Tag Index: ', self.current_tag_index)
			show_element(self.display.model[self.current_tag_index].elem)
			self.refresh_listboxes_and_quantities()
			p('{} succeeded'.format(button_name))
		else: p('There is no next tag to show.')

	def previous_tag(self):
		button_name = 'previous_tag'
		if self.current_tag_index >= 1 < len(self.display.model):
			self.current_tag_index -= 1
			p('Current Tag Index: ', self.current_tag_index)
			show_element(self.display.model[self.current_tag_index].elem)
			self.refresh_listboxes_and_quantities()
			p('{} succeeded'.format(button_name))
		else: p('There is no previous tag to show.')

	
	# Events to call from xaml UI
	def close_window(self, sender, args):
		self.Close()

	def refresh_window_event(self, sender, args):
		self.refresh_window_event_instance.Raise()

	def listbox_entire_model_selection_change_event(self, sender, args):
		self.listbox_entire_model_selection_change_event_instance.Raise()
	
	def listbox_entire_model_got_focus_event(self, sender, args):
		self.listbox_entire_model_got_focus_event_instance.Raise()

	def listbox_current_view_selection_change_event(self, sender, args):
		self.listbox_current_view_selection_change_event_instance.Raise()
	
	def listbox_current_view_got_focus_event(self, sender, args):
		self.listbox_current_view_got_focus_event_instance.Raise()

	def recenter_selected_tag_event(self, sender, args):
		self.recenter_selected_tag_event_instance.Raise()

	def delete_selected_tag_event(self, sender, args):
		self.delete_selected_tag_event_instance.Raise()

	def recenter_all_tags_in_view_event(self, sender, args):
		self.recenter_all_tags_in_view_event_instance.Raise()

	def delete_all_orphans_in_view_event(self, sender, args):
		self.delete_all_orphans_in_view_event_instance.Raise()

	def delete_room_tags_not_visible_in_any_view_event(self, sender, args):
		self.delete_room_tags_not_visible_in_any_view_event_instance.Raise()

	def next_tag_event(self, sender, args):
		self.next_tag_event_instance.Raise()

	def previous_tag_event(self, sender, args):
		self.previous_tag_event_instance.Raise()


# Instantiate the form
modeless_form = ModelessForm("ui.xaml")

ui.xaml file:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		xmlns:av="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:Collections="clr-namespace:System.Collections;assembly=mscorlib" mc:Ignorable="av"
		Title="Fix Room Tags" ResizeMode="NoResize" SizeToContent="WidthAndHeight" Background="WhiteSmoke" BorderBrush="#FF89AF98" Topmost="True">
	<StackPanel x:Name="main_stackpanel" Margin="10">
		<StackPanel Orientation="Horizontal" Margin="0,10,0,0">
			<StackPanel Orientation="Vertical" Margin="0,0,10,0">
                <!--<ComboBox x:Name="select_arch_model_combo_box" SelectionChanged="select_arch_model_event" Text="Select Architectural Model" Background="#FF89AF98" Foreground="Black" MinWidth="155" Margin="0,0,0,10">
				</ComboBox>-->
                <Button x:Name="delete_room_tags_not_visible_in_any_view_button" Click="delete_room_tags_not_visible_in_any_view_event" Content="Delete room tags not visible in any view" Padding="5,1,5,1" HorizontalContentAlignment="Left" Margin="0,1,0,11"/>
                <StackPanel Orientation="Horizontal" Margin="0,0,0,0">
					<TextBlock Text="Errant tags in entire model:" Padding="1,0,5,0" Margin="0,0,0,5" />
                    <TextBlock x:Name="textblock_quantity_in_model" Text="#" Padding="5,0,5,0" Margin="0,0,0,5" />
                </StackPanel>
                <ListBox x:Name="listbox_entire_model" GotFocus="listbox_entire_model_got_focus_event" SelectionChanged="listbox_entire_model_selection_change_event" Margin="0,0,0,10" Height="100" />
                <Button x:Name="recenter_selected_tag_button" Click="recenter_selected_tag_event" Content="Re-center Selected Tag" HorizontalContentAlignment="Left" Padding="5,1,1,1" />
                <Button x:Name="delete_selected_tag_button" Click="delete_selected_tag_event" Content="Delete Selected Tag" HorizontalContentAlignment="Left" Padding="5,1,1,1" />
            </StackPanel>
			<StackPanel Orientation="Vertical">
                <Button x:Name="refresh_button_Copy" Click="refresh_window_event" Content="Refresh" Padding="5,1,5,1" HorizontalContentAlignment="Right" Margin="0,1,0,11"/>
                <StackPanel Orientation="Horizontal" Margin="0,0,0,0">
                    <TextBlock Text="In current view:" Padding="1,0,5,0" Margin="0,0,0,5" />
                    <TextBlock x:Name="textblock_view_name" Text="Current View Name" Padding="5,0,5,0" Margin="0,0,0,5" />
                    <TextBlock x:Name="textblock_quantity_in_view" Text="#" Padding="5,0,5,0" Margin="0,0,0,5" />
                </StackPanel>
                <ListBox x:Name="listbox_current_view" GotFocus="listbox_current_view_got_focus_event" SelectionChanged="listbox_current_view_selection_change_event" Margin="0,0,0,10" Height="100" >
				</ListBox>
                <Button Content="Re-center All Tags in View" Click="recenter_all_tags_in_view_event" HorizontalContentAlignment="Right" Padding="1,1,5,1" />
                <Button Content="Delete All Orphans in View" Click="delete_all_orphans_in_view_event" HorizontalContentAlignment="Right" Padding="1,1,5,1" />
			</StackPanel>
		</StackPanel>
		<StackPanel Orientation="Horizontal" Margin="0,10,0,0" HorizontalAlignment="Right">
            <Button x:Name="previous_tag_button" Content="Previous Tag" Height="35" Click="previous_tag_event" Background="#FF89AF98" Foreground="#FF071E42" FontSize="20" BorderThickness="0,0,0,4" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="0,0,10,0" Padding="5,1,5,1" VerticalContentAlignment="Top" >
				<Button.BorderBrush>
					<SolidColorBrush Color="#FF609274" Opacity="1"/>
				</Button.BorderBrush>
			</Button>
            <Button x:Name="next_tag_button" Content="Next Tag" Height="35" Click="next_tag_event" Background="#FF89AF98" Foreground="#FF071E42" FontSize="20" BorderThickness="0,0,0,4" VerticalAlignment="Bottom" HorizontalAlignment="Right" Padding="5,1,5,1" VerticalContentAlignment="Top" >
				<Button.BorderBrush>
					<SolidColorBrush Color="#FF609274" Opacity="1"/>
				</Button.BorderBrush>
			</Button>
		</StackPanel>
	</StackPanel>
</Window>

Put those 3 files in a pyRevit pushbutton and you should be good to go. I would also welcome any suggestions to improve the code.

2 Likes

Hi,
I don’t see any ShowWindow(), maybe that’s why it’s not working properly.

Grab & hide active window:

xaml_window = ctypes.windll.user32.GetForegroundWindow()
# https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
# minimize xaml to tray
ctypes.windll.user32.ShowWindow(xaml_window, 6)

Restore previous:

# https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
# restore xaml window
ctypes.windll.user32.ShowWindow(xaml_window, 4)
# activate xaml window
ctypes.windll.user32.SetForegroundWindow(xaml_window)
1 Like

Hmm. That is good information, but I’m not sure if that was the issue. I tried implementing your fix like this:

	def regain_focus(self):
		ctypes.windll.user32.ShowWindow(self, 4)
		ctypes.windll.user32.SetForegroundWindow(self.as_foreground_window)

	def listbox_entire_model_got_focus(self):
		button_name = 'listbox_entire_model_got_focus'
		selected_index = self.listbox_entire_model.SelectedIndex
		if selected_index > -1:
			self.current_tag = self.display.model[selected_index].elem
			if len(self.display.model) > 0:
				show_element(self.current_tag)
				self.regain_focus()
			self.refresh_listboxes_and_quantities()
			self.current_tag_index = selected_index
			p('{} succeeded'.format(button_name))
		else: p('selected_index not > -1. Likely no list items in listbox.')

Is that what you were suggesting?

When the ListBox gets focus, the element is shown, the modeless dialog and Revit both become unresponsive (except weirdly, you can still zoom in and out of the revit view), then self.regain_focus should re-focus the modeless dialog…

Still, the UI does not regain responsiveness. I think the modeless window does successfully get set to foreground, but I guess this doesn’t fix anything.

Either way, I worked around the issue by not triggering the pop-up dialog to begin with, and I’m pretty happy with that solution.