Hook to multiple Events

Hello pyRevit friends :slight_smile:

After spending over 2 years with Revit and Dynamo I now want to start using pyRevit.
For me hook scripts are very interresting, i already got my first ones running :slight_smile:

I would like to know if it is possible to hook a script to multiple events, for example save and sync. Or do i have to copy the file several times because the filename determines the event?

Looking forward to learn much about pyRevit here :smiley:
Kind Regards from Vienna, Austria

I don’t use the pyRevit hooks, so I don’t know how useful my comment will be.
I implement my own listeners if I ever need to hook onto any of the events inside Revit.
I imagine if you want to create a single class or block of code that that listens to multiple events you will have to do it yourself. If that is not the case someone more experienced with the pyRevit hooks can correct me.

Here is an example of a simple class I made that listens for document changes and view changes to streamline the process of syncing asynchronous code with Revit.
I use this specific listener inside of an IUpdater so I can synchronize the database inside the updater as the active document/view changes, or new documents get opened/closed.

import pyevent
from Autodesk.Revit.UI import UIApplication
from Autodesk.Revit.UI.Events import ViewActivatedEventArgs
from Autodesk.Revit.DB.Events import (RevitAPIEventStatus, DocumentOpenedEventArgs,
                                      DocumentClosingEventArgs, DocumentClosedEventArgs)


class ViewChangedListener(object):
    def __init__(self, uiapp):
        # type: (UIApplication) -> ViewChangedListener
        self.view_changed, self._view_changed_caller = pyevent.make_event()
        self.register(uiapp)

    def register(self, uiapp):
        # type: (ViewChangedListener, UIApplication) -> None
        uiapp.ViewActivated += self.view_activated_handler

    def unregister(self, uiapp):
        # type: (ViewChangedListener, UIApplication) -> None
        uiapp.ViewActivated -= self.view_activated_handler

    def view_activated_handler(self, sender, args):
        # type: (ViewChangedListener, object, ViewActivatedEventArgs) -> None
        self._view_changed_caller(self, args)


class DocumentOpenedListener(object):
    def __init__(self, uiapp):
        # type: (UIApplication) -> DocumentOpenHandler
        self.opened, self._opened_caller = pyevent.make_event()
        self.register(uiapp)

    def register(self, uiapp):
        # type: (DocumentOpenedListener, UIApplication) -> None
        uiapp.Application.DocumentOpened += self.opened_handler

    def unregister(self, uiapp):
        # type: (DocumentOpenedListener, UIApplication) -> None
        uiapp.Application.DocumentOpened -= self.opened_handler

    def opened_handler(self, sender, args):
        # type: (DocumentOpenedListener, object, DocumentOpenedEventArgs) -> None
        self._opened_caller(self, args)


class DocumentClosedListener(object):
    def __init__(self, uiapp):
        # type: (UIApplication) -> DocumentCloseHandler
        self.closing_data = {}
        self.closed, self._closed_caller = pyevent.make_event()
        self.register(uiapp)

    def register(self, uiapp):
        # type: (DocumentClosedListener, UIApplication) -> None
        uiapp.Application.DocumentClosing += self.closing_handler
        uiapp.Application.DocumentClosed += self.closed_handler

    def unregister(self, uiapp):
        # type: (DocumentClosedListener, UIApplication) -> None
        uiapp.Application.DocumentClosing -= self.closing_handler
        uiapp.Application.DocumentClosed -= self.closed_handler

    @staticmethod
    def get_doc_name(doc):
        if doc.ProjectInformation:
            return doc.ProjectInformation.Name
        else:
            return doc.Title

    def closing_handler(self, sender, args):
        # type: (DocumentClosedListener, object, DocumentClosingEventArgs) -> None
        doc = args.Document
        doc_name = self.get_doc_name(doc)
        self.closing_data[str(args.DocumentId)] = doc_name

    def closed_handler(self, sender, args):
        # type: (DocumentClosedListener, object, DocumentClosedEventArgs) -> None
        if args.Status == RevitAPIEventStatus.Failed or args.Status == RevitAPIEventStatus.Cancelled:
            self.closing_data.pop(str(args.DocumentId), None)
        elif args.Status == RevitAPIEventStatus.Succeeded:
            self._closed_caller(self, self.closing_data.pop(str(args.DocumentId), None))
        else:
            pass


class DocumentListener(object):
    def __init__(self, uiapp):
        # type: (DocumentListener, UIApplication) -> None
        self._opened_listener = DocumentOpenedListener(uiapp)
        self._closed_listener = DocumentClosedListener(uiapp)
        self._view_changed_listener = ViewChangedListener(uiapp)

        # these are events that you can subscribe to
        # I.E. document_lister_instance.opened += some_func
        self.opened = self._opened_listener.opened
        self.closed = self._closed_listener.closed
        self.view_changed = self._view_changed_listener.view_changed

    def register(self, uiapp):
        # type: (DocumentListener, UIApplication) -> None
        self._opened_listener.register(uiapp)
        self._closed_listener.register(uiapp)
        self._view_changed_listener.register(uiapp)

    def unregister(self, uiapp):
        # type: (DocumentListener, UIApplication) -> None
        self._opened_listener.unregister(uiapp)
        self._closed_listener.unregister(uiapp)
        self._view_changed_listener.unregister(uiapp)

Here is an example of how you could use this to hook into multiple events within the same script.
I don’t actually know if this example work to be honest, but it should work. (I’m not able to test it at the moment.)
Edit: if it isn’t clear this example should just have a dialog popup saying “Document - Opened” or “Document - Closed”. But like I said I couldn’t test it.

from Autodesk.Revit.UI import TaskDialog, TaskDialogCommonButtons, TaskDialogResult


def warning_dialog(window_name, main_instruction, main_content):
    td = TaskDialog(window_name)
    td.TitleAutoPrefix = False
    td.AllowCancellation = True
    td.MainInstruction = main_instruction
    td.MainContent = main_content
    td.CommonButtons = TaskDialogCommonButtons.Ok
    td.DefaultButton = TaskDialogResult.Ok
    return td.Show()


def doc_opened_popup(sender, args):
    # type: (object, DocumentOpenedEventArgs) -> None
    doc = args.Document
    if doc.ProjectInformation:
        doc_name = doc.ProjectInformation.Name
    else:
        doc_name = doc.Title

    window_name = "{} - Opened".format(doc_name)
    main_instruction = window_name
    main_content = main_instruction

    td_result = warning_dialog(window_name, main_instruction, main_content)
    
    
def doc_closed_popup(sender, args):
    # type: (object, str | None) -> None
    doc_name = args
    window_name = "{} - Opened".format(doc_name)
    main_instruction = window_name
    main_content = main_instruction
    
    td_result = warning_dialog(window_name, main_instruction, main_content)



dl = DocumentListener(__revit__)
dl.opened += doc_opened_popup
dl.closed += doc_closed_popup
5 Likes

this is very interesting - do you implement this in the startup.py or as a button? Secondly, how do you safely unregister - I have had issues with the same event being registered multiple times and Revit crashing if I don’t explicitly unregister the event when reloading through pyrevit

Hello Nicholas,

Thanks for your reply, very interesting and I´ll keep that in mind, but for now this is out of my league and i will stay with the hooks :slight_smile:

Another question about hooks, in my “Extension/Hooks/” folder can only be one file for a event, for example “doc-syncing.py”. How can i hook another script to this event? Can there be several hook folders or only one?

@revitislife
Safety unregistering could be an issue with this. I use this in a IUpdater class that is intended to be initialized at startup. Thus, it should be active throughout the entire lifetime of Revit.

But, to be honest I’ve never had any issues with Revit crashing when I try to register the same thing twice. I’ve only had issues with already registered events being registered forever. However, I don’t know of a simple way of dealing with that issue.
If you have issues with Revit crashing that may be a different problem all together.

@Gerhard.P
I checked the pyRevit source code and it looks possible to have multiple hook files for same event, but I did not test it. However, I don’t see why you would need multiple files for the same event anyway though.
If you need to have multiple “scripts” for the same event, couldn’t you just have them in the same file? You could create two separate functions that get conditionally executed based on what you need.

If you want to have separate hooks it looks like you just need to name them differently like so.
“script_hook_one_doc-syncing.py”
“script_hook_two_doc-syncing.py”
The pyRevit source code will look for the event name at the end of the file name so you should be able to have multiple files for the same event.

I’m not expert with the pyRevit hooks, but I hope this helps.

2 Likes

do you by any chance have any documentation on the Iupdater Class in pyrevit?

@revitislife
The IUpdater class is not something that you need or should use to manage events. IUpdater is just the interface Revit provides to have code dynamically react to changes inside the Document. I only mentioned it because I specifically used the events inside of an implementation of the IUpdater which needs to be synchronized with new documents being opened/closed.

However, here is the page from revitapidocs.
And for a specific example. I have a different IUpdater which reacts to newly created or modified elevation tags. When a new one is created or modified it adds or modifies the elevation tags prefix/suffix based on the elevations tags referenced element.

2 Likes

@Nicholas.Miles i understand that it is a good method to put all code in one file, but I´m a noob coder and my primary goal is to keep every code as short as possible because finding errors is hard enough for me :smiley:
I´m still coding in dynamo codeblocks, i think the next step for me should be to use a compiler.

1 Like

@Gerhard.P
I completely understand. I only mentioned both options because I did not know if having a separate file would actually work or not.
It is best practice to separate code based on it’s function anyway. So the best solution would be to have separate files if they are completely separate scripts.
Glad I could help out. BTW, Thank you for posting that snapshot verifying the solution.

for reference

1 Like