Set ContextMenu items from a string list - powershell

Basically I'm trying to create a ContextMenu that shows a list of mounted network drive, so the user can click on one and access it.
The problem is : the list is not fix, but depend on a string list extracted from a DataGrid.
In my mind, I could just create the menu items by looping on that string list.
Here is the code sample :
$Main_Tool_Icon = New-Object System.Windows.Forms
[...]
# Add menu entries
$contextmenu = New-Object System.Windows.Forms.ContextMenu
$Main_Tool_Icon.ContextMenu = $contextmenu
foreach ($nameTag in $dataGrid[$DATAGRID_COLUMN_NAME]){
$menuEntry = New-Object System.Windows.Forms.MenuItem
$menuEntry.Text = $nameTag
$Main_Tool_Icon.ContextMenu.MenuItems.AddRange($menuEntry)
}
# Add separator
$Main_Tool_Icon.ContextMenu.MenuItems.Add('-')
# Last one to show main form
$displayMainform = New-Object System.Windows.Forms.MenuItem
$displayMainform.Text = "Edit"
$Main_Tool_Icon.ContextMenu.MenuItems.Add($displayMainform)
It appears that's wrong and only shows the separator and last "Edit" line.
Is there a way to populate a contextual menu with a list ?
Or I should look for a better way to do the job ?
EDIT #Mathias R. Jessen
You're right about AddRange(), it's just a mistake.
I can't show everything since it's quite big. I'll try to synthesize an answer properly.
The data grid is created/populated by my script and is just a table of strings from a csv file.
$DATAGRID_COLUMN_NAME = 'NAME' it's one of constants defined for my datagrid headers.
Here is the setup
# set datagrid
$dataGrid = New-Object System.Windows.Forms.DataGridView
[...]
$dataGrid.Columns.Add($DATAGRID_COLUMN_NAME,'Name')
$dataGrid.Columns[0].DataPropertyName = $dataGrid.Columns[0].Name
Then some databinding
# initiate the data table
$table = New-Object System.Data.DataTable
[...]loop on csv file[...]
# bind data table to data grid
$dataGrid.DataSource = $table
EDIT #SantiagoSquarzon
You were right for pointing out $dataGrid[$DATAGRID_COLUMN_NAME]. The syntax was bad and returned something null. Instead, I looped over the $dataGrid.Rows like so:
foreach ($row in $dataGrid.Rows){
$nameTag = $row.Cells[$DATAGRID_COLUMN_NAME].Value
if ($nameTag -ne $null){
$menuEntry = New-Object System.Windows.Forms.MenuItem
$menuEntry.Text = $nameTag
$Main_Tool_Icon.ContextMenu.MenuItems.AddRange($menuEntry)
}
}
Now I just have to figure out how to invoke there click event.

Related

How to replace/update the Value of an Attribute in LDAP Directory using PowerShell?

The entries in our companys Non-AD LDAP Server look like this:
uid = e145871
sn = Smith
givenName = John
department = Research & Development
department = Human Resource
And so on...
I've developed a PowerShell script to add specific attributes and values which is working just fine. Now I need to replace specific values but the issue is the identical attribute name. (In this case it's "department")
My goal is to replace "Research & Development" with "Something Else". If I run the following script it gets replaced but Human Resource is deleted as well. Is it possible to replace only one value without touching/deleting the other?
$r = New-Object -TypeName System.DirectoryServices.Protocols.ModifyRequest
$r.DistinguishedName = "uid=e145871,ou=identities,ou=users,o=items,dc=company,dc=domain,dc=com"
$DirectoryRequest_value = New-Object "System.DirectoryServices.Protocols.DirectoryAttributeModification"
$DirectoryRequest_value.Name = "department"
$DirectoryRequest_value.Contains("Research & Development")
$DirectoryRequest_value.Operation = [System.DirectoryServices.Protocols.DirectoryAttributeOperation]::Replace
$DirectoryRequest_value.Add("SomethingElse")
$r.Modifications.Add($DirectoryRequest_value)
$result = $connection.SendRequest($r)
Thanks!
The LDAP Replace operation replaces (or overwrites) the entire value of the attribute, including any existing values that might exist as part of a multi-valued attribute.
From RFC4511 ยง4.6 - "Modify Operation":
- operation: Used to specify the type of modification being
performed. Each operation type acts on the following
modification. The values of this field have the following
semantics, respectively:
[...]
replace: replace all existing values of the modification
attribute with the new values listed, creating the attribute
if it did not already exist. A replace with no value will
delete the entire attribute if it exists, and it is ignored
if the attribute does not exist.
Instead, add two separate modifications to the request - one to add "SomethingElse" and one to remove "Research & Development":
$targetObject = 'uid=e145871,ou=identities,ou=users,o=items,dc=company,dc=domain,dc=com'
$attributeName = 'department'
$oldValue = 'Research & Development'
$newValue = 'SomethingElse'
$request = [System.DirectoryServices.Protocols.ModifyRequest]::new()
$request.DistinguishedName = $targetObject
# This modification will add the new value "SomethingElse"
$addNewDepartment = #{
Name = $attributeName
Operation = 'Add'
} -as [System.DirectoryServices.Protocols.DirectoryAttributeModification]
$addNewDepartment.Add($newValue) |Out-Null
$request.Modifications.Add($addNewDepartment) |Out-Null
# This modification will remove the old value "Research & Development"
$removeOldDepartment = #{
Name = $attributeName
Operation = 'Delete'
} -as [System.DirectoryServices.Protocols.DirectoryAttributeModification]
$removeOldDepartment.Add($oldValue) |Out-Null
$request.Modifications.Add($removeOldDepartment) |Out-Null
$result = $connection.SendRequest($request)

PowerShell NotifyIcon Context Menu

I have the following NotifyIcon running using PowerShell:
This is the context menu opened by right clicking the icon, it's just showing exit at the moment:
I would like to know how I can add two event handlers:
Run a function when there is a left click event on the icon
Add another option to the right click menu that also runs a function
I've been searching the web for over an hour and I have tried about 20 different variations using old code from 5 or 6 different websites (all showing wildly different examples). I have gained nothing but a headache. Can anyone offer any guidance/direction?
$ProgramDataPath = "$ENV:ProgramData\test"
$ProgramFilesPath = "${env:ProgramFiles(x86)}\test"
[void][System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
$STForm = New-Object System.Windows.Forms.form
$NotifyIcon = New-Object System.Windows.Forms.NotifyIcon
$ContextMenu = New-Object System.Windows.Forms.ContextMenu
$MenuItem = New-Object System.Windows.Forms.MenuItem
$Timer = New-Object System.Windows.Forms.Timer
$HealthyIcon = New-Object System.Drawing.Icon("$ProgramFilesPath\icons\healthy.ico")
$UnhealthyIcon = New-Object System.Drawing.Icon("$ProgramFilesPath\icons\unhealthy.ico")
$STForm.ShowInTaskbar = $false
$STForm.WindowState = "minimized"
$NotifyIcon.Icon = $HealthyIcon
$NotifyIcon.ContextMenu = $ContextMenu
$NotifyIcon.ContextMenu.MenuItems.AddRange($MenuItem)
$NotifyIcon.Visible = $True
# We need to avoid using Start-Sleep as this freezes the GUI. Instead, we'll utilitse the .NET forms timer, it repeats a function at a set interval.
$Timer.Interval = 300000 # (5 min)
$Timer.add_Tick({ Load-Config })
$Timer.start()
# This will appear as a right click option on the system tray icon
$MenuItem.Index = 0
$MenuItem.Text = "Exit"
$MenuItem.add_Click({
$Timer.Stop()
$NotifyIcon.Visible = $False
$STForm.close()
})
function Load-Config
{
#Get-Content some Data from a file here
if ($warn)
{
$NotifyIcon.Icon = $UnhealthyIcon
$NotifyIcon.ShowBalloonTip(30000, "Attention!", "Some data from a file here...", [system.windows.forms.ToolTipIcon]"Warning")
Remove-Variable warn
}
else
{
$NotifyIcon.Icon = $HealthyIcon
}
}
Load-Config
[void][System.Windows.Forms.Application]::Run($STForm)
Lets talk about what you really need. It looks like you have alot of unneeded parts like a timer and so on. All you need is a runspace. A Open Form will keep the runspace open without the need for that timer. Make sure the $Form.ShowDialog() is the last thing run.
So lets move on to the NotifyIcon popup. The method that makes that popup happen is private, which means we will need to reach it through reflection. We will also need to set the event for the Notify Icon to run on MouseDown as well as get the button clicked $_.button
Make sure you set the $NotifyIcon.Icon to a Icon or else the Notify Icon wont show up.
Working Script
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object System.Windows.Forms.Form
$form.ShowInTaskbar = $true
$form.WindowState = [System.Windows.WindowState]::Normal
$MenuItemLeft = New-Object System.Windows.Forms.MenuItem
$MenuItemLeft.Text = "Left Exit"
$MenuItemLeft.add_Click({
$NotifyIcon.Visible = $False
$form.Close()
$NotifyIcon.Dispose()
})
$ContextMenuLeft = New-Object System.Windows.Forms.ContextMenu
$ContextMenuLeft.MenuItems.Add($MenuItemLeft)
$MenuItemRight = New-Object System.Windows.Forms.MenuItem
$MenuItemRight.Text = "Right Exit"
$MenuItemRight.add_Click({
$NotifyIcon.Visible = $False
$form.Close()
$NotifyIcon.Dispose()
})
$ContextMenuRight = New-Object System.Windows.Forms.ContextMenu
$ContextMenuRight.MenuItems.Add($MenuItemRight)
$NotifyIcon= New-Object System.Windows.Forms.NotifyIcon
$NotifyIcon.Icon = "C:\Test\Test.ico"
$NotifyIcon.ContextMenu = $ContextMenuRight
$NotifyIcon.add_MouseDown({
if ($_.Button -eq [System.Windows.Forms.MouseButtons]::left ) {
$NotifyIcon.contextMenu = $ContextMenuLeft
}else{
$NotifyIcon.contextMenu = $ContextMenuRight
}
$NotifyIcon.GetType().GetMethod("ShowContextMenu",[System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic).Invoke($NotifyIcon,$null)
})
$NotifyIcon.Visible = $True
$form.ShowDialog()
$NotifyIcon.Dispose()
I Reread your post so ill provide you with the most important parts
For Powershell to run your commands a runspace must be active. A Runspace takes powershell commands and turns them into real actions.
Since you went with powershell for this then the notifyicons actions are dependent on a runspace to interpret those actions.
A NotifyIcon is basically just a icon in the corner that can popup a balloon notification or Context Menu.
So when you look you will see $NotifyIcon.ContextMenu that is a property that holds a ContextMenu Object. A Context Menu Object contains Menu Items.
So just add MenuItems to a ContextMenu Object and then add that ContextMenu Object to $NotifyIcon.ContextMenu. Now you can change and add all the items you like.
Since powershell waits for the form to close before moving on to the next line of code the $Form.ShowDialog() will keep the runspace alive until the form is exited.
Lets look at this nasty mess :
$NotifyIcon.GetType().GetMethod("ShowContextMenu",[System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic).Invoke($NotifyIcon,$null)
This is called reflection. It allows you to interact with a class. In easier terms. ShowContextMenu method is private and cant be run normally from outside the internal workings of the class. Using reflection you can call it anyways.
So lets break it down just a little more since this is really what you asked about.
GetType() will get you what the object is. If i did "HEY".gettype() it would tell me that this object was a string. In this case $NotifyIcon.GetType() is telling me that this is a NotifyIcon. Whats happening its its bring me back a Type Class.
In this we see GetMethod("ShowContextMenu") but let me dig a little deeper here... How did we know there was a method called ShowContextMenu. Well what we can do is view all the members of this NotifyIcon class by using GetMembers(). Now GetMembers() is really just a search... by default only searches for public Members so we needs to search all Members. The parameters of what to search for is in an enum [System.Reflection.BindingFlags] and some Bitwise math.
$BitWise
[System.Reflection.BindingFlags].GetEnumNames() | %{
$BitWise = $BitWise -bor [System.Reflection.BindingFlags]$_
} | out-null
$NotifyIcon.GetType().GetMembers($BitWise) | ?{$_.Name -like "*Context*"} | select Name, MemberType
This says find all items that contain the word Context in its name and display its Full name and what Type it is. In response we get
Name MemberType
---- ----------
set_ContextMenu Method
get_ContextMenu Method
get_ContextMenuStrip Method
set_ContextMenuStrip Method
ShowContextMenu Method
ContextMenu Property
ContextMenuStrip Property
contextMenu Field
contextMenuStrip Field
We can see ShowContextMenu and we can also see its a method
So now we need to get that method directly. Here comes getMethod() which is just another search that brings back 1 item instead of all the items. So
GetMethod("ShowContextMenu",[System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic)
Get Method ShowContextMenu it will be Private so NonPublic and a instance of the class has to be created before it can run so Instance.
.Invoke($NotifyIcon,$null)
Then we invoke the method by telling it which control has the method we want to run and pass any parameters which are none so $null.
And thats how you do it.

How to get typed listbox item

I created a powershell form with a few entry fields that are checked for input before doing any further actions. One of them is a listbox with a set of numbers representing printer numbers in our organization.
When the end-user selects the item with the printer via the dropdown menu, my script can perfectly read via .SelectedItem. However, they can also type the number in the field, and I am struggling to find how to get the value from the field when it is typed instead of selected.
Listbox :
$dropdown_Machine = New-Object System.Windows.Forms.Combobox
$dropdown_Machine.Location = New-Object System.Drawing.Size(250,215)
$dropdown_Machine.Size = New-Object System.Drawing.Size(300,50)
$dropdown_Machine.Font = $DROPDOWNFONT
[void] $dropdown_Machine.Items.Add(" ")
[void] $dropdown_Machine.Items.Add("729")
[void] $dropdown_Machine.Items.Add("730")
[void] $dropdown_Machine.Items.Add("744")
... some more items ...
$Form_PrintLabel.Controls.Add($dropdown_Machine)
My check that is working when selected :
$searchmach = $dropdown_Machine.SelectedItem
When it is typed, it gives me an empty string. When it is selected it gives me the input I expected.
Can anyone push me in the right direction ?
Thanks.
Regards,
Mike
if ($dropdown_Machine.SelectedIndex -eq -1) { # nothing selected
$dropdown_Machine.Text
} else {
$dropdown_Machine.SelectedItem
}

PowerShell Windows Forms Wrapper

In PowerShell it is quiet common to use Windows Forms to build a User Interface for small cmdlets but the syntaxis required for this are often partly redundant and quiet verbose. This leads to the question:
Is there a way to minimize the code required or does there exist a Windows Forms wrapper for PowerShell to reduce the verbose and redundant syntaxis?
I am not looking for the ShowUI as this solution is too heavy considering it based on Windows Presentation Foundation (see also: WPF vs WinForms) and the fact that it concerns a PowerShell module which makes it more difficult to deploy it than a wrapper function.
In a lot of cases a wrapper in not required to make your code less verbose, take e.g. the lengthy WinForms PowerShell script here. Code pieces like this:
$System_Windows_Forms_Padding = New-Object System.Windows.Forms.Padding
$System_Windows_Forms_Padding.All = 3
$System_Windows_Forms_Padding.Bottom = 3
$System_Windows_Forms_Padding.Left = 3
$System_Windows_Forms_Padding.Right = 3
$System_Windows_Forms_Padding.Top = 3
$Tab1.Padding = $System_Windows_Forms_Padding
Can easily be simplified in WinForms to a single line:
$Tab1.Padding = 3
And if the padding would be different for each side, PowerShell will automatically convert:
$Tab1.Padding = "4, 6, 4, 6"
Note: PowerShell does not convert $Tab1.Padding = "3" or $Tab1.Padding = "4, 6"
Nevertheless, the native way to create a windows form control is far from DRY (don't repeat yourself) programming. Although (multiple) properties can be added at creation (using:New-Object System.Windows.Forms.Button -Property #{Location = "75, 120"; Size = "75, 23"}) , multiple properties can't be set right away at a later state. Above that, it isn't quick and easy to add events1, child controls and container properties (as e.g. RowSpan), or any combination, intermediately at creation of a windows form control. Bottom line, you have to reference the windows form control over and over again to set its properties and more (with e.g. $OKButton.<property> = ... as in this example) :
$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Point(75,120)
$OKButton.Size = New-Object System.Drawing.Size(75,23)
$OKButton.Text = "OK"
That's why I have created a reusable PowerShell Form Control wrapper that let's you minimize Windows Forms (WinForms) code to it's essence.
1) unless you use On<event> methods, see also: addEventListener vs onclick
PowerShell Form-Control Wrapper
Function Form-Control {
[CmdletBinding(DefaultParametersetName='Self')]param(
[Parameter(Position = 0)]$Control = "Form",
[Parameter(Position = 1)][HashTable]$Member = #{},
[Parameter(ParameterSetName = 'AttachChild', Mandatory = $false)][Windows.Forms.Control[]]$Add = #(),
[Parameter(ParameterSetName = 'AttachParent', Mandatory = $false)][HashTable]$Set = #{},
[Parameter(ParameterSetName = 'AttachParent', Mandatory = $false)][Alias("Parent")][Switch]$GetParent,
[Parameter(ParameterSetName = 'AttachParent', Mandatory = $true, ValueFromPipeline = $true)][Windows.Forms.Control]$Container
)
If ($Control -isnot [Windows.Forms.Control]) {Try {$Control = New-Object Windows.Forms.$Control} Catch {$PSCmdlet.WriteError($_)}}
$Styles = #{RowStyles = "RowStyle"; ColumnStyles = "ColumnStyle"}
ForEach ($Key in $Member.Keys) {
If ($Style = $Styles.$Key) {[Void]$Control.$Key.Clear()
For ($i = 0; $i -lt $Member.$Key.Length; $i++) {[Void]$Control.$Key.Add((New-Object Windows.Forms.$Style($Member.$Key[$i])))}
} Else {
Switch (($Control | Get-Member $Key).MemberType) {
"Property" {$Control.$Key = $Member.$Key}
"Method" {Invoke-Expression "[Void](`$Control.$Key($($Member.$Key)))"}
"Event" {Invoke-Expression "`$Control.Add_$Key(`$Member.`$Key)"}
Default {Write-Error("The $($Control.GetType().Name) control doesn't have a '$Key' member.")}
}
}
}
$Add | ForEach {$Control.Controls.Add($_)}
If ($Container) {$Container.Controls.Add($Control)}
If ($Set) {$Set.Keys | ForEach {Invoke-Expression "`$Container.Set$_(`$Control, `$Set.`$_)"}}
If ($GetParent) {$Container} Else {$Control}
}; Set-Alias Form Form-Control
Syntax
Creating a control
<System.Windows.Forms.Control> = Form-Control [-Control <String>] [-Member <HashTable>]
Modifying a control
<Void> = Form-Control [-Control <System.Windows.Forms.Control>] [-Member <HashTable>]
Adding a (new) control to a container
<System.Windows.Forms.Control> = Form-Control [-Control <String>|<System.Windows.Forms.Control>] [-Member <HashTable>] [-Add <System.Windows.Forms.Control[]>]
Piping a container to a (new) control
<System.Windows.Forms.Control> = <System.Windows.Forms.Control> | Form-Control [-Control <String>|<System.Windows.Forms.Control>] [-Member <HashTable>] [-Set <HashTable>] [-PassParent]
Parameters
-Control <String>|<System.Windows.Forms.Control> (position 0, default: Form)
The -Control parameter accepts either a Windows form control type name ([String]) or an existing form control ([System.Windows.Forms.Control] ). Windows form control type names are like Form, Label, TextBox, Button, Panel, ..., etc.
If a Windows form control type name ([String]) is supplied, the wrapper will create and return a new Windows form control with properties and settings as defined by the rest of the parameters.
If an existing Windows form control ([System.Windows.Forms.Control] ) is supplied, the wrapper will update the existing Windows form control using the properties and settings as defined by the rest of the parameters.
-Member <HashTable> (position 1)
Sets property values, invokes methods and add events on a new or existing object.
If the hash name represents property on the control, e.g. Size = "50, 50", the value will be assigned to the control property value.
If the hash name represents method on the control, e.g. Scale = {1.5, 1.5}, the control method will be invoked using the value for arguments .
If the hash name represents event on the control, take e.g. Click = {$Form.Close()}, the value ( [ScriptBlock]) will be added to the control events.
Two collection properties, ColumnStyles and RowStyles, are simplified especially for the TableLayoutPanel control which is considered a general substitute for the WPF Grid control:
- The ColumnStyles property, clears all column widths and reset them with the ColumnStyle array supplied by the hash value.
- The RowStyles property, clears all row Heigths and reset them with the RowStyle array supplied by the hash value.
Note: If want to add or insert a single specific ColumnStyle or RowStyle item, you need to fallback on the native statement, as e.g.: [Void]$Control.Control.ColumnStyles.Add((New-Object Windows.Forms.ColumnStyle("Percent", 100)).
-Add <Array>
The -Addparameter adds one or more child controls to the current control.
Note: the -add parameter cannot be used if container is piped to the control.
-Container <System.Windows.Forms.Control> (from pipeline)
The parent container is usually provided from the pipeline: $ParentContainer | Form $ChildControl and attached a (new) child control to the concerned container.
-Set <HashTable>
The -Setparameter sets (SetCellPosition, SetColumn, SetColumnSpan, SetRow, SetRowSpan and SetStyle) the specific child control properties related its parent panel container, e.g. .Set RowSpan = 2
Note: the -set column - and row parameters can only be used if a container is piped to the control.
-GetParent
By default the (child) control will be returned by the form-control function unless the -GetParent switch is supplied which will return the parent container instead.
Note: the -set column - and row parameters can only be used if a container is piped to the control.
Examples
There are two way to setup the Windows Forms hierarchy:
Adding a (new) control to a container
Piping a container to a (new) control
Adding a (new) control to a container
For this example I have reworked the Creating a Custom Input Box at learn.microsoft.com using the PowerShell Form-Control wrapper:
$TextBox = Form TextBox #{Location = "10, 40"; Size = "260, 20"}
$OKButton = Form Button #{Location = "75, 120"; Size = "75, 23"; Text = "OK"; DialogResult = "OK"}
$CancelButton = Form Button #{Location = "150, 120"; Size = "75, 23"; Text = "Cancel"; DialogResult = "Cancel"}
$Result = (Form-Control Form #{
Size = "300, 200"
Text = "Data Entry Form"
StartPosition = "CenterScreen"
KeyPreview = $True
Topmost = $True
AcceptButton = $OKButton
CancelButton = $CancelButton
} -Add (
(Form Label #{Text = "Please enter the information below:"; Location = "10, 20"; Size = "280, 20"}),
$TextBox, $OKButton, $CancelButton
)
).ShowDialog()
if ($result -eq [System.Windows.Forms.DialogResult]::OK)
{
$x = $TextBox.Text
$x
}
Note 1: Although the adding controls appears more structured especially for small forms, the drawback is that can't invoke methods that relate to both the parent container and child control (like -Set RowSpan).
Note 2: You might easily get lost in open and close parenthesis if try build child (or even grandchild) controls directly in a parent container (like the above Label control). Besides it more difficult to reference such a child (e.g. $OKButton vs. $Form.Controls["OKButton"], presuming you have set the button property Name = "OKButton)
Piping a container to a (new) control
For this example, I have created a user interface to test the dockproperty behavior. The form looks like this:
The PowerShell Form-Control code required for this:
$Form = Form-Control Form #{Text = "Dock test"; StartPosition = "CenterScreen"; Padding = 4; Activated = {$Dock[0].Select()}}
$Table = $Form | Form TableLayoutPanel #{RowCount = 2; ColumnCount = 2; ColumnStyles = ("Percent", 50), "AutoSize"; Dock = "Fill"}
$Panel = $Table | Form Panel #{Dock = "Fill"; BorderStyle = "FixedSingle"; BackColor = "Teal"} -Set #{RowSpan = 2}
$Button = $Panel | Form Button #{Location = "50, 50"; Size = "50, 50"; BackColor = "Silver"; Enabled = $False}
$Group = $Table | Form GroupBox #{Text = "Dock"; AutoSize = $True}
$Flow = $Group | Form FlowLayoutPanel #{AutoSize = $True; FlowDirection = "TopDown"; Dock = "Fill"; Padding = 4}
$Dock = "None", "Top", "Left", "Bottom", "Right", "Fill" | ForEach {
$Flow | Form RadioButton #{Text = $_; AutoSize = $True; Click = {$Button.Dock = $This.Text}}
}
$Close = $Table | Form Button #{Text = "Close"; Dock = "Bottom"; Click = {$Form.Close()}}
$Form.ShowDialog()

Unable to Name Excel Tabs when created in a function

My Goal:
Create a new tab and name it within a function.
My Issue:
When I create the new tab inside of the function I cannot name the tab.
Notes:
When I create the tab outside of the function it gets named correctly.
My Code:
Function Function_CreateWorksheets($Worksheets) {
ForEach ($Worksheet in $Worksheets) {
$Excel_Count_Worksheet++
If ($Excel_Count_Worksheet -gt 3) {$Script:Excel.Worksheets.Add() |Out-Null}
$Script:Excel.Worksheets.Item($Excel_Count_Worksheet).Name = $Worksheet
}
}
#Load-Module -Module grouppolicy -Reload
Get-Variable Excel_* |Remove-Variable -Force
#Create Excel Com Object
$Excel = New-Object -com Excel.Application
# Make the Excel Application Visible to the end user
$Excel.visible = $True
# Create a WorkBook inside the Excel application
# that we can start manipulating.
$Excel_Workbook = $Excel.Workbooks.Add()
$Action_CreateWorksheet =
#=======================================================
# Now that we have a workbook we need to create some
# additional worksheets (Tabs) beyond that initial 3
# that are created when the workbook is opened.
#=======================================================
#$Temp_MakeWorkSheet = $Excel.Worksheets.Add()
#=======================================================
# Once all the sheets are created each of the worksheets
# need to be assigned to a variable so they can be
# manipulated.
#=======================================================
Function_CreateWorksheets -Worksheets "System Summary","Test1","Test2","Test3","Test4"
The problem is that you're assuming that the new worksheet is added as the last worksheet, but by default the Add method adds the new worksheet at the beginning. The script as you have it works for up to three worksheets, but after that it adds new worksheets with default names at the beginning and keeps renaming the last worksheet. You need to add the worksheets at the end.
Change
$Script:Excel.Worksheets.Add()
to
$Script:Excel.Worksheets.Add([System.Reflection.Missing]::Value, $Script:Excel.Worksheets.Item($Script:Excel.Worksheets.Count))
How that works:
The Add method takes four arguments: Before, After, Number, and Type.
$Excel.Worksheets.Item($Excel.Worksheets.Count)) gets the object representing the last worksheet. You want to supply that as the After argument in order to add the new worksheet after the last worksheet.
[System.Reflection.Missing]::Value is a placeholder for the missing Before argument (it can't be null)
APPENDIX
As an afterthought...although it works for this specific script, I find it a little iffy to rely on the default initial configuration of three worksheets, have the function change behavior based on that assumption (rename three worksheets, then start adding more), and rely on a counter variable to determine which worksheet you're renaming rather than renaming the active sheet you just added. Since the function inherits a preexisting workbook rather than creating a new one, I think it's "cleaner" to write a function that will give you a workbook that has blank worksheets with the specified names regardless of the initial configuration.
Maybe it's just my personal philosophical inclination to write functions in ways that are more generally applicable (read: reusable) rather than ad hoc, but I prefer functions that make as few assumptions as possible. Here's how I'd do it:
function Function_CreateWorksheets {
[CmdletBinding()]
param (
[string[]] $WorkSheets,
[object] $Excel
)
for ($i = 1; $i -le $Excel.Worksheets.Count; $i++) {
$Excel.Worksheets.Item($i).Name = "DeleteMe$i"
}
foreach ($Worksheet in $Worksheets) {
$Excel.Worksheets.Add([System.Reflection.Missing]::Value,$Excel.Worksheets.Item($Excel.Worksheets.Count)) | Out-Null
$Excel.ActiveSheet.Name = $Worksheet
}
$Excel.DisplayAlerts = $false
while ($Excel.Worksheets.Count -gt $Worksheets.Count) {
$Excel.Worksheets.Item(1).Delete()
}
$Excel.DisplayAlerts = $true
}
The while loop at the end deletes all the preexisting worksheets. All new worksheets are added at the end, so the loop removes worksheets from the beginning until the number of worksheets in the workbook ($Excel.Worksheets.Count) matches the number of new worksheets ($Worksheets.Count). This needs to come at the end because a workbook has to have at least one worksheet, so you'll get an error if you try to delete all the worksheets before creating any new ones.
The initial for loop renames all the preexisting worksheets so that the function won't break if any of the new names match names that are already in use. You're going to get rid of them anyway, so you don't care what their names are.
By passing the Application object to the function as an argument, you can avoid the need to keep scoping it. You can call the function like this:
Function_CreateWorksheets -Worksheets "System Summary","Test1","Test2","Test3","Test4" -Excel $Excel
(Of course, you can leave off the parameter names -Worksheets and -Excel as long as you maintain the correct order.)
Thanks to your post I was able to get the result I needed. I did a slight variation to make the function reusable.
Function Function_CreateWorksheets {
[CmdletBinding()]
param (
[parameter(Mandatory=$true,ValueFromPipeline=$true)][object] $Excel,
[string[]] $WorkSheets
)
ForEach ($Worksheet in $Worksheets) {
$Script:Excel_Count_Worksheet++
If ($Excel_Count_Worksheet -gt $Excel.Worksheets.Count) {$Excel.Worksheets.Add([System.Reflection.Missing]::Value, $Excel.Worksheets.Item($Script:Excel.Worksheets.Count)) |Out-Null}
$Excel.Worksheets.Item($Excel_Count_Worksheet).Name = $Worksheet
}
While ($Excel.Worksheets.Count -gt $Script:Excel_Count_Worksheet) {
$Excel.Worksheets.Item($Excel.Worksheets.Count).Delete()
}
}