Powershell set combobox index from function - winforms

I want to modify the selection of a Windows form combobox from a function.
In the code below, the combobox is created in a function New-ComboBox and returned.
In a different function Set-ComboBoxSelection, I want to access this combobox and modify its selection. The function is able to read the selection from the combobox, but not to modify it.
Minimal example code:
Add-Type -assembly System.Windows.Forms
$main_form = New-Object System.Windows.Forms.Form
$main_form.Text ='Test combobox'
$main_form.Width = 300
$main_form.Height = 50
$main_form.AutoSize = $true
function New-ComboBox($cbstrings, $defaultIndex, $x, $y, $w){
$combobox = New-Object System.Windows.Forms.ComboBox
$combobox.Width = $w
$combobox.Location = New-Object System.Drawing.Point($x, $y)
$combobox.DropDownStyle = "DropDownList"
$main_form.Controls.Add($combobox)
Foreach ($strings in $cbstrings)
{ $combobox.Items.Add($strings); }
$combobox.SelectedIndex = $defaultIndex
return $combobox
}
function Set-ComboBoxSelection(){
Write-Host $combobox.SelectedIndex
Write-Host $combobox.SelectedItem
$combobox.SelectedIndex = 2
$combobox.SelectedItem = $combobox.Items[2]
}
$combobox = New-ComboBox "v1","v2","v3" 1 10 10 200
Set-ComboBoxSelection
$main_form.ShowDialog()
This will return:
The property 'SelectedIndex' cannot be found on this object.
Verify that the property exists and can be set.
The property 'SelectedItem' cannot be found on this object.
Verify that the property exists and can be set.
The values SelectedIndex and SelectedItem will however correctly return 2 and v3 from Set-ComboBoxSelection.
Why do I have "read" access but not "write" access on this combobox?
For a TextBox, created within a function and returned, I'm able to set the Text value from within a different function.

The issue in your code comes from Items.Add which adds a few integer to the output so while you expect having a ComboBox as output, but the output is an array of objects.
To solve the problem you can suppress unwanted outputs:
$combobox.Items.Add($strings) | Out-Null
More information
In PowerShell, in addition to what you return explicitly using return statement, or using Write-Out, if you write a literal, a variable or a function call which returns an output or use write those values also will be added to the function output.
For example, F2 in the following code will have two return values true and 1. To be more precise, the return value will be and array of objects object[], instead of int:
Function F1()
{
return $true
}
Function F2()
{
F1
return 1
}
To prevent the problem, you can:
| Out-Null: Use | out-null after functions to suppress their output, in above example F1 | Out-Null
[void]: Cast the out-put to void, in above example [void](F1)
> $null: Write output to null, in above example F1 > $null
Capture the output in a variable, in above example $null = F1
Enough of the concepts, the When creating PowerShell function, you need to be careful about what is returned from the function. You need to prevent unwanted return values from functions, for example return value of Items.Add is also returned from function and as a result, instead of just returning a single ComboBox, you are returning an array of objects.

Related

Windows forms - add data to list view on button click [duplicate]

This question already has answers here:
Using form to return variable fails to produce a return value
(3 answers)
Closed 1 year ago.
I have a winform application which populate some data after I click on $button_UpdateTS, how do I add the data stored in a variable, that comes available after I click on that button ?
The data I want in my list view is stored in an array called $results
$button_UpdateTS = New-Object System.Windows.Forms.Button
$button_UpdateTS.Location = New-Object System.Drawing.Size(15, 954)
$button_UpdateTS.Size = New-Object System.Drawing.Size(320, 32)
$button_UpdateTS.TextAlign = "MiddleCenter"
$button_UpdateTS.Text = “Update Tasksequence”
$button_UpdateTS.Add_Click( { $Results = Set-DynamicVariables
-Manufacturer "$($listview_Vendor.SelectedItems)"
-TSPackageID "$($ListView_Tasksequences.SelectedItems.SubItems[1].Text)" -WhatIf })
$Form.Controls.Add($button_UpdateTS)
Which gives me :
$Results =
SKUNotExistsDriverName : XPS Notebook 9560
SKUNotExistsDriverID : PS10053F
SKUNotExistsDriverSKU : 07BE
SKUNotExistsDriverVersion : A12
SKUNotExistsBIOSName : XPS Notebook 9560
SKUNotExistsBIOSID : PS10053E
SKUNotExistsBIOSSKU : 07BE
SKUNotExistsBIOSVersion : 1.15.0
This is the list I want it stored in :
$Global:listview_NotExists_SKU = New-Object System.Windows.Forms.ListView
$listview_NotExists_SKU.Location = New-Object System.Drawing.Size(515, 670)
$listview_NotExists_SKU.Size = New-Object System.Drawing.Size(486, 235)
$listview_NotExists_SKU.View = "Details"
$listview_NotExists_SKU.FullRowSelect = $true
$listview_NotExists_SKU.MultiSelect = $true
$listview_NotExists_SKU.Sorting = "None"
$listview_NotExists_SKU.AllowColumnReorder = $true
$listview_NotExists_SKU.GridLines = $true
$listview_NotExists_SKU.Add_ColumnClick( { SortListView $this $_.Column })
$Form.Controls.Add($listview_NotExists_SKU)
I tried with this function, but that does not work:
Function Get-Results {
ForEach ($Result in $Results) {
$listview_NotExists_SKU.Items.Add($Result)
}
}
$Form.Add_Shown( { $Form.Load; Get-results })
Because an event-handling script block added with e.g. .Add_Click() runs in in a child scope of the caller, assigning to variable $Results there ($Results = ...) creates a scope-local variable that neither the scope in which the event handler was set up nor subsequently invoked event handlers can see.
To create a variable in the script scope, which subsequently invoked event handlers can see as well[1], use the $script: scope specifier:
$button_UpdateTS.Add_Click( { $script:Results = ... } )
Note:
If the scope in which the event handlers are set up isn't the script scope (e.g., if the code is inside a function) and you want to more generically reference that scope from within an event handler, use Set-Variable -Scope 1 -Name Results -Value ..., which targets the respective parent scope.[1]
An alternative to setting a variable in the parent scope explicitly is to use a hashtable defined in the parent scope whose entries can be used in lieu of variables that the event-handler script blocks can modify too.[2] See this answer for an example.
[1] For more information about scopes in PowerShell, see the bottom section of this answer.
[2] This technique works, because even though the variable containing the hashtable is defined in the parent scope, the child scope can access its value and modify the entries of the referenced hashtable object rather than the variable itself.

values from a foreach loop in a function into an array

I have a function that replaces PackageID in a SCCM task sequence, I would like to capture all those package IDs into a variable, so I would be able to create a report based on that.
The problem is that I already have a foreach loop doing the work, and I can't figure out how to not overwrite the values.
$Driver.PackageID comes from a foreach loop based on $Drivers, which contains
If I run the code I get this as I have Write-Output defined:
Updated code:
function Set-Drivers{
foreach ($Driver in $Drivers) {
Write-Output "Driver Name: $($Driver.Name)"
Write-Output "DriverPackageID: $($Driver.PackageID)"
}
}
$array = #()
$array = Set-Drivers
$hash = [ordered]#{
'DriverName' = $Driver.Name
'DriverID' = $Driver.PackageID
}
$array += New-Object -Typename PSObject -Property $hash
Can someone explain, why I only get the first result in my $array? I can see the values are being overwritten if I run it in debug mode.
Your code is not iterating over the results, but instead only using one of them. This what you intended.
$array = $drivers | foreach {
[ordered]#{
DriverName = $_.Name
DriverID = $_.PackageID
}
}
Your function doesn't return anything. It only writes lines to the console. Then after the function is finished, you create a single object and add that to your array.
Try something like
function Set-Drivers{
$result = foreach ($Driver in $Drivers) {
[PsCustomObject]#{
'DriverName' = $Driver.Name
'DriverID' = $Driver.PackageID
}
}
# output the result
# the comma wraps the result in a single element array, even if it has only one element.
# PowerShell 'flattens' that upon return from the function, leaving the actual resulting array.
,$result
}
$array = Set-Drivers
# show what you've got
$array

Default text for DropDownList

For a long time I used the usual DropDown as a ComboBoxStyle. However, I have only 2 items in the ComboBox and searching manually in this case looks unreasonable. Therefore, I decided to refer to DropDownList, since the text in it is immutable.
However, along with this I ran into a problem. In the case when no element is selected (if I understand correctly, in this case the -1 element is selected) I can not display the default text, for example, an invitation to select an element from the list. The variant with ComboBox.Text ("Please, select any value") no longer works (because the text is immutable) and here I am stumped, because I do not know what to do.
Sure, I have tried to look for something in the C# branch, but did not find anything working for powershell. Here is the option I have tried and which does not works:
$MethodComboBox.Add_TextChanged($defaultLabel)
$defaultLabel =
{
if ($ComboBox.SelectedIndex -lt 0)
{
$ComboBox.Text = "Please, select any value";
}
else
{
$ComboBox.Text = $ComboBox.SelectedText;
}
}
You can set the DrawMode of the ComboBox to OwnerDrawFixed and then handle DrawItem event and render a custom select text when index is -1:
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$combo = New-Object System.Windows.Forms.ComboBox
$combo.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList
$combo.DrawMode = [System.Windows.Forms.DrawMode]::OwnerDrawFixed
$combo.Width = 200
$combo.ItemHeight = 24
$combo.Items.Add("Male")
$combo.Items.Add("Female")
$form.Controls.Add($combo)
$combo.Add_DrawItem({param($sender,$e)
$text = "-- Select Gender --"
if ($e.Index -gt -1){
$text = $sender.GetItemText($sender.Items[$e.Index])
}
$e.DrawBackground()
[System.Windows.Forms.TextRenderer]::DrawText($e.Graphics, $text, $combo.Font, `
$e.Bounds, $e.ForeColor, [System.Windows.Forms.TextFormatFlags]::Default)
})
$form.ShowDialog() | Out-Null
$form.Dispose()

Exception calling "ExecuteNonQuery" with "0" argument(s): "Failed to convert parameter value from a Object[] to a IEnumerable 1." [duplicate]

This is driving me crazy. I have a library I source from multiple scripts, which contains the following function:
function lib_open_dataset([string] $sql) {
$ds = new-object "System.Data.DataSet"
$da = new-object "System.Data.SqlClient.SqlDataAdapter" ($sql, $_conn_string)
$record_count = $da.Fill($ds)
return $ds
}
This is called pretty much everywhere and it works just fine, except that I normally have to do this:
$ds = lib_open_dataset($some_sql)
$table = $ds.Tables[0]
foreach ($row in $table.Rows) {
# etc
}
So I created a new simple wrapper function to avoid the extra step of dereferencing the first table:
function lib_open_table([string] $sql) {
$ds = lib_open_dataset $sql
return $ds.Tables[0]
}
The problem is that what's being returned from here is the Rows collection of the table for some reason, not the table itself. This causes the foreach row loop written as above to fail with a "Cannot index into a null array." exception. After much trial and error I figured out this works:
foreach ($row in $table) {
# etc
}
Note the difference between $table.Rows and just $table in the foreach statement. This works. Because $table actually points to the Rows collection. If the statement
return $ds.Tables[0]
is supposedly correct, why is the function returning a child collection of the table object instead of the table itself?
I'm guessing there's something in the way Powershell functions work that's causing this obviously, but I can't figure out what.
You can use the comma operator to wrap the rows collection in an array so that when the array is unrolled you wind up with the original rows collection e.g.:
function lib_open_table([string] $sql) {
$ds = lib_open_dataset $sql
return ,$ds.Tables[0]
}
Essentially you can't prevent PowerShell from unrolling arrays/collections. The best you can do is workaround that behavior by wrapping the array/collection within another, single element array.
PowerShell special-cases the DataTable internally. It does not implement any of the usual suspect interfaces like ICollection, IList or IEnumerable which normally trigger the unrolling. You can dig into this a bit with:
PS> $dt = new-object data.datatable
PS> $dt -is [collections.ienumerable]
False
Yet:
PS> $e = [management.automation.languageprimitives]::GetEnumerator($dt)
PS> $e.gettype()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
False False RBTreeEnumerator System.ValueType
-Oisin
2 things you need to indeed focus on
a) prepend your returned object with the comma indeed
b) when you're filling your adaptor, make sure to either assign the outcome to a (disposalble) variable or do an Out-Null
I didn't do the Out-Null and even with a prepended comma, I kept getting a collection back (item 0= number of rows from the query, item1= the datatable)
Drove my a bit crazy until I picked the Out-null parameter out.
Very weird IMHO, as I'm asking specifically to return the datatable but kept getting the collection back, even with the "," in front
function Oracleconnection
{
process
{
trap
{
Write-Host "error occured on oracle connection"
Write-Host $_
continue
}
[System.Reflection.Assembly]::LoadWithPartialName(“System.Data.OracleClient”) | out-null
$connection = new-object system.data.oracleclient.oracleconnection( `
"Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=myhost.host)(PORT=1800)) `
(CONNECT_DATA=(SERVICE_NAME=myservicename)));User Id=myid;Password=mypassword;");
$query = "SELECT country, asset FROM table "
$set = new-object system.data.dataset
$adapter = new-object system.data.oracleclient.oracledataadapter ($query, $connection)
$adapter.Fill($set) | Out-Null
$table = new-object system.data.datatable
$table = $set.Tables[0]
return ,$table
}
}
(Concepts from the Answer by Keith!)

powershell, function calls fomr an array

So I am curious how to call a function from an array?
Function example:
function test($a, $b)
{
write-host $a
write-host $b
}
An array
$testArray = #{"test";"testA";"testB"}
And the whole bit I want to work is
$testArray[0] $testArray[1] $testArray[2]
Essentially mimic this
test "testA" "testB"
Reason for this is I have a few arrays like this and a loop that would go through each one using the custom function call on the data in each array.
Basically trying a different programing style.
Ok, it sounds like you have an array of arrays to be honest, so we'll go with that. Then let's reference this SO question which very closely resembles your question, and from that take away the whole [scriptblock]::create() thing, and splatting arrays. From that we can come up with this script:
function test($a, $b)
{
Write-Host "Function 'test'"
write-host $a
write-host $b
}
function test2($a, $b)
{
Write-Host "Function 'test2'"
write-host $b
write-host $a
}
$TestArray = #() #the correct way to create an array, instead of a broken HashTable
$testArray = #("test","testA","testB"),#("test2","testC","testD")
ForEach($Test in $TestArray){
$script = [scriptblock]::Create($test[0]+" #Args")
$script.Invoke($test[1..$test.count])
}
If all you have is one array, and not an array of arrays then I guess this is pretty simple. You could do:
$testArray = #("test","testA","testB")
$script = [scriptblock]::Create($testArray[0]+" #Args")
$script.Invoke($testArray[1..$testArray.count])
Edit (Capturing): Ok, to capture the results of a function you should be able to prefix it with $Variable = and be good to go, such as:
$MyResults = $script.Invoke($testArray[1..$testArray.count])
That will capture any output given by the function. Now, since the functions we have been working with only perform Write-Host they don't actually output anything at all, they just print text to the screen. For this I would modify the function a bit to get real output that's usable. In this example the function takes 2 parameters as input, and creates a new object with those 2 parameters assigned to it as properties. That object is the output.
function test($a, $b)
{
New-Object PSObject -Property #{Value1=$a;Value2=$b}
}
$testArray = #("test","testA","testB")
$script = [scriptblock]::Create($testArray[0]+" #Args")
$MyResults = $script.Invoke($testArray[1..$testArray.count])
Now if you ran that you would end up with the variable $MyResults being a PSCustomObject that has 2 properties named Value1 and Value2. Value1 would contain the string "testA" and Value2 would contain the string "testB". Any pair of strings passed to that function would output 1 object with 2 properties, Value1 and Value2. So after that you could call $MyResults.Value1 and it would return testA.

Resources