text: 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:
  - Use the DynamicParam keyword.
  - Create a ParameterAttribute object.
  - Create an attribute collection.
  - Create a validation set.
  - Create a RuntimeDefinedParameter.
  - 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:
  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
  @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’
  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.
  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
  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 dotfilename.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
  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’
  @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.
  This is sweet but the issue I have is alluded to in the about_Functions_Advanced_Parameters help: "Dynamic parameters can be very useful, but use them only when necessary, because they can be difficult for users to discover." I really want to build a dynamic
  validate set for a parameter but when I turn the parameter to dynamic a ‘Get-Help Get-MyFunCmdlet’ does not show the dynamic parameter in the help, nor the parameter sets it can be used with so for my situation (sharing a custom module with colleagues who
  need the built-in help) this is a deal breaker.
  Hey Scripting Guy! You might already know that MS Word does not allow VBA to control the printer driver, i.e. can’t use VBA to force it to print 2 sided vs. 1-sided unless you set up a separate copy of your existing printer, or use Windows API, or write
  a dll, etc. (sheesh). Any ideas on how to do this quickly, easily and universally no matter what printer I have? (BTW, it’s a networked printer locked down by the powers that be, so I can’t just hack into it. LOL) Your help would be greatly appreciated….I’m
  not sure how to tell if you’ve posted a reply to my question, but if you’re on G+ my name there is Bryan "Ironman" Stark.