Value within Class resets to base value

Hey,

I created a dockable panel and the basics work just fine.
When i select an element in revit i get a message with the name of the selected element ( Trigger_ElementSelection).
when i click the button next (With the element still selected) it puts out message [5] just as the base value of CurrentSelection is. (ButtonClicked_1)
How can i update a var inside a definition?

import System
import os.path as op
from pyrevit import HOST_APP, framework, coreutils, PyRevitException
from pyrevit import revit, DB, UI
from pyrevit import forms, script
from pyrevit.framework import wpf, ObservableCollection
from System.Windows.Media import Brushes
import subprocess
import sys
import os

def MM_to_FEET(a):
    return a * 0.00328084

class _WPFPanelProvider(UI.IDockablePaneProvider):
    def __init__(self, panel_type, default_visible=True):
        self._panel_type = panel_type
        self._default_visible = default_visible
        self.panel = self._panel_type()

    def SetupDockablePane(self, data):
        data.FrameworkElement = self.panel
        data.VisibleByDefault = self._default_visible

def register_dockable_panel(panel_type, default_visible=True):
    if not issubclass(panel_type, forms.WPFPanel):
        raise PyRevitException(
            "Dockable pane must be a subclass of forms.WPFPanel"
            )

    panel_uuid = coreutils.Guid.Parse(panel_type.panel_id)
    dockable_panel_id = UI.DockablePaneId(panel_uuid)
    panel_provider = _WPFPanelProvider(panel_type, default_visible)
    HOST_APP.uiapp.RegisterDockablePane(
        dockable_panel_id,
        panel_type.panel_title,
        panel_provider
        )
    return panel_provider.panel




class DockableExample(forms.WPFPanel):
    panel_source = op.join(op.dirname(__file__), "MainWindow.xaml")
    panel_title = "Dockable Pane Sample"
    panel_id = "3110e336-f81c-4927-87da-4e0d30d4d64a"
    BaseDockablePanelsPath = "L:/REVIT/PyRevit/DockablePanels/"
    DockablePanelName = "PanelOne/"
    BasePath = BaseDockablePanelsPath + DockablePanelName   
    
    def __init__(self):
        super(DockableExample, self).__init__()
        self.CurrentSelection = [5]
    
     
    def ExecuteScript(self, ScriptName, DefName):
        with open(self.BasePath + ScriptName, 'r') as file:
            code = file.read()
        context = {}
        exec(code, context)
        return context.get(DefName)

    def Trigger_ElementSelection(self, sender, args):
        self.CurrentSelection = (self.ExecuteScript("GetCurrentSelection.py", "CurrentSelection_FamilyInstance"))()
        print(self.CurrentSelection)  
    
    def ButtonClicked_1(self, sender, args):
        print(self.CurrentSelection)
        #if not self.CurrentSelection == []:        
        #    print(self.ExecuteScript("GetParameterValue.py", "GetParameterValueByName"))#(self.CurrentSelection, "Annotation Offset RL")
        #    #(self.ExecuteScript("SetParameterValue.py", "SetParameterValueByName"))(self.CurrentSelection, "Annotation Offset RL", MM_to_FEET(+1.5))
        
        #forms.alert("3")
        
 



registered_panel = register_dockable_panel(DockableExample)

dockable_example_instance = DockableExample()  # Instantiate DockableExample

HOST_APP.uiapp.SelectionChanged += dockable_example_instance.Trigger_ElementSelection  # Add event handler


If you print(self), do you get the same instance (look for the address value, it should be the same)?

Try to move the event registration inside the __init__ of the class and remove it in the __del__ method

Thank you so much.

That did it!

Next thing is im trying to edit some elements in the active model.
I’ve tried with the ExecuteScript Def. Reading info works fine but writing is a problem.
Next ive tried the bit of code that is located in Def ButtonClicked_1.
But even the word “Test” does not print.
My current code:

import System
import os.path as op
from pyrevit import HOST_APP, framework, coreutils, PyRevitException
from pyrevit import revit, DB, UI
from pyrevit import forms, script
from pyrevit.framework import wpf, ObservableCollection
from System.Windows.Media import Brushes
import subprocess
import sys
import os

from Autodesk.Revit.DB import Transaction

def MM_to_FEET(a):
    return a * 0.00328084


def Flatten(List):
    NewList = []
    for Item in List:
        if isinstance(Item,list): NewList.extend(Flatten(Item))
        else: NewList.append(Item)
    return NewList



def SetParameterValueByName(Elements, ParameterName, ParameterWaarde):
                Parameters = Flatten(Elements.GetParameters(ParameterName))
                for Parameter in Parameters:                    
                    Parameter.Set(ParameterWaarde)



class _WPFPanelProvider(UI.IDockablePaneProvider):
    def __init__(self, panel_type, default_visible=True):
        self._panel_type = panel_type
        self._default_visible = default_visible
        self.panel = self._panel_type()

    def SetupDockablePane(self, data):
        data.FrameworkElement = self.panel
        data.VisibleByDefault = self._default_visible

def register_dockable_panel(panel_type, default_visible=True):
    if not issubclass(panel_type, forms.WPFPanel):
        raise PyRevitException(
            "Dockable pane must be a subclass of forms.WPFPanel"
            )

    panel_uuid = coreutils.Guid.Parse(panel_type.panel_id)
    dockable_panel_id = UI.DockablePaneId(panel_uuid)
    panel_provider = _WPFPanelProvider(panel_type, default_visible)
    HOST_APP.uiapp.RegisterDockablePane(
        dockable_panel_id,
        panel_type.panel_title,
        panel_provider
        )
    return panel_provider.panel


class DockableExample(forms.WPFPanel):
    panel_source = op.join(op.dirname(__file__), "MainWindow.xaml")
    panel_title = "Dockable Pane Sample"
    panel_id = "3110e336-f81c-4927-87da-4e0d30d4d64a"
    BaseDockablePanelsPath = "L:/REVIT/PyRevit/DockablePanels/"
    DockablePanelName = "PanelOne/"
    BasePath = BaseDockablePanelsPath + DockablePanelName   
    
    def __init__(self):
        super(DockableExample, self).__init__()
        #self.CurrentSelection = [5]
        HOST_APP.uiapp.SelectionChanged += self.Trigger_ElementSelection  # Register event handler
    def __del__(self):
        HOST_APP.uiapp.SelectionChanged -= self.Trigger_ElementSelection  # Unregister event handler
    
    def ExecuteScript(self, ScriptName, DefName):
        with open(self.BasePath + ScriptName, 'r') as file:
            code = file.read()
        context = {}
        exec(code, context)
        return context.get(DefName)

    def Trigger_ElementSelection(self, sender, args):
        self.CurrentSelection = (self.ExecuteScript("GetCurrentSelection.py", "CurrentSelection_FamilyInstance"))()[0]
        #print(self.CurrentSelection)  
        
    
    def ButtonClicked_1(self, sender, args):
        doc = __revit__.ActiveUIDocument.Document
        print(self.CurrentSelection)
        transact = Transaction(doc, "HEK - TEMPLATE")
        transact.Start()
        print("Test")
        Parameters = (self.CurrentSelection.GetParameters("Switch Code"))
        print(Parameters)
        for Parameter in Parameters:                    
            Parameter.Set("HG")
        transact.Commit()
        #if not self.CurrentSelection == []:
            #print((self.ExecuteScript("GetParameterValue.py", "GetParameterValueByName"))(self.CurrentSelection, "Annotation Offset RL"))
        #print((self.ExecuteScript("SetParameterValue.py", "SetParameterValueByName")))#(self.CurrentSelection, "Switch Code", "H"))
        
        #forms.alert("3")




registered_panel = register_dockable_panel(DockableExample)

dockable_example_instance = DockableExample()  # Instantiate DockableExample

#HOST_APP.uiapp.SelectionChanged += dockable_example_instance.Trigger_ElementSelection  # Add event handler







First stupid question: why do you load the script like this instead of importing the module?

Using the exec function is generally considered a bad practice, as it poses security risks (well, everything in python is interpreted, so it is a vulnerable language by default, but it’s better to reduce those risks).

If you need to use different functions for different dockable panes, just pass the functions as parameter in the class __init__, store it in an attribute and then call it in your event handler:

from GetCurrentSelection import my_selection_callback
# ...
class DockableExample(forms.WPFPanel):
    # ...
    def __init__(self, selection_callback):
        super(DockableExample, self).__init__()
        self._selection_callback = selection_callback
        HOST_APP.uiapp.SelectionChanged += self.Trigger_ElementSelection  # Register event handler
    # ...
    def Trigger_ElementSelection(self, sender, args):
        self.CurrentSelection = self._selection_callback()
# ...
dockable_example_instance = DockableExample(my_selection_callback)

my_selection_callback should be a function inside the GetCurrentSelection.py module that directly returns what you need.

Also, you can leverage the SelectionChangedEventArgs object (your args argument of the Trigger_ElementSelection method) to retrieve the document (args.GetDocument()) and selected elements ids (args.GetSelectedElements()). Maybe using these will solve your current problem - I did develop a modeless window inside Revit a long time ago, so I don’t remember exactly what I did, but IIRC the window/pane runs in a separate thread than the main revit/pyrevit process, so the __revit__ object could be not what you think…

Just a friendly advice: stick to a naming style convention and try not to mix them. This will make your code more readable for others and your future self.

  • python uses PascalCase for classes (only the name) and snake_case for everything else (modules, variables, functions, class methods/attributes);
  • .NET uses camelCase for variables and PascalCase for everything else (more or less).

Obviously if you’re calling functions/methods or subclassing classes from other libraries, you have to use their exact name, but when you’re creating them, use a consistent style :wink:

2 Likes

Thanks for your response.
I’ve been trying and failing at this for a few days now.
Im not that woried about the safety of running a script from within another script.
I’m the only person in my company that writes code and this code does not leave our servers.
As for the namestyle of my code: I think i’ll adopt the style you describe but its a lot of work to fix it since i have a template file with about 3k lines of definitions.

The thing i want to do is create a dockable window with multible buttons and each of the buttons runs a different script. Kinda like how the toolbars work.
The script i linked in ButtonClicked_1 is a very simple parameter write script.
It works if I run it from the toolbar but cant seem to get it to work when running it from my startup script.

This is my startup script

import System
import os.path as op
from pyrevit import HOST_APP, framework, coreutils, PyRevitException
from pyrevit import revit, DB, UI
from pyrevit import forms, script
from pyrevit.framework import wpf, ObservableCollection
from System.Windows.Media import Brushes
import subprocess
import sys
import os

from Autodesk.Revit.DB import Transaction


class _WPFPanelProvider(UI.IDockablePaneProvider):
    def __init__(self, panel_type, default_visible=True):
        self._panel_type = panel_type
        self._default_visible = default_visible
        self.panel = self._panel_type()

    def SetupDockablePane(self, data):
        data.FrameworkElement = self.panel
        data.VisibleByDefault = self._default_visible

def register_dockable_panel(panel_type, default_visible=True):
    if not issubclass(panel_type, forms.WPFPanel):
        raise PyRevitException(
            "Dockable pane must be a subclass of forms.WPFPanel"
            )

    panel_uuid = coreutils.Guid.Parse(panel_type.panel_id)
    dockable_panel_id = UI.DockablePaneId(panel_uuid)
    panel_provider = _WPFPanelProvider(panel_type, default_visible)
    HOST_APP.uiapp.RegisterDockablePane(
        dockable_panel_id,
        panel_type.panel_title,
        panel_provider
        )
    return panel_provider.panel


class DockableExample(forms.WPFPanel):
    panel_source = op.join(op.dirname(__file__), "MainWindow.xaml")
    panel_title = "Dockable Pane Sample"
    panel_id = "3110e336-f81c-4927-87da-4e0d30d4d64a"
    BaseDockablePanelsPath = "L:/REVIT/PyRevit/DockablePanels/"
    DockablePanelName = "PanelOne/"
    BasePath = BaseDockablePanelsPath + DockablePanelName   
    
    def __init__(self):
        super(DockableExample, self).__init__()
        HOST_APP.uiapp.SelectionChanged += self.Trigger_ElementSelection  # Register event handler
        
    def __del__(self):
        HOST_APP.uiapp.SelectionChanged -= self.Trigger_ElementSelection  # Unregister event handler
    
    def ExecuteScript(self, ScriptName, DefName):
        with open(self.BasePath + ScriptName, 'r') as file:
            code = file.read()
        context = {}
        exec(code, context)
        return context.get(DefName)

    def Trigger_ElementSelection(self, sender, args):
        self.CurrentSelection = (self.ExecuteScript("GetCurrentSelection.py", "CurrentSelection_FamilyInstance"))()[0]
    
    def ButtonClicked_1(self, sender, args):
        print(self.CurrentSelection)
        with open("C:/Documenten/Revit Toolbar/Tab P-BEH Melle/Melle.extension/P-BEH.tab/Persoonlijk MHo.panel/TestKnoppen.splitpushbutton/TestKnop1.pushbutton/script.py") as f:
            exec(f.read())
        
        



registered_panel = register_dockable_panel(DockableExample)

dockable_example_instance = DockableExample()  # Instantiate DockableExample

And this is the script i am trying to run

#-------------------------IMPORTS------------------------------------------------------------------
import clr
clr.AddReference("RevitServices")
clr.AddReference('RevitNodes')
clr.AddReference('RevitAPI')

import Autodesk
from Autodesk.Revit.DB import Transaction

doc = __revit__.ActiveUIDocument.Document
app = doc.Application
uidoc = __revit__.ActiveUIDocument
uiapp = DocumentManager.Instance.CurrentUIApplication
view = doc.ActiveView


transact = Transaction(doc, "HEK - TEMPLATE")
transact.Start()

def Flatten(List):
    NewList = []
    for Item in List:
        if isinstance(Item,list): NewList.extend(Flatten(Item))
        else: NewList.append(Item)
    return NewList

def ID_ToFamilyInstance(a):
    OUT = []
    try:
        for i in a:
            OUT.append(doc.GetElement(i))
    except:
        return doc.GetElement(a)
    return OUT

def CurrentSelection_FamilyInstance():
    return ID_ToFamilyInstance(uidoc.Selection.GetElementIds())

def SetParameterValueByName(Elements, ParameterName, ParameterWaarde):
    if type(ParameterName) == str:
        try:
            if type(Elements) == list:
                if type(ParameterWaarde) == list:
                    for Element, ParVal in zip(Elements, ParameterWaarde):
                        Parameters = Flatten(Element.GetParameters(ParameterName))
                        for Parameter in Parameters:
                            Parameter.Set(ParVal)
                else:
                    for Element in Elements:
                        Parameters = Flatten(Element.GetParameters(ParameterName))
                        for Parameter in Parameters:
                            Parameter.Set(ParameterWaarde)
            else:        
                Parameters = Flatten(Elements.GetParameters(ParameterName))
                for Parameter in Parameters:
                    Parameter.Set(ParameterWaarde)
        except:
            pass
    elif type(ParameterName) == list:
        try:
            if type(Elements) == list:
                    for Element in Elements:
                        for ParName, ParVal in zip(ParameterName, ParameterWaarde):
                            Parameters = Flatten(Element.GetParameters(ParName))
                            for Parameter in Parameters:
                                Parameter.Set(ParVal)
            else:  
                for ParName, ParVal in zip(ParameterName, ParameterWaarde):      
                    Parameters = Flatten(Elements.GetParameters(ParName))
                    for Parameter in Parameters:
                        Parameter.Set(ParVal)
        except:
            pass
    

if __name__ == '__main__':
    # test1.py executed as script
    # do something
    SEL = CurrentSelection_FamilyInstance()[0]
    SetParameterValueByName(SEL, "Comments", "FF")


transact.Commit()

I still don’t understand why you want to do that instead of just import the mod…

… Oh, I see, you’re trying to use the pushbutton script as is!

I’d recommend you to use another approach:

  • extract the code into one or more modules and place them in the lib folder under your extension folder;
  • don’t use global variables in those modules, rather add the doc and other needed variables as arguments of the functions that use them;
  • in the pushbutton script, import the extracted modules/functions and call them with the doc variable from __revit__ (or pyrevit.revit);
  • in your dockable panel button, call the same functions passing args.GetDocument() to the doc argument and args.GetSelectedElements() as the selection (of ID_ToFamilyInstance for instance)

Other unsolicited advice:

  • don’t wrap everything inside a transaction; Put the transaction inside the if __name__ == "__main__" block, so that when you just importing the module from another module it doesn’t open and close an useless transaction (well, if you rearrange the scripts as I wrote before, the script.py won’t be imported by any other modules, and the extracted function would contain only functions).
  • type(variable) == list is not pythonic, usually you do isinstance(variable, list)
  • you’re repeating yourself a lot, and this is a maintenance nightmare… extract the common parts into a function and call it instead
  • leverage pyrevits goodies to make the code lighter

The common module, let’s call it common.py, would be like

def flatten(old_list):
    new_list = []
    for item in old_list:
        if isinstance(item, list):
            new_list.extend(flatten(item))
        else:
            new_list.append(item)
    return new_list

def id_to_family_instance(a, document):
    try:
        return [document.GetElement(i) for i in a]
    except:
        return document.GetElement(a)

def set_parameters(element, name, value):
    for parameter in flatten(element.GetParameters(name)):
        try:
            parameter.Set(value)
        except:
            pass

def set_parameter_value_by_name(elements, parameter_name, parameter_value):
    if isinstance(parameter_name, str):
        if isinstance(elements, list):
            if isinstance(parameter_value, list):
                for element, value in zip(elements, parameter_value):
                    set_parameters(element, parameter_name, value)
            else:
                for element in elements:
                    set_parameters(element, parameter_name, parameter_value)
        else:
            set_parameters(elements, parameter_name, parameter_value)
    elif isinstance(parameter_name, list):
        if isinstance(elements, list):
            for element in elements:
                for name, value in zip(parameter_name, parameter_value):
                    set_parameters(element, name, value)
        else:  
            for name, value in zip(parameter_name, parameter_value):
                set_parameters(elements, name, value)

the pushbutton script.py reduces to

from pyrevit import revit
from pyrevit.revit import Transaction
import common

if __name__ == '__main__':
    doc = revit.doc
    with Transaction(doc, "HEK - TEMPLATE"):
        selection = common.id_to_family_instance(revit.uidoc.Selection.GetElementIds(), doc)[0]
        common.set_parameter_value_by_name(selection, "Comments", "FF")

the dockable pane button would be like

import common
# ...
        def Trigger_ElementSelection(self, sender, args):
            self.current_selection = args.GetSelectedElements()
            self.current_document = args.GetDocument()  # this shouldn't be needed, but just in case...

        def ButtonClicked_1(self, sender, args):
            selection = common.id_to_family_instance(self.current_selection, self.current_document)[0]
            common.set_parameter_value_by_name(selection, "Comments", "FF")
1 Like

Woow omg.

It just… Works now!
Thank you so much for your help and patience :slight_smile:

Now this set_parameter_value_by_name was just an easy thing to do.
The things a want to run are way more complex.
Things like create a custom legend with a lot of data and family’s
The base script works, i just need to link it to this dockable panel.
How would i go about that?
Do i replicate all the actions in ButtonClicked_1 or do i fit all the code in a single definition and load that in ButtonClicked_1.

The only user input is selecting the panel.
What is shown below is based on all circuits connected to the panel

This is just an example of one of many complex scripts.
What would you say is the best way to do this?

I’m sorry I don’t quite get what you’re asking.
No matter how complex a code is, if you can create a function that takes the selection and the document as input arguments, you can call it in the button click method.

In the example before, we could have added a function in the common module

def main(doc, selected_ids):
    with Transaction(doc, "HEK - TEMPLATE"):
        selection = common.id_to_family_instance(selected_ids, doc)[0]
        common.set_parameter_value_by_name(selection, "Comments", "FF")

and call that function in both script and button click method.