Dynamic Model Updater and Revit Events

Hallo pyRevit friends :slight_smile:

Im struggling to understand what the Dynamic model updater does and how this relates to Revit Events.

I found this thread where @eirannejad also created a implementation for the iUpdater to the pyRevit hooks.

Could someone please take the time to explain how all this works?
Here is an example usecase:

I want to update parameters of wall openings.
Looking at the available RevitEvents and the corresponing available Hooks there is nothing that really meets my needs. These events are firing way too often or not often enough.
I would wish for something that fires if a wall/wallopening gets changed. Is this where the iUpdater comes in the game? Can i really target specific elements with this method?

Is there any news for the pyRevit implemantation i should know?

Happy about any help :slight_smile:
Kind regards!

An IUpdater sounds perfect for what you want to do. The IUpdater can realistically target anything you want, no matter how specifc. When you implement an IUpdater you specify what the updater will react to by specifying the “ChangeType” and giving it a filter or list of elements to define which elements it will accept.

If you wanted to create an IUpdater to modify the walls paremters when wall openings change, your trigger would look something like this.

UpdaterRegistry.AddTrigger(self._updater_id, ElementClassFilter(Wall), Element.GetChangeTypeGeometry())

Here an implementation of IUpdater that I use for reference. The purpose of this code is to set elevation tags prefix and suffix base on what it is referencing.

import re

from Autodesk.Revit.DB import (IUpdater, UpdaterId, UpdaterRegistry, ElementCategoryFilter, ChangePriority,
                               Element, ChangeType, SpotDimension, BuiltInParameter)

from revit_utils import category

class IUpdaterBase(IUpdater):
    @classmethod
    def uuid(cls):
        import hashlib
        from System import Guid

        m = hashlib.md5()
        m.update(cls.__name__.encode('utf-8'))
        return Guid(m.hexdigest())

    def __init__(self, addin_id, *args, **kwargs):
        self._addin_id = addin_id
        self._updater_id = UpdaterId(addin_id, self._uuid())

    def GetUpdaterId(self):
        return self._updater_id

    def GetAdditionalInformation(self):
        return 'Not Implemented'

    def GetUpdaterName(self):
        return self.__class__.__name__

    def GetChangePriority(self):
        return ChangePriority.Annotations

    def register_updater(self):
        UpdaterRegistry.RegisterUpdater(self, True)
        self.register_triggers()

    def register_triggers(self):
        pass

    def unregister_triggers(self):
        UpdaterRegistry.RemoveAllTriggers(self._updater_id)

    def Execute(self, data):
        raise NotImplemented



class ElevationUpdater(IUpdaterBase):
    def __init__(self, addin_id, *args, **kwargs):
        super(ElevationUpdater, self).__init__(addin_id, args, kwargs)
        self._updated_elements = []

    def GetChangePriority(self):
        return ChangePriority.Annotations

    def register_triggers(self):
        change_type = ChangeType.ConcatenateChangeTypes(Element.GetChangeTypeElementAddition(),
                                                        Element.GetChangeTypeAny()
                                                        )
        filtered_category = category.elevation_tags.as_id
        if UpdaterRegistry.IsUpdaterRegistered(self._updater_id):
            UpdaterRegistry.RemoveAllTriggers(self._updater_id)
            UpdaterRegistry.AddTrigger(self._updater_id, ElementCategoryFilter(filtered_category), change_type)

    def Execute(self, data):
        try:
            doc = data.GetDocument()
            added_ids = data.GetAddedElementIds()
            modded_ids = data.GetModifiedElementIds()
            if added_ids:
                modified_elements = [doc.GetElement(eid) for eid in added_ids]  # type: list[SpotDimension]
            elif modded_ids:
                modified_elements = [doc.GetElement(eid) for eid in modded_ids]  # type: list[SpotDimension]
            else:
                return
            for element in modified_elements:
                
                # this is very important to prevent recursion
                if element.UniqueId in self._updated_elements:
                    self._updated_elements.remove(element.UniqueId)
                    continue
                    
                self._updated_elements.append(element.UniqueId)
                element_prefix = element.Prefix if element.Prefix else ""
                re_rule = re.compile(r'(\d+ )?(\d+/\d+|\d+)" [A-Z]+(-[A-Z]+)?')
                re_match = re_rule.search(element_prefix)
                if re_match:
                    s, e = re_match.span()
                    pre_prefix, post_prefix = element_prefix[:s], element_prefix[e:]
                else:
                    pre_prefix, post_prefix = '', element_prefix
                for reference in list(element.References):
                    ref_element = doc.GetElement(reference)
                    ref_id = ref_element.Category.Id.IntegerValue
                    if ref_id == category.pipes:
                        size = ref_element.get_Parameter(BuiltInParameter.RBS_CALCULATED_SIZE).AsString().strip()
                        sys_abv = ref_element.get_Parameter(BuiltInParameter.RBS_DUCT_PIPE_SYSTEM_ABBREVIATION_PARAM).AsString()
                        element.Prefix = pre_prefix + '%s %s' % (size, sys_abv) + post_prefix
                    elif ref_id in (category.pipe_fittings, category.pipe_accessories):
                        if 'PIPE SLEEVE' in ref_element.Symbol.FamilyName:
                            element.Prefix = pre_prefix + ref_element.Name.rstrip('.') + post_prefix
        except Exception as ex:
            import sys
            print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(ex).__name__, ex)

2 Likes

Hello @Nicholas.Miles :slight_smile:

That are good news, so it´s not a waste of time if i read up on IUpdater!
Uh, I´m afraid of any code that has a class definition. I will study you code, test and be back with questions.
Thanks for your help!

Took some time but now i got my first iUpdater running, the GetChangeTypeGeometry() method works perfect, it really triggers at any geometric changes but not for all changes like GetChangeTypeAny() :slight_smile:

Does it look ok?

from Autodesk.Revit.DB import IUpdater, UpdaterId, ElementId, UpdaterRegistry
from Autodesk.Revit.DB import Element, ElementCategoryFilter, BuiltInCategory, ChangePriority
from Autodesk.Revit.UI import TaskDialog
from System import Guid

# Define the SimpleUpdater
class SimpleUpdater(IUpdater):
    def Execute(self, data):
        print("Updater was triggered!")
    def GetUpdaterId(self):
        # Return the unique identifier for this updater
        return self.updater_id

    def GetUpdaterName(self):
        return 'SimpleUpdaterName'

    def GetAdditionalInformation(self):
        return 'A simple updater for testing purposes'

    def GetChangePriority(self):
        return ChangePriority.Annotations

    def Initialize(self):
        # This is where you can add trigger conditions for the updater
        pass

    def Uninitialize(self):
        pass

# Get the current document and application
doc = __revit__.ActiveUIDocument.Document
app = __revit__.Application

# Create an instance of the updater
updater = SimpleUpdater()

# Create a unique Guid for the updater
guid = Guid.NewGuid()

# Create an UpdaterId using the AddInId of the current application and the unique Guid
updater_id = UpdaterId(app.ActiveAddInId, guid)

# Set the identifier in the updater instance
updater.updater_id = updater_id

# Register the updater with a trigger for wall elements
if not UpdaterRegistry.IsUpdaterRegistered(updater_id, doc):
    UpdaterRegistry.RegisterUpdater(updater, doc)
    
    # Create a filter for wall elements
    wall_filter = ElementCategoryFilter(BuiltInCategory.OST_Walls)
    
    # Assign the trigger to the updater for element updates
    UpdaterRegistry.AddTrigger(updater_id, wall_filter, Element.GetChangeTypeGeometry())
    
    TaskDialog.Show('Success', 'Updater has been registered and trigger has been set!')
else:
    TaskDialog.Show('Notice', 'Updater is already registered.')

It seems that the check if the updater is registered does not work, because when i run the code multiple times i always get the ‘Success’ message…

And another one, also working fine, it gets triggered if a detail number changes or a view is placed on a sheet.

from Autodesk.Revit.DB import IUpdater, UpdaterId, ElementId, UpdaterRegistry
from Autodesk.Revit.DB import Element, ElementCategoryFilter, BuiltInCategory, ChangePriority,Element, ElementId, ElementParameterFilter, ParameterValueProvider, FilterStringEquals, BuiltInParameter
from Autodesk.Revit.UI import TaskDialog
from System import Guid

# Define the SimpleUpdater
class SimpleUpdater(IUpdater):
    def Execute(self, data):
        print("Updater was triggered!")
    def GetUpdaterId(self):
        # Return the unique identifier for this updater
        return self.updater_id

    def GetUpdaterName(self):
        return 'SimpleUpdaterName'

    def GetAdditionalInformation(self):
        return 'A simple updater for testing purposes'

    def GetChangePriority(self):
        return ChangePriority.Annotations

    def Initialize(self):
        # This is where you can add trigger conditions for the updater
        pass

    def Uninitialize(self):
        pass

# Get the current document and application
doc = __revit__.ActiveUIDocument.Document
app = __revit__.Application

# Create an instance of the updater
updater = SimpleUpdater()

# Create a unique Guid for the updater
guid = Guid.NewGuid()

# Create an UpdaterId using the AddInId of the current application and the unique Guid
updater_id = UpdaterId(app.ActiveAddInId, guid)

# Set the identifier in the updater instance
updater.updater_id = updater_id

# Create a filter for views
viewport_filter = ElementCategoryFilter(BuiltInCategory.OST_Viewports)

# Get the ElementId for the VIEWPORT_DETAIL_NUMBER parameter
param_id = ElementId(BuiltInParameter.VIEWPORT_DETAIL_NUMBER)

# Register the updater and add the trigger
if not UpdaterRegistry.IsUpdaterRegistered(updater_id, doc):
    UpdaterRegistry.RegisterUpdater(updater, doc)
    # Add trigger for the modification of the VIEWPORT_DETAIL_NUMBER parameter
    UpdaterRegistry.AddTrigger(updater_id, viewport_filter, Element.GetChangeTypeParameter(param_id))
    # Add trigger for the addition of a new viewport to a sheet
    UpdaterRegistry.AddTrigger(updater_id, viewport_filter, Element.GetChangeTypeElementAddition())
    TaskDialog.Show('Success', 'Updater has been registered and trigger has been set!')
else:
    TaskDialog.Show('Notice', 'Updater is already registered.')

Do not do this. Create a guid and save it in the script or create a function to generate a reproducible guid based on a seed. The reason you are always getting a success is because the guid you are using is different each time. A new instance of the updater is being registered each time you are running that script. Meaning that updater may be running multiple times depending on how many times you registered it.

In my original post i used this method to generate reproducible guids based on the class name of the updater. I’m not sure if the best solution, but I wanted something simple and easy to use so I can easily created new updaters without having to manually create guids.

2 Likes

I´m glad you pointed that out, I fixed this mistake and it works as expected :slight_smile:

But why not just using a fixed guid? I created one with guid = Guid.NewGuid() and just use that now.
Regarding the chance of collision, chatgpt says:

Collisions : If by some very rare chance another updater or software component uses the same GUID, you’ll run into issues, as GUIDs are supposed to be globally unique. However, the probability of a GUID collision is astronomically low when the GUIDs are generated correctly.

The chance of a collision in GUIDs (or UUIDs) depends on the version being used and the number of GUIDs generated. Here’s a general overview:

Background
A GUID (Globally Unique Identifier) or UUID (Universally Unique Identifier) is a 128-bit number, which means there are 2^128 (about 3.4 x 10^38) possible GUIDs.

For our purposes, let’s discuss UUID version 4, which is commonly used and based on random numbers. In version 4 UUIDs, 6 bits are set aside for version information, so the randomness is provided by the remaining 122 bits, yielding 2^122 (about 5.3 x 10^36) different UUIDs.

Collision Probability
Given the vast number of possible UUIDs (5.3 x 10^36 for version 4), the chance of two randomly generated UUIDs being identical (i.e., a collision) is extraordinarily small. However, as more UUIDs are generated, the probability of a collision increases, though it remains very small until an immense number of UUIDs have been generated.

For a more intuitive understanding, consider the Birthday Paradox in probability theory. Even with just 23 people in a room, there’s a better than even chance that two of them share the same birthday, despite there being 365 possible birthdays.

Similarly, after generating a certain number of UUIDs, the probability of a collision becomes more significant, but that number is vast.

Approximate Calculation
Using the Birthday Paradox formula for collision probability:

image

Where:

P(n) is the probability of a collision

n is the number of generated UUIDs

d is the number of possible UUIDs (i.e., 2^122 for version 4)

e is the base of the natural logarithm (approximately equal to 2.71828)
Even if we generate a billion (1 x 10^9) UUIDs, plugging in the numbers:

image

This value is so infinitesimally close to zero that for all practical purposes, it can be considered zero.

Conclusion
While it’s theoretically possible for two UUIDs to collide, the probability is so small with a reasonable number of UUIDs that it’s generally considered negligible. In real-world applications, other issues (e.g., faulty random number generators, implementation errors) are more likely to cause problems than a genuine random collision.

I’m a little confused by what you mean by this. You make it sound like you still use guid = Guid.NewGuid(). The issue isn’t collision of guids, the issue is that revit creates and saves updaters based on guids. The Guid.NewGuid() method generates a completely different guid each time, so you will always have a new updater being registered.

Just to be completely clear with what I’m trying to say here’s what I mean by creating a guid and saving the guid in the script.


^^^
Do this and copy paste the guid into the script, so your guid is static.

guid = Guid("010e9cde-81c2-4080-982d-32e1ef823b0d")

Edit:
Nevermind. I just re-read your post and I think you mean exactly what I just tried to demonstrate. LOL

1 Like

Yes you are correct thats how I´m doing it :slight_smile:

1 Like

After my first days with the iUpdater here are some problems i encountered regarding the triggers and how specific (or not) they can be.

I now have 3 different, working iUpdaters:

  1. Trigger if View Range (Cut Plane Offset) of a Plan View is modified:
    First, the filter for the trigger can only filter by BICs, so I can only use OST_Views, that means the trigger will run for all views, ~70% are sections that are not the target of my code.
    Second, I can not set up a GetChangeTypeParameter trigger because the BuiltinParameters that exist for the View Range (PLAN_VIEW_CUT_PLANE_HEIGHT, VIEW_DEPTH, PLAN_VIEW_TOP_CLIP_HEIGHT, etc) do not work with the API, the can´t be read (result= None) and therefore can´t be used for a trigger.
    This means I have to make a GetChangeTypeAny trigger. So this trigger will now run for all views and every single change. That´s really bad :frowning: Is there any way to improve that? I don´t think i can use it like that…

  2. Trigger if door family or specific generic model family is changed in size:
    Here I use GetChangeTypeGeometry triggers, for doors it works great because i just use OST_Doors. The generic model family is a problem because I can not set a trigger for a specific family, i have to trigger OST_GenericModel, so many unnecessary runs of the updater will occur. But i think that will be OK.

  3. Trigger if Detail Number of a View is changed:
    I can´t believe it, BuiltInParameter.VIEWPORT_DETAIL_NUMBER works perfectly as parameter trigger :smiley: No unnecessary runs here, just pure joy!

Would be really interrested in your thoughts @Nicholas.Miles

Another big question is, should I run 3 iUpdaters parallel, or should I put all together in one script?

kind regards!

And another question:

Is it possible to use a smartbutton for registering/unregistering an updater?
I never used a smartbutton before so I don´t get the concept of the code…

@Gerhard.P
You ought to create another post for this question

That being said check the documentation Notion – The all-in-one workspace for your notes, tasks, wikis, and databases.
There are a few smart buttons in the main set of tools and the de tools as well.

The change of icon happens in the self-init function

1 Like

@Gerhard.P

I took a little time to see if there may be a way to do this, but I don’t think there would be a way to do this without saving all the views in the model and their view ranges. The updater would then have to do a check against the saved views to determine if there has a been a change. It wouldn’t be too difficult to do this if you need it.

It appears the PlanViewRange class is what is responsible for defining a views view range. Because of that I looked at the possibility of having the updater trigger on any changes to any PlanViewRange (If that’s even possible. I didn’t test it). However, it appears there is no way to get which view the PlanViewRange is attached to, so that did not seem like it would work.

Thanks for your reply @Nicholas.Miles

I don´t quiet understand, doesn´t that mean I would still have to trigger at any change? Or save the view range info at any change? So there would be no real benefit.

What do you mean by “save”? write that info to a .csv?