I'm experimenting with creating GUIs and using classes in powershell. I'm really new to both of those things (and to a lesser extent powershell generally) so bear with me.
The problem I am having is I cannot make any control which makes any modification to the form. This is because when adding a handler to a button it goes into the scope of the button class in the handler and none of the form references are accessible.
Most examples of UI code in powershell are not class heavy. I realize that I could get around this, if it was not in a class, by having the handlers and form being in the global scope, but I'm trying to make use of classes so that I have the ability to make base forms and inherit from them. And I want to see what is possible.
Below is some test code including multiple of my attempts to make this work with the results commented. I even got the idea of passing the form reference into the handler (DI style). Things I'm trying are all over the map since I'm also feeling out basic powershell syntax.
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
class Window : System.Windows.Forms.Form
{
Handler () {
Write-Host Handler
$this.BackColor = [System.Drawing.Color]::Blue
}
HandlerArgs ([object]$sender, [System.Eventargs]$eventArgs) {
Write-Host HandlerArgs
$this.BackColor = [System.Drawing.Color]::Blue
}
$HandlerVar = {
Write-Host HandlerVar
$this.BackColor = [System.Drawing.Color]::Blue
}
HandlerParam ($form) {
Write-Host HandlerParam
$form.BackColor = [System.Drawing.Color]::Blue
}
$HandlerVarParam = {
(params $form)
Write-Host HandlerVarParam
$form.BackColor = [System.Drawing.Color]::Blue
}
Window ()
{
$button = New-Object System.Windows.Forms.Button
$button.Text = "ClickMe"
$button.AutoSize = $true
$this.Controls.Add($button)
# $button.Add_Click( $this.Handler )
# "Cannot convert argument "value", with value: "void Handler()", for "add_Click"
# to type "System.EventHandler": "Cannot convert the "void SelectNextPage()"
# value of type "System.Management.Automation.PSMethod" to type "System.EventHandler"."
# $button.Add_Click(([System.EventHandler]$x = $this.Handler ))
# turns the window blue immediatly
# $button.Add_Click( $this.HandlerArgs )
# "Cannot convert the "void HandlerArgs(System.Object sender, System.EventArgs eventArgs)"
# value of type "System.Management.Automation.PSMethod" to type "System.EventHandler".""
# $button.Add_Click( $this.HandlerVar )
# this works but turns the button blue instead of the form
# $button.Add_Click( { $this.Handler } )
# does nothing?
# $button.Add_Click( { $this.Handler() } )
# Method invocation failed because [System.Windows.Forms.Button] does not contain a
# method named 'Handler'.
# $button.Add_Click( $this.HandlerParam($this) )
# turns the window blue immediatly
# $button.Add_Click( { $this.HandlerParam($this) } )
# Method invocation failed because [System.Windows.Forms.Button] does not contain a
# method named 'HandlerParam'.
# $button.Add_Click( $this.HandlerVarParam $this )
# parse error
# I can't find a syntax that lets me pass a param to a function in a variable
}
}
$foo = New-Object Window
$foo.ShowDialog()
Although it's likely super obvious already, c# is my main language.
Perhaps this is just a limitation of the OO support in an interpreted scripting language, or maybe it's just my syntax deficiency. Is there any pattern that will get me what I want in this class-based structure? I would hope the pattern would be a general solution for doing normal form-things with handlers of form-controls.
Although mklement0 is absolutely spot on - class method can't be used directly as event delegates - there is a way to bind the instance to it without storing the handler in a property.
The following approach fails because $this resolves to the event owner at runtime:
$button.add_Click( { $this.Handler() } )
# Method invocation failed because [System.Windows.Forms.Button] does not contain a
# method named 'Handler'.
You can bypass this late-binding behavior by using any other (non-automatic) local variable to reference $this and then closing over it before calling add_Click():
$thisForm = $this
$button.add_Click( { $thisForm.Handler() }.GetNewClosure() )
# or
$button.add_Click( { $thisForm.HandlerArgs($this,$EventArgs) }.GetNewClosure() )
Now, $thisForm will resolve to whatever $this referenced when the handler was added, and the button will work as expected.
PowerShell, as of PowerShell 7.1, only knows how to pass script blocks as event delegates, not custom-class methods:
Mathias R. Jessen's helpful answer shows how to work around this limitation:
By wrapping a call to the event-handler method in a script block...
... and additionally providing a closure that captures a variable that refers to the class instance usually accessible as $this under a different name, to work around $this inside the script block referring to the event-originating object instead (same as the first handler argument, $sender), so as to ensure that access to the class instance remains possible.
As an aside: in this particular case, a solution without a .GetNewClosure() call would have been possible too, by calling $this.FindForm().
The following defines an idiom for generalizing this approach by encapsulating via a single helper method, which may be of interest if you have multiple event handlers in your class.:
As in your original approach and in Mathias' solution, individual event handlers are defined as you would normally define them in C#, as instance methods.
An auxiliary GetHandler() method encapsulates the logic of wrapping a call to a given event-handler method in a script block so that it is accepted by .add_{Event}() calls.
Any .add_{EventName}() call must then be passed the event-handler method via this auxiliary $this.GetHandler() method, as shown below.
Add-Type -AssemblyName System.Windows.Forms
class Window : System.Windows.Forms.Form {
# Define the event handler as an instance method, as you would in C#.
hidden ClickHandler([object] $sender, [EventArgs] $eventArgs) {
# Diagnostically print info about the sender and the event arguments.
$sender.GetType().FullName | Write-Verbose -vb
$eventArgs | Format-List | Out-String | Write-Verbose -vb
$this.BackColor = [System.Drawing.Color]::Blue
}
# Define a generic helper method that takes an event-handling instance method
# and turns it into a script block, so that it is accepted by
# .add_{Event}() calls.
hidden [scriptblock] GetHandler([Management.Automation.PSMethod] $method) {
# Wrap the event-handling method in a script block, because only a
# script block can be passed to .add_{Event}() calls.
# The .GetNewClosure() call is necessary to ensure that the $method variable
# is available when the script block is called by the event.
# Calling via a *method* also ensures that the method body still sees $this
# as the enclosing class instance, whereas the script block itself sees
# $this as the event-originating object (same as $sender).
return {
param([object] $sender, [EventArgs] $eventArgs)
$method.Invoke($sender, $eventArgs)
}.GetNewClosure()
}
# Constructor.
Window() {
$button = New-Object System.Windows.Forms.Button
$button.Text = "ClickMe"
$button.AutoSize = $true
$this.Controls.Add($button)
# Pass the event-handler method via the GetHandler() helper method.
$button.add_Click( $this.GetHandler($this.ClickHandler) )
}
}
$foo = New-Object Window
$foo.ShowDialog()
Note:
Add-Type -AssemblyName System.Windows.Forms must actually be executed before the script loads in order for you custom class definition deriving from System.Windows.Forms.Form to work, which is a known problem:
Only if the assembly containing the type that a custom PS class derives from has already been loaded at script parse time does the class definition succeed - see GitHub issue #3641.
The same applies to the using assembly statement - see about_Using, (which in PowerShell [Core] as of 7.0 has the added problem of not recognizing well-known assemblies such as System.Windows.Forms - see GitHub issue #11856)
In general, support for custom classes in PowerShell is, unfortunately, very much a work in progress, and many existing issues are tracked in GitHub meta issue #6652.
Related
I have a form in which as soon as ready several elements will be added (for example, a list). It may take some time to add them (from fractions of a second to several minutes). Therefore, I want to add processing to a separate thread (child). The number of elements is not known in advance (for example, how many files are in the folder), so they are created in the child stream. When the processing in the child stream ends, I want to display these elements on the main form (before that the form did not have these elements and performed other tasks).
However, I am faced with the fact that I cannot add these elements to the main form from the child stream. I will give a simple example as an example. It certainly works:
$Main = New-Object System.Windows.Forms.Form
$Run = {
# The form is busy while adding elements (buttons here)
$Top = 0
1..5 | % {
$Button = New-Object System.Windows.Forms.Button
$Button.Top = $Top
$Main.Controls.Add($Button)
$Top += 30
Sleep 1
}
}
$Main.Add_Shown($Run)
# Adding and performing other tasks on the form here
[void]$Main.ShowDialog()
But, adding the same thing to the child stream I did not get the button to display on the main form. I do not understand why.
$Main = New-Object System.Windows.Forms.Form
$Run = {
$RS = [Runspacefactory]::CreateRunspace()
$RS.Open()
$RS.SessionStateProxy.SetVariable('Main', $Main)
$PS = [PowerShell]::Create().AddScript({
# Many items will be added here. Their number and processing time are unknown in advance
# Now an example with the addition of five buttons.
$Top = 0
1..5 | % {
$Button = New-Object System.Windows.Forms.Button
$Button.Top = $Top
$Main.Controls.Add($Button)
$Top += 30
Sleep 1
}
})
$PS.Runspace = $RS; $Null = $PS.BeginInvoke()
}
$Main.Add_Shown($Run)
[void]$Main.ShowDialog()
How can I add elements to the main form that are created in the child stream? thanks
While you can create controls on thread B, you cannot add them to a control that was created in thread A from thread B.
If you attempt that, you'll get the following exception:
Controls created on one thread cannot be parented to a control on a different thread.
Parenting to means calling the .Add() or .AddRange() method on a control (form) to add other controls as child controls.
In other words: In order to add controls to your $Main form, which is created and later displayed in the original thread (PowerShell runspace), the $Main.Controls.Add() call must occur in that same thread.
Similarly, you should always attach event delegates (event-handler script blocks) in that same thread too.
While your own answer attempts to ensure adding the buttons to the form in the original runspace, it doesn't work as written - see the bottom section.
I suggest a simpler approach:
Use a thread job to create the controls in the background, via Start-ThreadJob.
Start-ThreadJob is part of the the ThreadJob module that offers a lightweight, thread-based alternative to the child-process-based regular background jobs and is also a more convenient alternative to creating runspaces via the PowerShell SDK.
It comes with PowerShell [Core] v6+ and in Windows PowerShell can be installed on demand with, e.g., Install-Module ThreadJob -Scope CurrentUser.
In most cases, thread jobs are the better choice, both for performance and type fidelity - see the bottom section of this answer for why.
Show your form non-modally (.Show() rather than .ShowDialog()) and process GUI events in a [System.Windows.Forms.Application]::DoEvents() loop.
Note: [System.Windows.Forms.Application]::DoEvents() can be problematic in general (it is essentially what the blocking .ShowDialog() call does behind the scenes), but in this constrained scenario (assuming only one form is to be shown) it should be fine. See this answer for background information.
In the loop, check for newly created buttons as output by the thread job, attach an event handler, and add them to your form.
Here is a working example that adds 3 buttons to the form after making it visible, one after the other while sleeping in between:
Add-Type -ea Stop -Assembly System.Windows.Forms
$Main = New-Object System.Windows.Forms.Form
# Start a thread job that will create the buttons.
$job = Start-ThreadJob {
$top = 0
1..3 | % {
# Create and output a button object.
($btn = [System.Windows.Forms.Button] #{
Name = "Button$_"
Text = "Button$_"
Top = $top
})
Start-Sleep 1
$top += $btn.Height
}
}
# Show the form asynchronously
$Main.Show()
# Process GUI events in a loop, and add
# buttons to the form as they're being created
# by the thread job.
while ($Main.Visible) {
[System.Windows.Forms.Application]::DoEvents()
if ($button = Receive-Job -Job $job) {
# Add an event handler...
$button.add_Click({ Write-Host "Button clicked: $($this.Name)" })
# .. and it to the form.
$Main.Controls.AddRange($button)
}
}
# Clean up.
$Main.Dispose()
Remove-Job -Job $job -Force
'Done'
As of this writing, your own answer tries to achieve adding the controls to the form in the original runspace by using Register-ObjectEvent to subscribe to the other thread's (runspace's) events, given that the -Action script block used for event handling runs (in a dynamic module inside) the original thread (runspace), but there are two problems with that:
Unlike your answer suggests, the -Action script block neither directly sees the $Main variable from the original runspace, nor the other runspace's variables - these problems can be overcome, however, by passing $Main to Register-ObjectEvent via -MessageData and accessing it via $Event.MessageData in the script block, and by accessing the other runspace's variables via $Sender.Runspace.SessionStateProxy.GetVariable() calls.
More importantly, however, the .ShowDialog() call will block further processing; that is, your events won't fire and therefore your -Action script block won't be invoked until after the form closes.
Update: You mention a workaround in order to get PowerShell's events to fire while the form is being displayed:
Subscribe to the MouseMove event with a dummy event handler whose invocation gives PowerShell a chance to fire its own events while the form is being displayed modally; e.g.: $Main.Add_MouseMove({ Out-Host }); note that this workaround is only effective if the script block
calls a command, such as Out-Host in this example (which is effectively a no-op); a mere expression or .NET method call is not enough.
However, this workaround is suboptimal in that it relies on the user (continually) mousing over the form for the PowerShell events to fire; also, it is somewhat obscure and inefficient.
I think you can't create form and controls on different threads. But you can access control properties though. So you can create form with control placeholders in a runspace, then change them on the main thread once your calculations are complete. Example:
$form = New-Object System.Windows.Forms.Form
$rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$rs.ApartmentState = [System.Threading.ApartmentState]::MTA
$ps = [powershell]::create()
$ps.Runspace = $rs
$rs.Open()
$out = $ps.AddScript({param($form)
$button1 = New-Object System.Windows.Forms.Button
$button1.Name = "button1"
$form.Controls.Add($button1)
$form.ShowDialog()
}).AddArgument($form).BeginInvoke()
#-----------------------------
sleep 1;
$form.Controls["button1"].Text = "some button"
This is the way I'm using now. Thanks to #mklement0 for the talk about the Register-ObjectEvent method (here). I applied it here. The essence of the method is that the elements are created in the child stream (in this case, the Button), and when the child space has finished work, Register-ObjectEvent is processed. Register-ObjectEvent is located in the main space and therefore allows you to add an element (Button here) to the form.
$Main = New-Object System.Windows.Forms.Form
$Run = {
$RS = [Runspacefactory]::CreateRunspace()
$RS.Open()
$RS.SessionStateProxy.SetVariable('Main', $Main)
$PS = [PowerShell]::Create().AddScript({
$Button = New-Object System.Windows.Forms.Button
}
})
$PS.Runspace = $RS
$Null = Register-ObjectEvent -InputObject $PS -EventName InvocationStateChanged -Action {
if ($EventArgs.InvocationStateInfo.State -in 'Completed', 'Failed') {
$Main.Controls.Add($Button)
}
}
$Null = $PS.BeginInvoke()
}
$Main.Add_Shown($Run)
[void]$Main.ShowDialog()
This is my a workaround. However, I still do not know if it is possible in principle to add elements of a child space to a form from a child space. This, of course, is about adding, not managing, because managing from the child space is successful.
Putting this here, since it is too long for the regular comment section.
Now, I don't spend much time using runspaces in production, as I've no had a real need (at least to date) for them, in class, sure, but I digress.
However, from all my previous readings and notes I've kept, this sounds like a use case for RunSpace Pools. Here are three of my saved resources. Working under the assumption that you may have not seen all of them of course. Now, I would post their code as well, but all are very long, so, there's that. Based on your use case, it could be seen as a duplicate to the last link resource.
PowerShell and WPF: Writing Data to a UI From a Different
Runspace
PowerShell Tip: Utilizing Runspaces for Responsive WPF GUI
Applications
Sharing Variables and Live Objects Between PowerShell Runspaces
How to access a different powershell runspace without WPF-object
I am working on refactoring a tool to OOP in PS5.
I have a number of classes. Sets can contain other Sets as well as Packages. Packages can contain other Packages and Tasks. And Tasks can contain other Tasks. For example...
Set1
Package1.1
Task1.1
Set2
Package2.1
Task2.1
Set2A
Package2A
Task2A.1
Task2A.2
Package2.2
Task2.2
Set3
Package3.1
Task3.1
Task3.1A
I plan to have Set, Package and Task classes, but there are a number of different Tasks with some common features and some unique, so I will have a base Task class that is then extended by the various final task classes.
My question relates to the data structure to contain the nested objects. If each class could only contain the next deeper type everything would be easy; the variable to hold the Packages in a Set could be an array of Packages, i.e. [Package[]]$Contents.
I could make it super flexible and just do an array; [Array]$Contents, but that allows for invalid items like strings and such.
Alternatively I could have some sort of Root class, with Sets, Packages and Tasks all extended that, and final Tasks then extending Tasks, and use[Root[]]$Contents or some such. But that might not be possible and it would still allow for adding a Task to a Set, since a final Task class would ultimately be extending from Root.
So, the question becomes, can you define an array that accepts multiple possible types but is still limited, something like [Set/Package[]]$Contents? Or is there perhaps a totally different way to define a variable that limits the valid members? An Enum seems to have potential, but it seems like they are limited to strings as I tried
enum AllowedTypes {
[Array]
[Strings]
}
and that in no good.
Or am I best of just using an Array and validating what I am adding in the Add method of each Class? I can see a possible solution there where I have overloaded Add methods in the Set class, one that takes a Set, one that takes a Package, and one that takes a generic object and throws an error to log. Assuming that the more specific overload method takes priority rather than everything going to the generic method since it's technically valid. Or perhaps that generic method won't even work since the collection of overloaded Add methods technically can't collapse to one valid choice because a Set is both a [Set] and a [PSObject] I guess.
PetSerAl, as countless times before, has provided an excellent (uncommented) solution in a comment on the question, without coming back to post that solution as an answer.
Given the limits of code formatting in comments, it's worth presenting the solution in a more readable format; additionally, it has been streamlined, modularized, extended, and commented:
In short: a PowerShell custom class (PSv5+) is used to subclass standard type [System.Collections.ObjectModel.Collection[object]] in order to limit adding elements to a list of permitted types passed to the constructor.
class MyCollection : System.Collections.ObjectModel.Collection[object] {
# The types an instance of this collection
# is permitted to store instance of, initialized via the constructor.
[Type[]] $permittedTypes
# The only constructor, to which the permitted types must be passed.
MyCollection([Type[]] $permittedTypes) { $this.permittedTypes = $permittedTypes }
# Helper method to determine if a given object is of a permitted type.
[bool] IsOfPermittedType([object] $item) {
return $this.permittedTypes.Where({ $item -is $_ }, 'First')
}
# Hidden helper method for ensuring that an item about to be inserted / added
# / set is of a permissible type; throws an exception, if not.
hidden AssertIsOfPermittedType([object] $item) {
if (-not $this.IsOfPermittedType($item)) {
Throw "Type not permitted: $($item.GetType().FullName)"
}
}
# Override the base class' .InsertItem() method to add type checking.
# Since the original method is protected, we mark it as hidden.
# Note that the .Add() and .Insert() methods don't need overriding, because they
# are implemented via this method.
hidden InsertItem([int] $index, [object] $item) {
$this.AssertIsOfPermittedType($item)
([System.Collections.ObjectModel.Collection[object]] $this).InsertItem($index, $item)
}
# Override the base class' SetItem() method to add type checking.
# Since the original method is protected, we mark it as hidden.
# This method is implicitly called when indexing ([...]) is used.
hidden SetItem([int] $index, [object] $item) {
$this.AssertIsOfPermittedType($item)
([System.Collections.ObjectModel.Collection[object]] $this).SetItem($index, $item)
}
# Note: Since the *removal* methods (.Remove(), .RemoveAt())
# need to type checking, there is no need to override them.
}
With the above class defined, here's sample code that exercises it:
# Create an instance of the custom collection type, passing integers and strings
# as the only permitted types.
# Note the (...) around the type arguments, because they must be passed
# as a *single argument* that is an *array*.
# Without the inner (...) PowerShell would try to pass them as *individual arguments*.
$myColl = [MyCollection]::new(([int], [string]))
# OK, add an [int]
# .Add() implicitly calls the overridden .InsertItem() method.
$myColl.Add(1)
$myColl.Add('hi') # OK, add a [string]
# OK, override the 1st element with a different [int]
# (though a [string] would work too).
# This implicitly calls the overridden .SetItem() method.
$myColl[0] = 2
# OK - insert a [string] item at index 0
$myColl.Insert(0, 'first')
# $myColl now contains: 'first', 2, 'hi'
# Try to add an impermissible type:
$myColl.Add([long] 42)
# -> Statement-terminating error:
# 'Exception calling "Add" with "1" argument(s): "Type not permitted: System.Int64"'
Is it possible to add data to a listview after a certain variable contains data ?
I have a function, which gets info from ConfigMgr when I press on a button.
After I press on that button, some info will be stored in a variable called $Results.
I want the winform listview to wait until the variable $Results contains data, and then load my function, that will add data to my listview - is that possible ?
It's working fine if I don't clear the variable $Results, and run the winform a second time, because then the variable $Results is not empty,
by having$Form.Add_Shown( { $Form.Activate(); Results }) in my script
Is there an equivalent method to achieve what I want ?
This is my function:
Function Get-SKUNotExists {
#Get objects stored in $Results
$listview_NotExists_SKU_Results = $Results | Select-Object Name,ID
# Compile a list of the properties
$listview_NotExists_SKU_Properties = $listview_NotExists_SKU_Results[0].psObject.Properties
# Create a column in the listView for each property
$listview_NotExists_SKU_Properties | ForEach-Object {
$listview_NotExists_SKU.Columns.Add("$($_.Name)")
}
# Looping through each object in the array, and add a row for each
ForEach ($listview_NotExists_SKU_Result in $listview_NotExists_SKU_Results) {
# Create a listViewItem, and assign it it's first value
$listview_NotExists_SKU_Item = New-Object System.Windows.Forms.ListViewItem($listview_NotExists_SKU_Result.Name)
# For each properties, except for 'Id' that we've already used to create the ListViewItem,
# find the column name, and extract the data for that property on the current object/Tasksequence
$listview_NotExists_SKU_Result.psObject.Properties | Where-Object { $_.Name -ne "ID" } | ForEach-Object {
$listview_NotExists_SKU_Item.SubItems.Add("$($listview_NotExists_SKU_Result.$($_.Name))")
}
# Add the created listViewItem to the ListView control
# (not adding 'Out-Null' at the end of the line will result in numbers outputred to the console)
$listview_NotExists_SKU.items.Add($listview_NotExists_SKU_Item)
}
# Resize all columns of the listView to fit their contents
$listview_NotExists_SKU.AutoResizeColumns("HeaderSize")
}
This is the button that generate data to $results:
# Adding another button control to Form
$button_whatif = New-Object System.Windows.Forms.Button
$button_whatif.Location = New-Object System.Drawing.Size(352, 954)
$button_whatif.Size = New-Object System.Drawing.Size(320, 32)
$button_whatif.TextAlign = "MiddleCenter"
$button_whatif.Text = “WhatIf”
$button_whatif.Add_Click( { $script:Results = Set-DynamicVariables -Manufacturer "$($listview_Vendor.SelectedItems)" -TSPackageID "$($ListView_Tasksequences.SelectedItems.SubItems[1].Text)" -WhatIf })
$Form.Controls.Add($button_whatif)
Simply call your list-populating function (Get-SKUNotExists) directly after assigning to $Results:
$button_whatif.Add_Click({
$Results = Set-DynamicVariables -Manufacturer "$($listview_Vendor.SelectedItems)" -TSPackageID "$($ListView_Tasksequences.SelectedItems.SubItems[1].Text)" -WhatIf
Get-SKUNotExists
})
Note that I've removed the $scipt: scope specifier from $Results, since it is no longer needed, given that you're calling Get-SKUNotExists from the same scope, which means that the child scope that Get-SKUNotExists runs in implicitly sees it.
That said:
In general, if a value is directly available, it's more robust to pass it as a parameter (argument) rather than relying on PowerShell's dynamic scoping (where descendant scopes implicitly see variables from ancestral scopes, unless shadowed by local variables).
If values must be shared between multiple event handlers, storing them in the script scope ($script:...), or, more generally, the parent scope may be needed, after all - see this answer.
I have a module with a lot of advanced functions.
I need to use a long list of ValidateSet parameters.
I would like to put the whole list of possible parameters in an array and then use that array in the functions themselves.
How can I pull the list of the whole set from an array?
New-Variable -Name vars3 -Option Constant -Value #("Banana","Apple","PineApple")
function TEST123 {
param ([ValidateScript({$vars3})]
$Fruit)
Write-Host "$Fruit"
}
The problem is that when I use the function it doesn't pull the content from the constant.
TEST123 -Fruit
If I specify the indexed value of the constant then it works.
TEST123 -Fruit $vars3[1]
It returns Apple.
You are misunderstanding how ValidateScript ...
ValidateScript Validation Attribute
The ValidateScript attribute specifies a script that is used to
validate a parameter or variable value. PowerShell pipes the value to
the script, and generates an error if the script returns $false or if
the script throws an exception.
When you use the ValidateScript attribute, the value that is being
validated is mapped to the $_ variable. You can use the $_ variable to refer to the value in the script.
... works. As the others have pointed out thus far. You are not using a script you are using a static variable.
To get what I believe you are after, you would do it, this way.
(Note, that Write- is also not needed, since output to the screen is the default in PowerShell. Even so, avoid using Write-Host, except for in targeted scenarios, like using color screen output. Yet, even then, you don't need it for that either. There are several cmdlets that can be used, and ways of getting color with more flexibility. See these listed MS powershelgallery.com modules)*
Find-Module -Name '*Color*'
Tweaking your code you posted, and incorporating what Ansgar Wiechers, is showing you.
$ValidateSet = #('Banana','Apple','PineApple') # (Get-Content -Path 'E:\Temp\FruitValidationSet.txt')
function Test-LongValidateSet
{
[CmdletBinding()]
[Alias('tlfvs')]
Param
(
[Validatescript({
if ($ValidateSet -contains $PSItem) {$true}
else { throw $ValidateSet}})]
[String]$Fruit
)
"The selected fruit was: $Fruit"
}
# Results - will provide intellisense for the target $ValidateSet
Test-LongValidateSet -Fruit Apple
Test-LongValidateSet -Fruit Dog
# Results
The selected fruit was: Apple
# and on failure, spot that list out. So, you'll want to decide how to handle that
Test-LongValidateSet -Fruit Dog
Test-LongValidateSet : Cannot validate argument on parameter 'Fruit'. Banana Apple PineApple
At line:1 char:29
Just add to the text array / file but this also means, that file has to be on every host you use this code on or at least be able to reach a UNC share to get to it.
Now, you can use the other documented "dynamic parameter validate set". the Lee_Daily points you to lookup, but that is a bit longer in the tooth to get going.
Example:
function Test-LongValidateSet
{
[CmdletBinding()]
[Alias('tlfvs')]
Param
(
# Any other parameters can go here
)
DynamicParam
{
# Set the dynamic parameters' name
$ParameterName = 'Fruit'
# Create the dictionary
$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
# Create the collection of attributes
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
# Create and set the parameters' attributes
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$ParameterAttribute.Position = 1
# Add the attributes to the attributes collection
$AttributeCollection.Add($ParameterAttribute)
# Generate and set the ValidateSet
$arrSet = Get-Content -Path 'E:\Temp\FruitValidationSet.txt'
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)
# Add the ValidateSet to the attributes collection
$AttributeCollection.Add($ValidateSetAttribute)
# Create and return the dynamic parameter
$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
$RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
return $RuntimeParameterDictionary
}
begin
{
# Bind the parameter to a friendly variable
$Fruit = $PsBoundParameters[$ParameterName]
}
process
{
# Your code goes here
$Fruit
}
}
# Results - provide intellisense for the target $arrSet
Test-LongValidateSet -Fruit Banana
Test-LongValidateSet -Fruit Cat
# Results
Test-LongValidateSet -Fruit Banana
Banana
Test-LongValidateSet -Fruit Cat
Test-LongValidateSet : Cannot validate argument on parameter 'Fruit'. The argument "Cat" does not belong to the set "Banana,Apple,PineApple"
specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
At line:1 char:29
Again, just add to the text to the file, and again, this also means, that file has to be on every host you use this code on or at least be able to reach a UNC share to get to it.
I am not sure exactly what your use case is, but another possibility if you're using PowerShell 5.x, or newer, is to create a class, or if you're using an older version you could embed a little C# in your code to create an Enum that you can use:
Add-Type -TypeDefinition #"
public enum Fruit
{
Strawberry,
Orange,
Apple,
Pineapple,
Kiwi,
Blueberry,
Raspberry
}
"#
Function TestMe {
Param(
[Fruit]$Fruit
)
Write-Output $Fruit
}
As title says, I need help to create an event listener. I've searched for other answers, but they don't really help me, and I'm new to programming so I am struggling with this.
The purpose of the script is to search for scripts on the local system, pull information about them, and then display it on a GUI interface. From the interface, you would select the different displayed scripts, and be able to run the selected script, get script notes from the corresponding text file for the script, and also open the file path in file explorer.
The first section of my code is this:
Add-Type –assemblyName PresentationFramework
Add-Type –assemblyName PresentationCore
Add-Type –assemblyName WindowsBase
[Xml]$xaml = (design code omitted)
$xmlNodeReader = New-Object System.Xml.XmlNodeReader($xaml)
$Window = [System.Windows.Markup.XamlReader]::Load($xmlNodeReader)
This is the base for the rest of my stuff, which I haven't been able to find much help with. The next section of code is the header to prepare to fill the columns.
$values = Get-ChildItem -Path C:\(Directory) -Filter *.ps1 -Recurse -ErrorAction SilentlyContinue -Force
$ListView = $Window.FindName("OutputList")
# outputlist is the name of the listview in the xaml #
(further down would be the foreach loop to create the columns and register the data)
$ListView.ItemsSource = $values
Past that, I have nothing. I can't figure out how to create the listener for the selected item on the ListView. If I can do that, I should be able to grab the data and be set.
From other questions, WPF listview has a SelectionChanged event, and from FoxDeploy's WPF GUIs in PowerShell series, Part IV, the way to add event listeners is like so:
$ListView.Add_SelectionChanged({
# code here
})
Where the {} is a PowerShell scriptblock