Use Dynamic Parameters to Populate List of Printer Names

Use Dynamic Parameters to Populate List of Printer Names

  • Comments 7
  • Likes

Summary: Microsoft Scripting Guy, Ed Wilson, shows how to use dynamic parameters to populate a list of printer names.

Microsoft Scripting Guy, Ed Wilson, is here. Using a Windows PowerShell function to send a test page to a printer is pretty cool (see Use PowerShell to Send Test Page to a Printer). But it would be even better if I did not have to type the name of the printer. I know I can use the Get-Printer function in Windows 8.1 to list all of my printers (and I can use a CIM session to do this remotely), so why can’t I run that script to create my list of available printers? It turns out that I can do this by using dynamic parameters. The problem is that it is a bit confusing to do.

Note I am not going to be able to explain all of this to everyone’s fullest satisfaction. It is a bit complicated. But hopefully, I will explain it well enough that you can copy my script and modify it to do other things. So in effect, what I hope to create is a dynamic parameter template that you can use to create powerful and useful scripts. This is cool stuff, and for me, it is worth the effort. Also note that you do not have to do this. You can easily use available tools that will populate a WinForm with a drop-down list of available printers. Part of the point of today’s post is to show you how you can do this. If you choose to do it is entirely up to you. Hope you enjoy the post as much as I had fun writing it.

When I use the Param statement to add parameters to a function, I am accepting input that will change the way the function works. Typically, I use parameters to permit me to change the target of a function, and I call the ComputerName parameter as shown here:

Function example

{

 Param([string]$computername)

}

I can also set default values for a parameter, which gives me a default behavior for my function. Typically, I like to target a function on my local computer. I like to obtain this value from the environmental variables as shown here:

Function example

{

 Param([string]$computername = $env:computername)

}

But what if I wanted to produce a list of available computers that might be on my target network? This would require running a script block, and unfortunately I cannot do this. I need a dynamic parameter.

Here is what I need to do to create a dynamic parameter:

  1. Use the DynamicParam keyword.
  2. Create a ParameterAttribute object.
  3. Create an attribute collection.
  4. Create a validation set.
  5. Create a RuntimeDefinedParameter.
  6. Create a RuntimeDefinedParameterDictionary object.

Create ParameterAttribute object

I need to create a ParameterAttribute object. To do this, I use the New-Object cmdlet, and then I create a ParameterAttribute object. This class resides in the System.Management.Automation namespace. (This is the namespace that contains all of the Windows PowerShell stuff). I store the newly created object in a variable named $attributes.

Now I want to specify a name for the parameter set. This is a straight-forward value assignment to the ParameterSetName property. I decide to call it __ParameterSets. I also decide to make the attribute mandatory. Here are the three lines of script that accomplish this task:

    $attributes = new-object System.Management.Automation.ParameterAttribute

    $attributes.ParameterSetName = "__AllParameterSets"

    $attributes.Mandatory = $true

Create attribute collection

Now I need to create an attribute collection so I have a place to put the attributes that I just created. I use the New-Object cmdlet again, and this time, I store the returned attribute collection object in the $attributeCollection variable. The collection object comes from the System.Collections.ObjectModel namespace, and I specify that it will be of the [system.Attribute] type. When I have the object, I add my attributes to it by calling the Add method. This script is shown here:

$attributeCollection =

      new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]

    $attributeCollection.Add($attributes)

Create validation set

I need to get my collection of printer names. I use the Get-CimInstance cmdlet to query the Win32_Printer WMI class, and I return only the Name property. I store these printer names in the $_Values variable.

Now I create another object. This time it is the ValidateSetAttribute object. Again, this comes from the System.Management.Automation .NET Framework namespace. When I am creating the ValidateSetAttribute class, I specify the printer names stored in the $_Values variable. I store the returned ValidateSetAttribute object in the $validateSet variable. I pass this object to the Add method of the AttributeCollection object that I created in the previous step. Here is the script that does this:

$_Values = (Get-CimInstance win32_Printer).name       

    $ValidateSet =

      new-object System.Management.Automation.ValidateSetAttribute($_Values)

    $attributeCollection.Add($ValidateSet)

Create RuntimeDefinedParameter

Whew…

I still have not created everything I need to permit me to create my dynamic parameter. I need to create two more objects, and finally I will be done. I now use the New-Object cmdlet to create a RuntimeDefinedParameter. When I do this, I need to tell it the name of the parameter. In this example, it is Printer.

I also need to tell it where to find the potential values. The values come from the $attributeCollection variable. I store the RunTimeDefinedParameter in the $dynParam1 variable. Here is the script that accomplishes this task:

$dynParam1 =

      new-object -Type System.Management.Automation.RuntimeDefinedParameter(

      "Printer", [string], $attributeCollection)

Create RuntimeDefinedParameterDictionary object

The last object I need to create is the RuntimeDefinedParameterDictionary. Once again, this dictionary object comes from the System.Management.Automation .NET Framework namespace. I add the name of Printer, and the RuntimeDefinedParameter object that I stored in the $dynParam1 variable.

Now it is time to return the RuntimeDefinedParameterDictionary object that I stored in the $paramDictionary object, so I call the Return keyword and pass the $paramDictionary variable as shown here:

    $paramDictionary =

      new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary

    $paramDictionary.Add("Printer", $dynParam1)

    return $paramDictionary }

Dude, it is a regular advanced function

At this point, it is just a regular advanced function. I add a Begin and a Process section to the function. I am not doing anything special in the begin section, and the process section is where I place my script block. Following the script block, I have my End section. This is shown here:

begin {}

  process {

 $printer = $PSBoundParameters.printer

 Invoke-CimMethod -MethodName printtestpage -InputObject ( Get-CimInstance win32_printer -Filter "name LIKE '$printer'") }

   end {}

}

When I run the function, I am presented with a nice drop-down list that permits me to select a printer. This is the dynamic portion of the parameters is shown in the following image:

Image of command output

To me, it is worth a little bit of extra script to enable this functionality.  Here is the complete script:

Function out-TestPage {

 [CmdletBinding()]

    Param()

    DynamicParam {

    $attributes = new-object System.Management.Automation.ParameterAttribute

    $attributes.ParameterSetName = "__AllParameterSets"

    $attributes.Mandatory = $true

    $attributeCollection =

      new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]

    $attributeCollection.Add($attributes)

    $_Values = (Get-CimInstance win32_Printer).name       

    $ValidateSet =

      new-object System.Management.Automation.ValidateSetAttribute($_Values)

    $attributeCollection.Add($ValidateSet)

    $dynParam1 =

      new-object -Type System.Management.Automation.RuntimeDefinedParameter(

      "Printer", [string], $attributeCollection)

    $paramDictionary =

      new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary

    $paramDictionary.Add("Printer", $dynParam1)

    return $paramDictionary }

  begin {}

  process {

 $printer = $PSBoundParameters.printer

 Invoke-CimMethod -MethodName printtestpage -InputObject ( Get-CimInstance win32_printer -Filter "name LIKE '$printer'") }

   end {}

}

Join me tomorrow when we will have more cool Windows PowerShell stuff.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • Ed,
    Looks like there is an error in the code listing you posted. Just copying your code and running as shown in ISE gives:
    PS I:\> Out-TestPage -Printer
    Out-TestPage : Missing an argument for parameter 'Printer'. Specify a parameter of type 'System.String' and try again.
    At line:1 char:14
    + Out-TestPage -Printer
    + ~~~~~~~~
    + CategoryInfo : InvalidArgument: (:) [Out-TestPage], ParameterBindingException
    + FullyQualifiedErrorId : MissingArgument,Out-TestPage

  • Hi Ed,

    Great article,

    This one goes straight into my "how to use parameters examples.PS1"
    Even though I've never came across a situation where I needed something like this,
    it's good to know you can do a dynamic parameter validation.




  • Reply to Pete...

    Actually, it took me a few minutes to figure it out as well when running it within a PoSH shell and not within the ISE editor. [When running within the ISE shell, the lists of parameters and printer names appear in the Intellisense pop-ups like Ed showed in the picture in the article.] So, assuming you've launched a PoSH shell and that you've dot sourced the function to register it for use --> that's dot space dot\filename.ps1 or . . \fncOutTestPage.ps1 ). Try this at the command prompt:

    1) Start by entering the function name:

    PS C:\> Out-TestPage

    2) Continue by typing a space and then entering the dash '-' character followed by a character. When you do, the "-Printer" parameter option should appear.

    PS C:\> Out-TestPage - ... should then change to PS C:\> Out-TestPage -Printer

    3) Next, type a space and then type another character. At that point, the items in the dynamic list should start to appear... one for each tab that you enter. Here's what I see when I do it (oh, and you can continue to enter tabs all you want, the list just wraps around.

    PS C:\> Out-TestPage -Printer Fax
    ....another tab and ...
    PS C:\> Out-TestPage -Printer Snagit 11
    ....another tab and ...
    PS C:\> Out-TestPage -Printer HP Photosmart
    ....another tab and ...
    PS C:\> Out-TestPage -Printer Microsoft XPS Document Writer
    ....another tab and back again to...
    PS C:\> Out-TestPage -Printer Fax


    The problem I have is that some of the printers have spaces in their names, so when I press the enter key on it, I get an error (the printer named 'Fax' works just fine, the others, not so much). I think the problem is in the Process section of the code and with all the single and double quotes in it already, I'm not yet sure how to get it to handle printer's with spaces in the names.

  • Web site character conversion issue.. when I entered < tab=""> without any spaces it converted to a non-visible character... here's the update:
    ==========================================

    1) Start by entering the function name:

    PS C:\> Out-TestPage

    2) Continue by typing a space and then entering the dash '-' character followed by a 'tab' character. When you do, the "-Printer" parameter should appear.

    PS C:\> Out-TestPage - ... should then change to ===> PS C:\> Out-TestPage -Printer

    3) Next, type a space and then type another 'tab' character. At that point, the items in the dynamic list should start to appear... one for each tab that you enter. Here's what I see when I do it (oh, and you can continue to enter tabs all you want, the list just wraps around).

    PS C:\> Out-TestPage -Printer Fax
    ....another tab and ...
    PS C:\> Out-TestPage -Printer Snagit 11
    ....another tab and ...
    PS C:\> Out-TestPage -Printer HP Photosmart
    ....another tab and ...
    PS C:\> Out-TestPage -Printer Microsoft XPS Document Writer
    ....another tab and back again to...
    PS C:\> Out-TestPage -Printer Fax

  • @Jerry C

    To send output to the printers with a space in the name, just enclose the name within a single Quote like this.

    Out-TestPage -Printer 'HP Photosmart'

  • @Sean Kearny

    That is true and that does work. So, as is, it is certainly doable and still cool. However, I thought the point was to be able to simply use the 'tab' key to cycle through the choices and then hit enter/return. The latter does not work without selecting the item of choice and then manually entering the quotes.

    If you try and use the function as is, you get an error on printer names with spaces in them as follows:

    Out-TestPage : Cannot validate argument on parameter 'Printer'. The argument
    "Snagit" does not belong to the set "WebEx Document Loader,Snagit 11,Send To
    OneNote 2013,Microsoft XPS Document Writer,HP Photosmart,Fax" specified by
    the ValidateSet attribute. Supply an argument that is in the set and then
    try the command again.

    Notice that it thinks the 'set' is the entire list of printers enclosed in a single set of double-quotes where each name is separated by commas.

    So I figured I should try and have it store the printer names with quotes (single or double) built into them and I actually got that to work (see the 'for' loop line below):

    $_Values = (Get-CimInstance win32_Printer).name
    for ($i=0; $i -lt $_Values.Count; $i++){$_Values[$i] = "'" + $_Values[$i] + "'"}
    $ValidateSet = new-object System.Management.Automation.ValidateSetAttribute($_Values)

    When you use that, then at each tab, the 'choice' is already surrounded by the single or double quotes (as coded above, it uses single quotes).

    But.. then the function to look up the value didn't work (on any of them... internal spaces or not). The error is...

    Out-TestPage : Cannot validate argument on parameter 'Printer'. The argument
    "Snagit 11" does not belong to the set "'WebEx Document Loader','Snagit 11',
    'Send To OneNote 2013','Microsoft XPS Document Writer','HP Photosmart','Fax'"
    specified by the ValidateSet attribute. Supply an argument that is in the set
    and then try the command again.

    The difference in the errors is subtle in the way the values are stored. Now the list of printers itself is the same, but each item has quotes around it. But the lookup process doesn't like that. I've tried several other alternatives, but nothing that can be made to work.