Creating a ValueTable with columns that have drop down choices.

9544
5
02-18-2015 11:20 AM

Creating a ValueTable with columns that have drop down choices.

Thought I would give this part of GeoNet a try, share my experience and hopefully have some python guru come in and shoot me down in flames and tell me how it should be done!

 

Scenario

 

I want a python script tool that has a value table with drop down choices. In this example I want to add multiple layers and select different fields as different datasets have different names for their ID fields. The interface I have created looks as below.

Untitled.png

When you add a new activity layer the ID field becomes a drop down for that row, listing fields from the dataset.

 

How I did it

 

To create this interface I created a Python Toolbox. This was not a script that I had created and wired up into a Script Tool. The code is below and I've tried to document it mainly for my benefit but to help others understand. After the code are some limitations discussed.

 

 

 

 

 

 

import arcpy

class Toolbox(object):
    def __init__(self):
        """Define the toolbox (the name of the toolbox is the name of the .pyt file)."""
        self.label = "My first python toolbox"
        self.alias = "Examples"

        # List of tool classes associated with this toolbox
        self.tools = [Tool1]

class Tool1(object):
    def __init__(self):
        """Define the tool (tool name is the name of the class)."""
        self.label = "Test tool"
        self.description = "Example showing how to create a value table tool with drop downs"
        self.canRunInBackground = False

    def getParameterInfo(self):
        """Define parameter definitions"""

        #
        # Define Parameter 0
        #

        # Param0 will be a FeatureLayer that accepts only polygon layers
        param0 = arcpy.Parameter(displayName = "Site Layer",name="Site_layer",datatype="GPFeatureLayer",parameterType="Required",direction="Input")

        # This filters for polygon layers only
        param0.filter.list = ["Polygon"]

        #
        # Define Parameter 1
        #

        # Param1 will be a field which is dependant on the layer in Param0
        param1 = arcpy.Parameter(displayName = "Site ID Field",name="SiteID_field",datatype="Field",parameterType="Required",direction="Input")

        # This is filtering for text fields only
        param1.filter.list=["Text"]

        # This says that param1 is dependant upon param0
        param1.parameterDependencies=[param0.name]

        #
        # Define Parameter 2
        #

        # Param2 is a valuetable with 2 columns, a layer and an ID field
        param2 = arcpy.Parameter(displayName = "Activity Layers",name="Activity_layers",datatype="GPValueTable",parameterType="Optional",direction="Input")

        # This creates 2 columns a featurelayer and a string
        param2.columns=[["Feature Layer","Activity Layer"],["String","ID field"]]

        # This sets the filter on the first column, only point data is allowed
        param2.filters[0].list = ["Point"]

        # This says the filter on the second column will be a value List
        param2.filters[1].type="ValueList"

        # This adds a dummy value to the list, this will get overwritten by the updateParameters function
        param2.filters[1].list=["x"]

        # Create a list of parameters and return
        params = [param0,param1,param2]
        return params

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

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

        # Update drop down for second column. Logic is that the last entry in 
        # the value table is the one you want to select a field ID for.

        if parameters[2].altered:

            # Return a list of lists
            lol = parameters[2].values

            # The number of rows in the table
            numrows = len(lol)
            i = 0

            for al in lol:
                i+=1
                if i == numrows:
                    # We are at the end of the table, now update filter to
                    # list only the fields for the FeatureLayer in the last row.
                    lay = al[0]
                    fi = al[1]
                    desc = arcpy.Describe(lay)
                    fields = desc.fields
                    l=[]
                    for f in fields:
                        if f.type in ["Integer","OID"]:
                            l.append(f.name)

                    parameters[2].filters[1].list = l
        return

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

    def execute(self, parameters, messages):
        """The source code of the tool."""
        try:
            # Some code to show that I had managed to get what I wanted intot the ValueTable
            fo = open(r"C:\test.txt","w")
            fo.write(str(parameters[2].values))
            fo.close()
        except arcpy.ExecuteError:
            messages.addErrorMessage(arcpy.GetMessages())

 

 

 

 

 

Limitations

 

The main limitation is that the logic of the script searches for the last row, reads the layer name and then sets the filter to be the fields of that dataset (just what you want). If you have added more than 2 layers and then go back to say the first row to choose another ID field you discover that the filter applies to the whole column and not the individual row so you will get fields offered up from the last layer in the valuetable.

 

I have been unable to work out how you can set drop downs specific to a row. There is no way that I have found that captures the onclick event of a cell in a valuetable. If you are an esri python guru (may be part of the development team?) reading this I would love to know if this can be done? Just look at the Create TIN tool to observe the technique I am looking for.

 

Final note; there appears to be no way to run a python toolbox in debug mode, yes there is an esri blog page but that is a real fudge and you can't use it to test how the interface is responding.

 

Hope you find this helpful?

Comments

I think your code would only be available to 10.3 and up. the parameter.filters property wasn't available in 10.2. Just as an FYI. Otherwise, it looks interesting!

I've been trying to figure out how to do almost exactly this. Thanks!  One thing I'm curious about is the updateParameters function.  Is there any reason to loop through each layer rather than just using the index of the last row directly?

something like this:

def updateParameters(self, parameters):  
        """Modify the values and properties of parameters before internal
        validation is performed.  This method is called whenever a parameter
        has been changed."""  
 
        # Update drop down for second column. Logic is that the last entry in   
        # the value table is the one you want to select a field ID for.  
 
        if parameters[2].altered:  
 
            # Return a list of lists  
            lol = parameters[2].values  
 
            # The number of rows in the table
            # Use numrows to refer directly to last row rather than looping through without doing anything 
            numrows = len(lol)   

            # now update filter to list only the fields for the FeatureLayer in the last row.  
            lay = lol[numrows][0]  
            fi = lol[numrows][1]    
            fields = arcpy.ListFields(lay)  
            l=[]  
            for f in fields:  
                if f.type in ["Integer","OID"]:  
                    l.append(f.name)  
 
            parameters[2].filters[1].list = l  
        return 

Hi David,

No there is no reason, just how my brain resolved it! Your approach is certainly more efficient. Thanks!

Duncan

def updateParameters(self, parameters):  
        """Modify the values and properties of parameters before internal
        validation is performed.  This method is called whenever a parameter
        has been changed."""  
 
        # Update drop down for second column. Logic is that the last entry in   
        # the value table is the one you want to select a field ID for.  
 
        if parameters[2].altered:  
 
            # Return a list of lists  
            lol = parameters[2].values  

            # now update filter to list only the fields for the FeatureLayer in the last row.  
            lay = lol[-1][0]  # capture layer portion of the parameter in last row
            fi = lol[-1][1] 
            fields = arcpy.ListFields(lay)  
     # using list comprehension
     parameters[2].filters[1].list = [field.name for field in fields if field.type in ["Integer","OID"]] 
        return

Python technicalities but looks cleaner

Hi Duncan,

Great document here, thanks for sharing.

For debugging purposes, you could use the arcpy.AddMessage function to get direct feedback in the arcpy console once you hit the OK button (?) , for example reading values from parameters

def execute(self, parameters, messages):  
        """Execute the source code of the tool."""  
        fields = parameters[2].values
        arcpy.AddMessage(fields 
        return
Version history
Last update:
‎09-11-2023 02:41 PM
Updated by:
Contributors