How to build dynamic link list with powershell and WPF? - winforms

I have an array of variable length with several website names und corresponding links. I will show them up in a Windows Forms based GUI.
The array will be read from an XML file, but it looks like this
$linklist = #(
("Site 1" , "https://link1.com"),
("Site 2" , "https://link2.com")
)
Then i have a Windows Forms window named "mainform" and create each item in there:
$Link1 = New-Object System.Windows.Forms.LinkLabel
$Link1.Text = $sitename
$Link1.Location = New-Object System.Drawing.Point(40,$calculatedPosition)
$Link1.add_Click({ start $sitelink })
$mainform.Controls.Add($Link1)
That could be done manually for each item in my array - so far, so easy, as log i have a fixed amount of items in my array.
But i like to do it dynamically, to handle arrays with customized content.
I tried to use dynamic variables, because every LinkLabel needs a different variable name.
I know, Dynamic variable names can be created by the New-Variable Cmdlet, but i have no idea, how manage this new variable for building a LinkLabel.
Thank you in advance for all your helpful ideas...

I would first create an ordered Hashtable out of your $linklist array-of-arrays to make things easier:
$linklist = #(
("Site 1" , "https://link1.com"),
("Site 2" , "https://link2.com")
)
# convert the array of arrays into an ordered Hashtable
$linkHash = [ordered]#{}
$linklist | ForEach-Object { $linkHash[$_[0]] = $_[1] }
Using that hashtable, creating the linklabels dynamically is not very hard to do:
$linkHash.GetEnumerator() | ForEach-Object {
$lnk = New-Object System.Windows.Forms.LinkLabel
$lnk.Text = $_.Name # set the name for the label
$lnk.Tag = $_.Value # store the link url inside the control's Tag property
$lnk.Location = New-Object System.Drawing.Point(40, $calculatedPosition)
# inside the scriptblock, $this refers to the LinkLabel control itself
$lnk.Add_Click({ Start-Process $this.Tag })
$mainform.Controls.Add($lnk)
$calculatedPosition += 15 # just a guess, you may want different vertical spacing
}

Related

How can I bind data to a wpf combobox to give me a display and data value?

I searched for hours to find a solution to the requirement below. I've found a number of articles but none make clear how you do this using wfp forms and PowerShell. I've tried to translate the examples I found that used Win forms and/or C# without success.
I have a two dimension arraylist that contains DisplayText and LinkText. The array will be dynamically created so I don't want to hard code the values anywhere.
What I want to do is display the combo box that displays the DisplayText but uses the associated value of LinkText when a selection is made.
I've read up on data binding and have a bit of an understanding of how it works; and hope it will become clearer as I use it.
My test code is as below.
[System.Collections.ArrayList]$BoxItems = #(#{Display = 'Google'; Link = 'https://www.google.com/'},#{Display = 'Yahoo'; Link = 'https://yahoo.com/'},#{Display = 'MSN'; Link = 'https://www.msn.com/'})
$BoxList = #()
foreach ($bi in $BoxItems) {
$BoxListItem = new-object System.Object
$BoxListItem | add-member -type NoteProperty -name Display -value $BoxItems[$i].Display
$BoxListItem | add-member -type NoteProperty -name Link -value $BoxItems[$i].Link
$BoxList += $BoxListItem
}
$WPFcbotest.ItemsSource = $BoxList
$WPFcbotest.DisplayMemberPath = $BoxList.Display
$WPFcbotest.SelectedValuePath = $BoxList.Link
$MainForm.ShowInTaskbar = $true
$MainForm.ShowDialog() > $null
Because I've tried, psCustomObjects, hashtables and arrays as a data source I'm no longer convinced I'm even using one method but maybe a mix up.
The current code shows a form with a combo box with 3 blank options.
I'd appreciate any direction or solution to get this sorted in the simplest way possible please.
OK, so I went back to the multiple sites I'd collected in my favourites on this topic. I figured the best option was https://powerintheshell.com/2015/12/05/psgui-binding-examples/ by David das Neves.
I noticed his example used a process object, so I cast my arraylist to type PSCustomObject.
I tidied up my code as below and tested.
[System.Collections.ArrayList]$BoxItems = #([PSCustomObject]#{Display = 'Google'; Link = 'https://www.google.com/'},[PSCustomObject]#{Display = 'Yahoo'; Link = 'https://yahoo.com/'},[PSCustomObject]#{Display = 'MSN'; Link = 'https://www.msn.com/'})
$WPFcbotest.ItemsSource = $BoxItems
$WPFcbotest.DisplayMemberPath = 'Display'
$WPFcbotest.SelectedValuePath = 'Link'
$MainForm.ShowInTaskbar = $true
$MainForm.ShowDialog() > $null
$wpfcbotest.SelectedValue now returns the link value to the corresponding display value.
The above works for me but is not the entry into data binding I thought I'd be making. If anyone has another method I'd be interested to see it.

Winform application Add data to list view after certain variable contains data

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.

How to have a Powershell validatescript parameter pulling from an array?

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
}

Sort Hashtable with Arrays as values

Description: I'm building a PowerShell-script that searches for files, then gives them unique names, copies them and then verifies them via hash-calculation - I chose to split the script in functions for each step, so it's easier to maintain the whole thing.
To get all values from one function to the other, I chose to use [hashtable]$FooBar - inside $FooBar, there are multiple arrays, such as FullName or OutputPath (which may change per file as they will be copied to subfolders named yyyy-mm-dd). All arrays are correlating with each other (meaning that index 1 contains all values of the first file, index 2 the values for the second file,...) and this works fine as of now.
A short simplified visualisation:
$FooBar = #{}
$FooBar.FullName = #()
$FooBar.Size = #()
$FooBar.Ext = #()
Get-ChildItem | ForEach-Object {
$FooBar.FullName += $_.FullName
$FooBar.Size += $_.Length
$FooBar.Ext += $_.Extension
}
However, I now need to sort them all by one value-set of one of the arrays, e.g. the size. Or, visualised again:
# From:
$FooBar
Name Value
---- -----
fullname {D:\AAA.XYZ, D:\BBB.ZYX, D:\CCC.YZX}
size {222, 111, 555}
extension {.XYZ, .ZYX, .YZX}
# To:
$FooBar = $FooBar | Sort-Object -Property Size -Descending
$FooBar
Name Value
---- -----
fullname {D:\CCC.YZX, D:\AAA.XYZ, D:\BBB.ZYX}
size {555, 222, 111}
extension {.YZX, .XYZ, .ZYX}
I tried $FooBar.GetEnumerator() | Sort-Object -Property Size, but this does not change anything. Google turned up suggestions on how to sort an array of hashtables, but in my case, it's the other way round, and I can't get my head around this because I don't even understand why this is a problem in the first place.
So my question is: is there any way to sort all arrays inside the hashtable by the value-set of one of the arrays? I can't get my head around this.
Disclaimer: I'm a PowerShell-autodidact with no reasonable background in scripting/programming, so it might well be that my "include everything in one hashtable"-solution isn't going to work at all or might be extremely inefficient - if so, please tell me.
The easiest way to go about what I believe you are trying to do is Select-Object
$fooBar = Get-ChildItem | Select-Object FullName, Size, Extension
This will create an array of new objects that only have the desired properties. The reason this works and your method doesn't is because Sort-Object works on properties and the property you are specifying is behind a few layers.
If you need more flexibility than just exact properties, you can create your own like this
$fooBar = Get-ChildItem | Select-Object #{Name = 'SizeMB'; Expression = {$_.Size / 1MB}}
Or manually create new properties with the [PSCustomObject] type accelerator:
$fooBar = Get-ChildItem | ForEach-Object {
[PSCustomObject]#{
FullName = $_.FullName
Extension = $_.Extension
Size = $_.Size
}
}
Update
If you need to add additional properties to the object after it's initially created you have a few options.
Add-Member
The most common method by far is by using the Add-Member cmdlet.
$object | Add-Member -MemberType NoteProperty -Name NewProperty -Value 'MyValue'
$object
Something important to keep in mind is that by default this cmdlet does not return anything. So if you place the above statement at the end of a function and do not separately return the object, your function won't return anything. Make sure you either use the -PassThru parameter (this is also useful for chaining Add-Member commands) or call the variable afterwards (like the example above)
Select-Object
You can select all previous properties when using calculated properties to add members. Keep in mind, because of how Select-Object works, all methods from the source object will not be carried over.
$fooBar | Select-Object *, #{Name = 'NewProperty'; Expression = {'MyValue'}}
psobject.Properties
This one is my personal favorite, but it's restricted to later versions of PowerShell and I haven't actually seen it used by anyone else yet.
$fooBar.psobject.Properties.Add([psnoteproperty]::new('NewProperty', 'MyValue'))
$fooBar
Each member type has it's own constructor. You can also add methods to $fooBar.psobject.Methods or either type to $fooBar.psobject.Members. I like this method because it feels more explicit, and something about adding members with members feels right.
Summary
The method you choose is mostly preference. I would recommend Add-Member if possible because it's the most used, therefore has better readability and more people who can answer questions about it.
I would also like to mention that it's usually best to avoid adding additional members if at all possible. A function's return value should ideally have a reliable form. If someone is using your function and they have to guess when a property or method will exist on your object it becomes very difficult to debug. Obviously this isn't a hard and fast rule, but if you need to add a member you should at least consider if it would be better to refactor instead.
For all practical purposes I'd strongly suggest you just store the objects you need in a single array, sort that once and then reference the individual properties of each object when needed:
$FooBar = Get-ChildItem |Sort-Object -Property Length
# Need the Extension property of the object at index 4?
$FooBar[4].Extension
To answer your actual question:
Array.Sort() has an overload that takes keys and values arrays separately. You could make a copy of the array you want to sort on for each other property you want to sort:
# Create hashtable of correlated arrays
$FooBar = #{}
$FooBar.FullName = #()
$FooBar.Size = #()
$FooBar.Ext = #()
# Types cast explicitly to avoid Array.Sort() calling .CompareTo() on the boxing object
Get-ChildItem | ForEach-Object {
$FooBar.FullName += [string]$_.FullName
$FooBar.Size += [int]$_.Length
$FooBar.Ext += [string]$_.Extension
}
# Define name of reference array property
$SortKey = 'Size'
# Sort all arrays except for the reference array
$FooBar.Keys |Where-Object {$_ -ne $SortKey} |ForEach-Object {
# Copy reference values to new array
$Keys = $FooBar[$SortKey].Clone()
# Sort values in target array based on reference values
[array]::Sort($Keys,$FooBar[$_])
}
# Finally sort the reference array
[array]::Sort($FooBar[$SortOn])
The above only works as long as the reference array is made up of value types
PowerShell makes working with objects ridiculously easy.
Try:
$FooBar = Get-Childitem
$FooBar | Get-Member
This will tell you that $Foobar actually contains objects of FileInfo and DirectoryInfo type, and show you the Properties available.
$FooBarSortedBySizeDesc = $FooBar | Sort-Object Length -Descending
$FooBarFullNamesOnly = $FooBar.FullName

Index is out of range powershell

I have a script that builds a GUI with a list of printers that will be selected by the user.
These printers are also on a CSV file built like this :
Computer (name of the printer); IP
xxxx;x.x.x.x
I want to collect all the selected values in an array named x
Then I want to take every entry in the CSV that corresponds to the selected item and put it in another array named y
Finally I export the y array into a new CSV that will be used to install the printers on the domain.
I tried to go straight from second step to last step but i couldn't.
Here is the part of the code :
$OKButton.Add_Click({
foreach ($objItem in $objListbox.SelectedItems)
{$x += $objItem}
y=#()
for ($i=0; $i -lt $x.length; $i++)
{
$y[$i]=Import-Csv C:\Users\Administrateur\Desktop\Classeur33.csv | Where-Object {$_.Computer -eq $x[$i]}
}
$y > C:\Users\Administrateur\Desktop\allezjoue.csv
I've tried to do it with a 3 values x array in another script and it worked fine, but I really need to keep the listbox that allows the user to select the printers he wants.
Powershell always returns me "Index out of range"
I tried to put "$y=$x" so they have the same range, but when I do this it returns that I can't index in an object which has "System.string" type.
This is PowerShell and very object oriented. Use the objects and collections at hand.
Decriptive variable names are your friend.
$objListbox.SelectedItems is already a collection of objects.
Put it in a variable and loop through it with Foreach-Object aka foreach.
Import-CSV returns a collection of objects.
$Selection = $ObjListbox.SelectedItems
$printers = Import-CSV 'C:\Users\Administrateur\Desktop\Classeur33.csv'
foreach ($chosen in $Selection) {
$printers = $printers | where-object { $_.Computer -eq $Chosen.Name }
}
$printers | Export-CSV 'C:\Users\Administrateur\Desktop\allezjoue.csv' -NoTypeInformation
$Chosen.Name should be edited to conform with whatever objects you get in $Selection. You can test this by $ObjListbox.SelectedItems | Get-Member and examining the members for a property with the name of the item selected, then assuming the names match what's in your CSV, you should be good.
(bonus note) Storing data in and running as local admin is bad practice, even on your home lab. Your mistakes will have the power of local admin, and your users will not be able to run the scripts since the source/results files are in admin's desktop.

Resources