Conditional dropdown in Tool Validator based on user input

1641
7
Jump to solution
11-03-2020 06:34 PM
AllisonClark
New Contributor II

This picks up from Conditional Drop Down Lists - Tool Validator 

My basic question is, how do you write a script tool parameter window that takes data entry input as a filter on a feature class field? And then lets the user select values from the filtered output. 

My script tool is meant to prompt the user to enter the first six digits of a parcel ID, such as '0401 01' and then uses that to filter a list of parcels pulled from the PARCELS_BASE feature class. I tried to reuse the code from https://community.esri.com/thread/210869-conditional-drop-down-lists-tool-validator

because it's similar, but could not get it to filter based on data entry. 

Here's my most recent iteration. In my situation, the feature class and field are fixed, so I tried substituting in the hard-coded names, leaving just the one input parameter, GridQuadDbl, for user data entry. I expected this to display the filtered list as the OutValue. All I got was an error on the line where I invoked SearchCursor, saying that NoneType object has no attribute 'list'. That's going to be something dumb and obvious, but it's insane trying to debug inside Tool Validator, and it's late, and I'm tired.

First, the attribute table, with PIN being the field where I'm trying to filter the results: 

The parameter window when you run the tool:

Next, the parameters (ignore debugger) 

The code:  

class ToolValidator(object):

    def __init__(self):
        self.params = arcpy.GetParameterInfo()

    def initializeParameters(self):
        
    def updateParameters(self):
        # set values for fc and field
        fc, col = "PARCELS_BASE", "PIN"

        # get value for grid-quad-dbl
        if self.params[0].value:
            wc = "{0} LIKE '{1}%'".format(col, str(self.params[0].value))
            self.params[2].value = wc
            self.params[1].filter.list = [str(val) for val in sorted(
                set(row.getValue(col) for row in arcpy.SearchCursor(fc, fields=col, where_clause=wc)))]

        if self.params[1].value not in self.params[1].filter.list:
            self.params[1].value = self.params[1].filter.list[0]

        return
0 Kudos
2 Solutions

Accepted Solutions
RandyBurton
MVP Regular Contributor

I was experimenting with some code that uses a LIKE in the where clause.  It pulls parts of the PIN from the data and loads it into dropdowns for the user to select from.   Hopefully, it is similar enough to your project that it will give you some ideas.  Here's the validator section:

import arcpy
class ToolValidator(object):

    def __init__(self):
        import arcpy
        self.params = arcpy.GetParameterInfo()

    def initializeParameters(self):
        # (initializeParameters code here)
        return

    def updateParameters(self):

        if self.params[0].value: # feature has been selected
            useFields = ['PIN'] # name of field used by tool (parcel identification number - a string)
            desc = arcpy.Describe(self.params[0].value)  # information about the input feature
            fieldNames = { f.name: f.type for f in desc.fields } # dictionary used to keep field names and types together

            if set(useFields).issubset(fieldNames.keys()): # all fields were found by tool


                # NOTE: if any selections have been made in fc, then only selected features will be searched
                fc, fld = str(self.params[0].value), useFields

                self.params[1].filter.list = sorted(set([f[0][:4] for f in arcpy.da.SearchCursor(fc,fld)]))
                if self.params[1].value not in self.params[1].filter.list:
                    self.params[1].value = self.params[1].filter.list[0]

                where_1 = "PIN LIKE '{}%'".format(self.params[1].value)
                
                self.params[2].filter.list = sorted(set([f[0][5:7] for f in arcpy.da.SearchCursor(fc,fld,where_1)]))
                if self.params[2].value not in self.params[2].filter.list:
                    self.params[2].value = self.params[2].filter.list[0]

                where_2 = "PIN LIKE '{} {}%'".format(self.params[1].value, self.params[2].value)

                self.params[3].filter.list = sorted(set([f[0][8:] for f in arcpy.da.SearchCursor(fc,fld,where_2)]))
                if self.params[3].value not in self.params[3].filter.list:
                    self.params[3].value = self.params[3].filter.list[0]

                # output completed whereClause
                self.params[4].value = "PIN = '{} {} {}'".format(self.params[1].value, self.params[2].value, self.params[3].value)

            else: # at least one of the field names does not exist in feature; using the parameter to hold an error message

                if 'PIN' not in fieldNames.keys():
                    self.params[1].value = "ERROR: Field 'PIN' not in selected feature class."

        return

    def updateMessages(self):
        
        self.params[0].clearMessage()
        self.params[1].clearMessage()
        self.params[2].clearMessage()
        self.params[3].clearMessage()
        if self.params[0].value is not None: # set error message if field not in feature so user can correct problem

            if self.params[1].value is not None:
                if self.params[1].value.startswith("ERROR:"):
                    self.params[1].value = None # clear error message in parameter value, if desired
                    self.params[1].setErrorMessage("Field '{}' is not in Input FC '{}'".format('PIN', self.params[0].value))

        return

For my testing, I used the following script:

import arcpy

inFC = arcpy.GetParameterAsText(0) # input feature class (Input, Data Type 'Feature Layer')
PIN_a = arcpy.GetParameterAsText(1) # PIN[:4]  (Input, Data Type 'String')
PIN_b = arcpy.GetParameterAsText(2) # PIN[5:7] (Input, Data Type 'String')
PIN_c = arcpy.GetParameterAsText(3) # PIN[8:]  (Input, Data Type 'String')
whereClause = arcpy.GetParameterAsText(4) # set by ToolValidator updateParameters (Output, Derived, Data Type 'String')

arcpy.AddMessage("Where clause used: {}".format(whereClause)) 
arcpy.SelectLayerByAttribute_management(inFC, "NEW_SELECTION", where_clause=whereClause)

for i in range(arcpy.GetMessageCount()):  # display messages from SelectLayerByAttribute tool
    arcpy.AddMessage(arcpy.GetMessage(i))

n = arcpy.GetCount_management(inFC)
arcpy.AddMessage("Number of records selected: {}".format(n))

Hope this helps.

View solution in original post

RandyBurton
MVP Regular Contributor

If you want to clear selections in the Tool Validator section, you might try:

    def updateParameters(self):

        if self.params[0].value: # feature has been selected

            arcpy.SelectLayerByAttribute_management(self.params[0].value, "CLEAR_SELECTION")

            # code continues

View solution in original post

7 Replies
RandyBurton
MVP Regular Contributor

I was experimenting with some code that uses a LIKE in the where clause.  It pulls parts of the PIN from the data and loads it into dropdowns for the user to select from.   Hopefully, it is similar enough to your project that it will give you some ideas.  Here's the validator section:

import arcpy
class ToolValidator(object):

    def __init__(self):
        import arcpy
        self.params = arcpy.GetParameterInfo()

    def initializeParameters(self):
        # (initializeParameters code here)
        return

    def updateParameters(self):

        if self.params[0].value: # feature has been selected
            useFields = ['PIN'] # name of field used by tool (parcel identification number - a string)
            desc = arcpy.Describe(self.params[0].value)  # information about the input feature
            fieldNames = { f.name: f.type for f in desc.fields } # dictionary used to keep field names and types together

            if set(useFields).issubset(fieldNames.keys()): # all fields were found by tool


                # NOTE: if any selections have been made in fc, then only selected features will be searched
                fc, fld = str(self.params[0].value), useFields

                self.params[1].filter.list = sorted(set([f[0][:4] for f in arcpy.da.SearchCursor(fc,fld)]))
                if self.params[1].value not in self.params[1].filter.list:
                    self.params[1].value = self.params[1].filter.list[0]

                where_1 = "PIN LIKE '{}%'".format(self.params[1].value)
                
                self.params[2].filter.list = sorted(set([f[0][5:7] for f in arcpy.da.SearchCursor(fc,fld,where_1)]))
                if self.params[2].value not in self.params[2].filter.list:
                    self.params[2].value = self.params[2].filter.list[0]

                where_2 = "PIN LIKE '{} {}%'".format(self.params[1].value, self.params[2].value)

                self.params[3].filter.list = sorted(set([f[0][8:] for f in arcpy.da.SearchCursor(fc,fld,where_2)]))
                if self.params[3].value not in self.params[3].filter.list:
                    self.params[3].value = self.params[3].filter.list[0]

                # output completed whereClause
                self.params[4].value = "PIN = '{} {} {}'".format(self.params[1].value, self.params[2].value, self.params[3].value)

            else: # at least one of the field names does not exist in feature; using the parameter to hold an error message

                if 'PIN' not in fieldNames.keys():
                    self.params[1].value = "ERROR: Field 'PIN' not in selected feature class."

        return

    def updateMessages(self):
        
        self.params[0].clearMessage()
        self.params[1].clearMessage()
        self.params[2].clearMessage()
        self.params[3].clearMessage()
        if self.params[0].value is not None: # set error message if field not in feature so user can correct problem

            if self.params[1].value is not None:
                if self.params[1].value.startswith("ERROR:"):
                    self.params[1].value = None # clear error message in parameter value, if desired
                    self.params[1].setErrorMessage("Field '{}' is not in Input FC '{}'".format('PIN', self.params[0].value))

        return

For my testing, I used the following script:

import arcpy

inFC = arcpy.GetParameterAsText(0) # input feature class (Input, Data Type 'Feature Layer')
PIN_a = arcpy.GetParameterAsText(1) # PIN[:4]  (Input, Data Type 'String')
PIN_b = arcpy.GetParameterAsText(2) # PIN[5:7] (Input, Data Type 'String')
PIN_c = arcpy.GetParameterAsText(3) # PIN[8:]  (Input, Data Type 'String')
whereClause = arcpy.GetParameterAsText(4) # set by ToolValidator updateParameters (Output, Derived, Data Type 'String')

arcpy.AddMessage("Where clause used: {}".format(whereClause)) 
arcpy.SelectLayerByAttribute_management(inFC, "NEW_SELECTION", where_clause=whereClause)

for i in range(arcpy.GetMessageCount()):  # display messages from SelectLayerByAttribute tool
    arcpy.AddMessage(arcpy.GetMessage(i))

n = arcpy.GetCount_management(inFC)
arcpy.AddMessage("Number of records selected: {}".format(n))

Hope this helps.

AllisonClark
New Contributor II

Randy, wow thanks! Your code worked beautifully, ran the Toolvalidator script and then my main geoprocessing script seamlessly. I am reworking it now to display a list of full-length parcels in the 3rd param and then let the user select multiple parcels for running the script. I will post it once I get it working with my changes.

But I have a question on debugging the ToolValidator. How do you structure it so that you can run it as a script in an IDE (Pycharm in my case)? 

I had earlier tried setting it up using the example at the bottom of this page: https://desktop.arcgis.com/en/arcmap/latest/analyze/creating-tools/debugging-a-toolvalidator-class.h...

Then I tried the script you posted at the bottom of your post. Then I tried combining the two. They handle the parameters differently, so I tried it both ways. The problems are mostly about accessing the parameters and feeding them into the Tool Validator script. Here's the debug script. What am I not understanding?  

 

# DEBUGGING SCRIPT FOR TOOLVALIDATOR CODE - 11/15/2020

# create parameter array and values
import arcpy
# Load the toolbox and get the tool's parameters, using the tool name (not the tool label).
tb = r"G:\projects\ocp\pd\env_wetlands\Env_Assessment_maps\Map_compositions\Environmental_Map_Template_dev2\Parcel or Address Search.tbx"
arcpy.ImportToolbox(tb)
params = arcpy.GetParameterInfo("RequestParcelInput")

# set required parameters
# params[0].value = "PARCELS_BASE"
# params[1].value = "0401 01"
# params[2].value = "0401 01 0001"

inFC = arcpy.GetParameterAsText(0) # input feature class (Input, Data Type 'Feature Layer')
PIN = arcpy.GetParameterAsText(1) # PIN[:8] (Input, Data Type 'String')
PIN2 = arcpy.GetParameterAsText(2) # full PIN (Input, Data Type 'String')
whereClause = arcpy.GetParameterAsText(3) # set by ToolValidator updateParameters (Output, Derived, Data Type 'String')

# arcpy.AddMessage("Where clause used: {}".format(whereClause))
arcpy.SelectLayerByAttribute_management(inFC, "NEW_SELECTION", where_clause=whereClause)
for i in range(arcpy.GetMessageCount()): # display messages from SelectLayerByAttribute tool
arcpy.AddMessage(arcpy.GetMessage(i))
# n = arcpy.GetCount_management(inFC)
# arcpy.AddMessage("Number of records selected: {}".format(n))‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

#########################################################################################

class ToolValidator(object):
"""Class for validating a tool's parameter values and controlling
the behavior of the tool's dialog."""

def __init__(self:(
"""Setup arcpy and the list of tool parameters."""
self.params = arcpy.GetParameterInfo()

def initializeParameters(self:(
"""Refine the properties of a tool's parameters. This method is
called when the tool is opened."""

def updateParameters(self:(
"""Modify the values and properties of parameters before internal
validation is performed. This method is called whenever a parameter
has been changed."""

if self.params[0].value: # feature class has been selected
useFields = ['PIN'] # name of field used by tool (parcel identification number - a string)
desc = arcpy.Describe(self.params[0].value) # information about the input feature
fieldNames = {f.name: f.type for f in desc.fields} # dictionary used to keep field names and types together

if set(useFields).issubset(fieldNames.keys()): # all fields were found by tool
# NOTE: if any selections have been made in fc, then only selected features will be searched
fc, fld = str(self.params[0].value), useFields
# GQD unique-values filter from all parcels in PARCELS_BASE
self.params[1].filter.list = sorted(set([f[0][:7] for f in arcpy.da.SearchCursor(fc, fld)]))
if self.params[1].value not in self.params[1].filter.list:
self.params[1].value = self.params[1].filter.list[0]

wc = "PIN LIKE '{}%'".format(self.params[1].value)
self.params[5].value = "querystring: " + wc # display to debugger

self.params[2].filter.list = sorted(set([f[0][:] for f in arcpy.da.SearchCursor(fc, fld, wc)]))
if self.params[2].value not in self.params[2].filter.list:
self.params[2].value = self.params[2].filter.list[0]

# output completed whereClause
self.params[3].value = "PIN = '{}'".format(self.params[2].value)
# self.params[3].value = "PIN = '{} {}'".format(self.params[1].value, self.params[2].value)
self.params[5].value = self.params[3].value # display to debugger

else: # at least one of the field names does not exist in feature; using the parameter to hold an error message

if 'PIN' not in fieldNames.keys():
self.params[1].value = "ERROR: Field 'PIN' not in selected feature class."

return

def updateMessages(self:(
"""Modify the messages created by internal validation for each tool
parameter. This method is called after internal validation."""

self.params[0].clearMessage()
self.params[1].clearMessage()
self.params[2].clearMessage()
self.params[3].clearMessage()
self.params[4].clearMessage()
self.params[5].clearMessage()

if self.params[0].value is not None: # set error msg if field not in feature so user can correct problem
if self.params[1].value is not None:
if self.params[1].value.startswith("ERROR:":(
self.params[1].value = None # clear error message in parameter value, if desired
self.params[1].setErrorMessage(
"Field '{}' is not in Input FC '{}'".format('PIN', self.params[0].value))

return

def isLicensed(self:(
"""Set whether tool is licensed to execute."""
return True

#########################################################################################
# Below Toolvalidator code, create Toolvalidator class and call UpdateParameters + messages
validator = ToolValidator()
validator.updateParameters()
validator.updateMessages()

 

0 Kudos
RandyBurton
MVP Regular Contributor

Here's the script I used for debugging my Tool Validator code.  Since it is tested outside ArcMap, there are some modifications that are necessary.  For example, a full path to the feature containing your PINs is needed.

# HELP at http://desktop.arcgis.com/en/arcmap/latest/analyze/creating-tools/debugging-a-toolvalidator-class.htm

import arcpy

# Load the toolbox and get the tool's parameters, using the tool
#  name (not the tool label).
#
arcpy.ImportToolbox(r"C:\Path\to\toolbox.tbx") # toolbox location
params = arcpy.GetParameterInfo("SearchParcel") # name of script (not tool label)

# Set required parameters
#
params[0].value = r"C:\Path\to\geodatabase.gdb\PARCELS_BASE" # feature layer
# params[1].value = '0401' # first part of PIN
# params[2].value = '02' # middle part of PIN

# ToolValidator class block
# ----------------------------------------------------------------
class ToolValidator(object):

    def __init__(self):
        import arcpy
        self.params = arcpy.GetParameterInfo()

    def initializeParameters(self):
        # (initializeParameters code here)
        return

    def updateParameters(self):

        if self.params[0].value: # feature has been selected
            useFields = ['PIN'] # name of field used by tool (parcel identification number - a string)
            desc = arcpy.Describe(self.params[0].value)  # information about the input feature
            print(desc.dataType) # # Debug # #
            fieldNames = { f.name: f.type for f in desc.fields } # dictionary used to keep field names and types together
            print(fieldNames) # # Debug # #

            if set(useFields).issubset(fieldNames.keys()): # all fields were found by tool

                print("Field found in feature class") # # Debug # #
                # NOTE: If feature has selections, only the selections will be searched

                fc, fld = str(self.params[0].value), useFields

                self.params[1].filter.list = sorted(set([f[0][:4] for f in arcpy.da.SearchCursor(fc,fld)]))
                if self.params[1].value not in self.params[1].filter.list:
                    self.params[1].value = self.params[1].filter.list[0]
                    print(self.params[1].filter.list[0]) # # Debug # #
                    print(len(self.params[1].filter.list)) # # Debug # #
                    print(self.params[1].filter.list) # # Debug # #

                where_1 = "PIN LIKE '{}%'".format(self.params[1].value)
                print(where_1) # # Debug # #
                
                self.params[2].filter.list = sorted(set([f[0][5:7] for f in arcpy.da.SearchCursor(fc,fld,where_1)]))
                if self.params[2].value not in self.params[2].filter.list:
                    self.params[2].value = self.params[2].filter.list[0]
                    print(self.params[2].filter.list[0]) # # Debug # #
                    print(len(self.params[2].filter.list)) # # Debug # #
                    print(self.params[2].filter.list) # # Debug # #

                where_2 = "PIN LIKE '{} {}%'".format(self.params[1].value, self.params[2].value)
                print(where_2) # # Debug # #

                self.params[3].filter.list = sorted(set([f[0][8:] for f in arcpy.da.SearchCursor(fc,fld,where_2)]))
                if self.params[3].value not in self.params[3].filter.list:
                    self.params[3].value = self.params[3].filter.list[0]
                    print(self.params[3].filter.list[0]) # # Debug # #
                    print(len(self.params[3].filter.list)) # # Debug # #
                    print(self.params[3].filter.list) # # Debug # #

                # output completed whereClause
                self.params[4].value = "PIN = '{} {} {}'".format(self.params[1].value, self.params[2].value, self.params[3].value)
                print(self.params[4].value) # # Debug # #

            else: # at least one of the field names does not exist in feature; using the parameter to hold an error message

                if 'PIN' not in fieldNames.keys():
                    self.params[1].value = "ERROR: Field 'PIN' not in selected feature class."
                    print(self.params[1].value)  # # Debug # #

        return

    def updateMessages(self):
        
        self.params[0].clearMessage()
        self.params[1].clearMessage()
        self.params[2].clearMessage()
        self.params[3].clearMessage()
        if self.params[0].value is not None: # set error message if field not in feature so user can correct problem

            if self.params[1].value is not None:
                if self.params[1].value.startswith("ERROR:"):
                    self.params[1].value = None # clear error message in parameter value, if desired
                    self.params[2].value = None
                    self.params[3].value = None
                    self.params[0].setErrorMessage("Field '{}' is not in Input FC '{}'".format('PIN', self.params[0].value))
                    print('Setting message 0') # # Debug # #

        return
# ----------------------------------------------------------------
# Call routine(s) to debug
#
validator = ToolValidator()
validator.updateParameters()
validator.updateMessages()

print('\n')
for p in params:
    print(p.name, p.datatype, p.parameterType, p.direction, p.value)
print('\n')

# tool script (modified for debugging):
inFC = params[0].value # arcpy.GetParameterAsText(0) # input feature class (Input, Data Type 'Feature Layer')
PIN_a = params[1].value # arcpy.GetParameterAsText(1) # PIN[:4]  (Input, Data Type 'String')
PIN_b = params[2].value # arcpy.GetParameterAsText(2) # PIN[5:7] (Input, Data Type 'String')
PIN_c = params[3].value # arcpy.GetParameterAsText(3) # PIN[8:]  (Input, Data Type 'String')
whereClause = params[4].value # arcpy.GetParameterAsText(4) # set by ToolValidator updateParameters (Output, Derived, Data Type 'String')

arcpy.AddMessage("Where clause used: {}".format(whereClause))
print("Where clause used: {}".format(whereClause)) # # Debug # #

# convert inFC to feature layer since we are outside arcmap
arcpy.MakeFeatureLayer_management(inFC,'inFC_layer')
# use layer created
arcpy.SelectLayerByAttribute_management('inFC_layer', "NEW_SELECTION", where_clause=whereClause)

for i in range(arcpy.GetMessageCount()):  # display messages from SelectLayerByAttribute tool
    arcpy.AddMessage(arcpy.GetMessage(i))
# print messages created by SelectLayers
print(arcpy.GetMessages()) # # Debug # #

n = arcpy.GetCount_management(inFC)
arcpy.AddMessage("Number of records selected: {}".format(n))
print("Number of records selected: {}".format(n)) # # Debug # #

 

You will notice that I've added some print statements inside the Tool Validator.  Normally I wouldn't try to debug the tool's script while working on the validator's code, but it can be placed at the bottom and will need some modification.  Even then, you may get some unexpected results.

For testing the params (just before the validator class), I started with just the feature and added parts of the PIN to see what changes this would make.

Hope this helps.

0 Kudos
AllisonClark
New Contributor II

Hello Randy, 

I've been stuck on a weird problem with resetting the filters. The dropdowns that draw from the PARCELS_BASE feature layer work beautifully until I actually run the tool script. Thereafter, the dropdowns only show the parcel ID that was used by the script. The only way to reset them is to close the project and reopen it. 

The attachment shows an example. 

I checked the PARCELS_BASE table; my script does a CLEAR SELECTION on it, and nothing is still selected. 

I didn't find much online on the topic of resetting filter lists drawn from the database, and nothing at all about using them in ToolValidator. 

I tried these statements in different places to no effect:

self.params[1].filter.list = []

cursor.reset()     (where cursor = arcpy.da.SearchCursor())

What am I missing? 

0 Kudos
AllisonClark
New Contributor II

Update: It looks like I need to put SelectLayerByAttribute("PARCELS_BASE", "CLEAR_SELECTION") into the ToolValidator code specifically?? I tried debugging in the Pro python window and found that worked. 

 

RandyBurton
MVP Regular Contributor

Yes, the tool should clear the selection.  Typically, this would be when things complete.  The user would need to manually clear any selections before reusing the tool, or the next use of the tool would be limited to the selections.

If it is important to see the selected feature after running the tool, I suppose you could insert the "clear selection" instruction at the start of the tool validator code.  The idea would be to clear any selections before populating the drop-downs. 

I could also see that allowing the user to select a group of features to search may help performance if there are lots of parcels and only a specific neighborhood needs to be searched.  Then the user would need to know if the dropdown doesn't contain enough selections, then exit the tool, clear any selections and try again.

0 Kudos
RandyBurton
MVP Regular Contributor

If you want to clear selections in the Tool Validator section, you might try:

    def updateParameters(self):

        if self.params[0].value: # feature has been selected

            arcpy.SelectLayerByAttribute_management(self.params[0].value, "CLEAR_SELECTION")

            # code continues