Need help with purging Shared Parameters

Hello,

Trying to see if it is possible to purge all the unused shared parameters from project.
I have done a bit of digging and found this on the ReviApi.doc website, but not sure how to implement it into my code

could someone explain or provide an example of how to use this in python please.
Other thing i found is to check if the parameter “HasValue”,
but i am curious to know how to use “ParameterValuePresenceRule”

Thanks for your help

1 Like

@hoss53 ,

check out this topic

it is easy to transform to a pyRevit pushbutton

1 Like

Thanks for your help

1 Like

I have adapted that code into something cleaner for pyRevit. I also added a few extra features. Hopefully it can help others!

  1. Added an option to only remove Project Parameters
  2. Improved filtering so it won’t remove parameters that are used in schedules (even if the schedules are empty)
  3. Result logging.
"""Purge Unused Parameters
Removes unused parameters. Can select between project parameters, shared parameters, or both.

Author: c.poupin, adapted by Perry Lackowski
Tested: 2023.1
"""

import sys
import clr
import System
from System.Collections.Generic import List

# Add references to Revit API and other necessary libraries
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB

clr.AddReference('RevitAPIUI')
from Autodesk.Revit.UI import *

clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Transactions import TransactionManager

from pyrevit import revit, script, forms
doc = revit.doc
output = script.get_output()

###   
### FUNCTION DEFINITIONS
###

# Create a dialog to ask the user which type of parameters to delete
dialog = TaskDialog("Purge Unused Parameters")

# Set dialog properties
dialog.MainInstruction = 'Choose Parameter Type to Delete'
dialog.FooterText = "Note: Only instance parameters will be processed."
# Add command links for user options
dialog.AddCommandLink(TaskDialogCommandLinkId.CommandLink1,
                      'Delete all Parameters',
                      'Deleting all unused Parameters')
dialog.AddCommandLink(TaskDialogCommandLinkId.CommandLink2,
                      'Delete only Shared Parameters',
                      'Deleting Unused Shared Parameters (Only)')
dialog.AddCommandLink(TaskDialogCommandLinkId.CommandLink3,
                      'Delete only Project Parameters',
                      'Deleting Unused Project Parameters (Only)')

# Show the dialog and get the user's choice
diag = dialog.Show()

# Force close any open transactions to ensure a clean slate
TransactionManager.Instance.ForceCloseTransaction()

# Function to collect all schedule parameters
def get_schedule_parameters(doc):
    schedule_parameters = set()
    schedules = FilteredElementCollector(doc).OfClass(ViewSchedule).ToElements()
    for schedule in schedules:
        schedule_definitions = schedule.Definition.GetFieldOrder()
        for field_id in schedule_definitions:
            field = schedule.Definition.GetField(field_id)
            schedule_parameters.add(field.ParameterId)
    return schedule_parameters

# Function to check if a parameter is used in any schedule
def is_parameter_in_schedule(definition, schedule_parameters):
    return definition.Id in schedule_parameters

# Function to generate the list of parameters to remove
def get_unused_parameters(doc, schedule_parameters):
    def_toRemove = []
    iterator =  doc.ParameterBindings.ForwardIterator()
    while iterator.MoveNext():
        definition = iterator.Key
        binding = iterator.Current
        if definition.IsValidObject:
            if isinstance(binding, InstanceBinding):
                categories = binding.Categories
                # Create a filter to select elements in the categories of the binding
                filterCats = ElementMulticategoryFilter(List[ElementId]([cat.Id for cat in categories]))
                # Create a predicate to filter elements that have a value for the current parameter definition
                filterByHasValue = System.Predicate[System.Object](lambda x : x.get_Parameter(definition) is not None and x.get_Parameter(definition).HasValue)
                # Find all elements that have a null value for the current parameter definition
                instanceNullValues = List[DB.Element](FilteredElementCollector(doc).WherePasses(filterCats).WhereElementIsNotElementType().ToElements()).FindAll(filterByHasValue)
                # If no elements have a value for the parameter and it's not in a schedule, mark it for removal
                if len(instanceNullValues) == 0 and not is_parameter_in_schedule(definition, schedule_parameters):
                    def_toRemove.append(definition)
    return def_toRemove

# Function to remove parameters
def remove_parameters(doc, unused_parameters, diag):
    result = []
    for param in unused_parameters:
        if hasattr(param, "Id"):
            
            filterbyId = System.Predicate[System.Object](lambda x : x.Id == param.Id)
            
            # Create a list of the 
            all_params = List[DB.Element](FilteredElementCollector(doc).OfClass(ParameterElement).WhereElementIsNotElementType().ToElements()).Find(filterbyId)
            shared_params = List[DB.Element](FilteredElementCollector(doc).OfClass(SharedParameterElement).WhereElementIsNotElementType().ToElements()).Find(filterbyId)
            
            param_type = "param_type not set"
            if all_params is not None and shared_params is not None:
                param_type = "Shared"
            if all_params is not None and shared_params is None:
                param_type = "Project"
            
            deletion_approved = False

            if diag == TaskDialogResult.CommandLink1:
                deletion_approved = True

            elif diag == TaskDialogResult.CommandLink2:
                if param_type == "Shared":
                    deletion_approved = True

            elif diag == TaskDialogResult.CommandLink3:
                if param_type == "Project":
                    deletion_approved = True

            # If the parameter element is found, delete it
            if deletion_approved:
                doc.Delete(param.Id)
                result.append([param.Name, param_type, output.linkify(param.Id)])
    return result

###   
### MAIN CODE
###

# Collect all parameters that are in use on a schedule or schedule template.
schedule_parameters = get_schedule_parameters(doc)

# Generate the list of parameters to remove
unused_parameters = get_unused_parameters(doc, schedule_parameters)

# Start a transaction to remove the parameter definitions
t = Transaction(doc, "Remove Parameters")
t.Start()

results = remove_parameters(doc, unused_parameters, diag)

t.Commit()    
t.Dispose()

# OUT = results

if results:
    output.print_md("**{} parameter(s) removed.**".format(len(results)))

    #Sort by type then name
    results = sorted(results, key=lambda x: (x[1], x[0]))

    #print results
    for name, type, link in results:
        print("{} Parameter: {} - '{}'".format(type, link, name))

    output.print_md("**{} parameter(s) removed. Script complete.**".format(len(results)))
else:
    forms.alert('No parameters found for removal.')
2 Likes

I made some further improvements.

  1. This version works from project files and the family editor.
  2. You can now choose which parameters to purge from a list. The list also describes if the parameters are shared/non-shared, so you can filter and select one type or the other or all of them.
  3. More parameters are excluded. Anything used in table schedules, panel schedules, sheet properties, and view filters gets filtered out. I wanted to extend it to exclude any parameters that are used by tag families in the project, but the parameters in a tag’s label are currently inaccessible in the API.

Future work could be done to give the user a list of all the parameters and explain where they are used, but that would mean setting up data objects to track that info, and building a UI to display it, which are both fairly time consuming.

"""Purge Unused Parameters
Select parameters for removal from a list of all the unused parameters in the active project or family. 

Author: c.poupin, adapted by Perry Lackowski
Tested: 2023.1
"""

import sys
import clr
import System
from System.Collections.Generic import List

# Add references to Revit API and other necessary libraries
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB

clr.AddReference('RevitAPIUI')
from Autodesk.Revit.UI import *

clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Transactions import TransactionManager

from pyrevit import revit, script, forms
doc = revit.doc
output = script.get_output()

###   
### FUNCTION DEFINITIONS
###

# Generate a set of all parameter ids that are used in any of the table Schedules
def get_schedule_parameter_ids(doc):
    
    schedule_parameter_ids = set()
    schedules = DB.FilteredElementCollector(doc).OfClass(DB.ViewSchedule).ToElements()
    for schedule in schedules:
        schedule_definitions = schedule.Definition.GetFieldOrder()
        for field_id in schedule_definitions:
            field = schedule.Definition.GetField(field_id)
            schedule_parameter_ids.add(field.ParameterId)
    return schedule_parameter_ids
    
# Generate a set of all parameter ids that are used in any of the Sheet properties
def get_sheet_parameter_ids(doc):
    sheet_parameter_ids = set()
    sheets = DB.FilteredElementCollector(doc).OfClass(DB.ViewSheet).ToElements()
    for sheet in sheets:
        parameters = sheet.Parameters
        for param in parameters:
            sheet_parameter_ids.add(param.Id)
    return sheet_parameter_ids

# Generate a set of all parameter ids that are used in any of the Panel Schedules
def get_panel_schedule_parameter_ids(doc):
    
    panel_schedule_parameter_ids = set()

    # Get all panel schedule templates
    panel_schedules = DB.FilteredElementCollector(doc).OfClass(DB.Electrical.PanelScheduleTemplate).WhereElementIsNotElementType().ToElements()

    # Define the section types to iterate through
    section_types = [DB.SectionType.Header, DB.SectionType.Body, DB.SectionType.Footer, DB.SectionType.Summary]

    for panel_schedule in panel_schedules:
        # print("Panel Schedule: {}".format(panel_schedule.Name))
        
        for section_type in section_types:
            # print("Section Type: {}".format(section_type))
            
            # Get the section data
            section_data = panel_schedule.GetSectionData(section_type)
            
            # Iterate through rows and columns to get cell data
            for row in range(section_data.NumberOfRows):
                for col in range(section_data.NumberOfColumns):
                    cell_data = section_data.GetCellText(row, col)
                    param_id = section_data.GetCellParamId(row, col)
                    
                    if cell_data:
                        # print("Row {}, Column {}: {}".format(row, col, cell_data))
                        pass
                    
                    if param_id.IntegerValue != -1:
                        param = doc.GetElement(param_id)
                        if param:
                            param_name = param.Name
                            # print("Row {}, Column {}: Parameter Name: {}".format(row, col, param_name))
                            panel_schedule_parameter_ids.add(param_id)
    
    return panel_schedule_parameter_ids

# Generate a set of all parameter ids that are used in any of the View Filters
def get_view_filter_parameter_ids(doc):
    # ParameterFilterElement or (View Filters)
    parameter_filter_elements = DB.FilteredElementCollector(doc).OfClass(DB.ParameterFilterElement).ToElements()

    parameter_ids_used_in_project_view_filters = set()
    for parameter_filter_element in parameter_filter_elements:
            # print("{} {}".format(output.linkify(parameter_filter_element.Id), parameter_filter_element.Name))

            # View Filters have two ways to access the parameters they use. 
            # the first lists the parameters that apply to all categories...
            parameter_ids_used_in_all_categories = parameter_filter_element.GetElementFilterParameters()
            for parameter_id_used_in_all_categories in parameter_ids_used_in_all_categories:
                    parameter_ids_used_in_project_view_filters.add(parameter_id_used_in_all_categories)

            # ...and the second lists only parameters that belong to specific
            # categories. In the case of our project template, these have all
            # been empty, but it doesn't hurt to add them anyway.
            category_ids = parameter_filter_element.GetCategories()
            for category_id in category_ids:
                parameter_ids_used_in_category = parameter_filter_element.GetElementFilterParametersForCategory(category_id)
                # print(parameter_ids_used_in_category)
                # print(parameter_ids_used_in_category.Count)
                # parameter_ids_used_in_category = [x.Id for x in parameter_ids_used_in_category]
                for parameter_id_used_in_category in parameter_ids_used_in_category:
                    parameter_ids_used_in_project_view_filters.add(parameter_id_used_in_category)

    # Note: some of these will likely be built in parameters, with negative ids.
    # I could probably filter them out but I don't think they are harming anything.
    return parameter_ids_used_in_project_view_filters

# Function to generate the list of unused parameters in a project.
# This actually returns parameter InternalDefinitions, to be specific.
def get_unused_project_parameters(doc):
    assert not doc.IsFamilyDocument

    # Collect all parameters that are in use on a schedule or schedule template or sheet.
    used_parameter_ids = set()
    used_parameter_ids.update(get_schedule_parameter_ids(doc))
    used_parameter_ids.update(get_panel_schedule_parameter_ids(doc))
    used_parameter_ids.update(get_sheet_parameter_ids(doc))
    used_parameter_ids.update(get_view_filter_parameter_ids(doc))

    def_toRemove = []
    parameter_iterator = doc.ParameterBindings.ForwardIterator()

    while parameter_iterator.MoveNext():
        parameter_internal_def = parameter_iterator.Key
        binding = parameter_iterator.Current
        if parameter_internal_def.IsValidObject and isinstance(binding, InstanceBinding):
            if parameter_internal_def.Id in used_parameter_ids: #skip these items
                continue
            categories = binding.Categories # type categories that contain the parameter
            filterCats = DB.ElementMulticategoryFilter(List[DB.ElementId]([cat.Id for cat in categories]))
            #check if any elements in the bound categories have a value
            elements_using_param = [
                elem for elem in DB.FilteredElementCollector(doc).WherePasses(filterCats).WhereElementIsNotElementType().ToElements()
                if elem.get_Parameter(parameter_internal_def) is not None and elem.get_Parameter(parameter_internal_def).HasValue
            ]
            if not elements_using_param:                
                def_toRemove.append(doc.GetElement(parameter_internal_def.Id))
    return def_toRemove

# Function to generate the list of unused parameters in a single family.
# This actually returns a list of FamilyParameters, to be specific.
def get_unused_family_parameters(doc):

    # Get all family parameters
    family_manager = doc.FamilyManager

    # Dictionary to store parameters that are blank for all types
    unused_parameters = {}
    all_parameters = []

    #Code is a bit complex here because if you want to see the values for the parameters in a family editor, you can only see one set of values at a time.
    # if you want to see the values for a different Type, you need to switch the CurrentType, then access the parameters again. Unlike in a project setting,
    # since we're approaching from the CurrentType in the family manager instead of from the parameters themselves (which have no accessible values in the family setting),
    # you can't see what storage type the parameters are. This is why you have to check all the types AsString, AsDouble, AsInteger, etc.

    # Iterate over all family types
    for family_type in family_manager.Types:
        
        family_manager.CurrentType = family_type
        
        # print(" ------- {} ------ ".format(family_type.Name))

        for param in family_manager.Parameters:
            
            if param.Definition.BuiltInParameter != DB.BuiltInParameter.INVALID:
                continue
            
            param_name = param.Definition.Name
        
            # Initialize the dictionary entry if it doesn't exist
            if param_name not in unused_parameters:
                unused_parameters[param_name] = 0
                all_parameters.append(param)
            
            if family_manager.CurrentType.AsValueString(param) == None and \
               (family_manager.CurrentType.AsString(param) == None or family_manager.CurrentType.AsString(param) == "") and \
               family_manager.CurrentType.AsDouble(param) == None and \
               family_manager.CurrentType.AsElementId(param) == DB.ElementId.InvalidElementId and \
               family_manager.CurrentType.AsInteger(param) == None:
                pass
            else:
                unused_parameters[param_name] += 1

            # print("{}: {}, {}, {}, {}, {}, {} {}".format(param_name,
            #                                       param.StorageType,
            #                                       param_value_string,
            #                                       family_manager.CurrentType.AsString(param),
            #                                       family_manager.CurrentType.AsDouble(param),
            #                                       family_manager.CurrentType.AsInteger(param), 
            #                                       family_manager.CurrentType.AsElementId(param),
            #                                       result
            #                                       ))

    # Collect the parameters that are blank for all family types
    blank_parameters = [param for param in all_parameters \
                        if unused_parameters[param.Definition.Name] == 0]
    
    return blank_parameters

# Function to remove project parameters
def remove_unused_parameters_from_project(doc):
    # Generate the list of parameters to remove
    unused_parameters = get_unused_project_parameters(doc)

    if not unused_parameters: return None

    parameter_tuples = []
    for param in unused_parameters:

        if param.GetType() == DB.ParameterElement:
            param_type = "Project Parameter"
        elif param.GetType() == DB.SharedParameterElement: # otherwise it would be a DB.SharedParemeterElement
            param_type = "Shared Project Parameter"
        else:
            param_type = "param_type_not_set"

        parameter_tuples.append((param, param.Name, param_type))
    
    parameter_tuples_to_delete = display_and_select_parameters(parameter_tuples)
    
    result = delete_parameters(parameter_tuples_to_delete)

    return result


# Function to remove family parameters
def remove_unused_parameters_from_family(doc):

    unused_parameters = get_unused_family_parameters(doc)

    if not unused_parameters: return None
    
    parameter_tuples = []
    for param in unused_parameters:
        if param.IsShared:
            param_type = "Shared Family Parameter"
        else:
            param_type = "Family Parameter"
        parameter_tuples.append((param, param.Definition.Name, param_type))

    parameter_tuples_to_delete = display_and_select_parameters(parameter_tuples)

    result = delete_parameters(parameter_tuples_to_delete)

    return result


def delete_parameters(param_tuple_list):
    result = []
    # Force close any open transactions to ensure a clean slate
    TransactionManager.Instance.ForceCloseTransaction()

    for param, param_name, param_type in param_tuple_list:
        result.append([param_name, param_type, output.linkify(param.Id)])
        doc.Delete(param.Id)

    return result


# Used with forms.SelectFromList to convert the parameter tuples into a 
# usable string for the dialog box.
class MyOptionParamTuplesToListItems(forms.TemplateListItem):
    @property
    def name(self):
        param, param_name, param_type = self.item
        return '{} ({})'.format(param_name, param_type)

def display_and_select_parameters(param_tuples):

    list_items = [MyOptionParamTuplesToListItems(x) for x in param_tuples]
    
    # print(list_items)

    list_items = sorted(list_items, key=lambda x: x.name)

    # Use pyRevit forms to display a checklist dialog
    selected_param_tuples = forms.SelectFromList.show(
        list_items,
        title="Select Unused Parameters to Purge",
        multiselect=True
    )
    
    # If nothing was selected, exit early
    if not selected_param_tuples:
        t.Dispose()        
        sys.exit()
        
    return selected_param_tuples


###   
### MAIN CODE
###

# Start a transaction to remove the parameters
t = DB.Transaction(doc, "Remove Parameters")
t.Start()

if doc.IsFamilyDocument:
    results = remove_unused_parameters_from_family(doc)
else:
    results = remove_unused_parameters_from_project(doc)

t.Commit()    

if results:
    output.print_md("**{} parameter(s) removed.**".format(len(results)))

    #Sort by type then name
    results = sorted(results, key=lambda x: (x[1], x[0]))

    #print results
    for name, type, link in results:
        print("{}: {} - {}".format(type, link, name))

    output.print_md("**{} parameter(s) removed. Script complete.**".format(len(results)))
else:
    forms.alert('No parameters found for removal.')