I have a powershell script that I am developing to create new users in the organisation. Version 1 of the script worked well but was freezing too much when the script is running. WIth the next iteration, I have started developing using runspaces to reduce the unresponsiveness. The script should just get fields from the operator and then use the information to create the user. There are 2 functions that occurs when the Create button is clicked:
- Check the form that all the fields have been filled in correctly (Test-FormField);
- If correct, then run the New-User function
This is the code that I have so far:
$Global:syncHash = [hashtable]::Synchronized(@{})
# Add custom properties to the sync hash table
$syncHash.OfficeNameCollection = $UserDataFile.office.name
$syncHash.ClassificationCollection = $UserDataFile.class.classification
$syncHash.BUDepartmentsCollection = ($UserDataFile.BU.Department|Sort-Object)
$syncHash.EmploymentTypeCollection = $UserDataFile.EmploymentType.type
# Create runspace
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState ="STA"
$newRunspace.ThreadOptions ="ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
# Load WPF assembly if necessary
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
$psCmd = [PowerShell]::Create().AddScript({
[xml]$xaml = @"
<Window x:Name="CreateUserForm" x:Class="Add_User.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Add_User"
mc:Ignorable="d"
Title="Create User Form" ResizeMode="NoResize">
<Grid Margin="0,0,0,80">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="225*"/>
<ColumnDefinition Width="29*"/>
</Grid.ColumnDefinitions>
<Label x:Name="CreateUserLabel" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="313.56" Height="47" Content="Enter the required fields.
Then click the 'Create User' button to create the user" FontWeight="Bold"/>
<TextBox x:Name="FirstName" HorizontalAlignment="Left" Height="22" Margin="10,63,0,0" TextWrapping="Wrap" Text="First Name" VerticalAlignment="Top" Width="222" TabIndex="0"/>
<Button x:Name="Exit" Content="EXIT" HorizontalAlignment="Left" Margin="424,36,0,0" VerticalAlignment="Top" Width="132" Height="39" Background="#FFE44F4F" FontSize="18" IsCancel="True" TabIndex="13"/>
<Button x:Name="Create" Content="Create User" HorizontalAlignment="Left" Margin="603,36,0,0" VerticalAlignment="Top" Width="127" Height="39" Background="#FF62CD51" FontSize="18" Grid.ColumnSpan="2" TabIndex="12"/>
<TextBox x:Name="OutputBox" HorizontalAlignment="Left" Height="242" Margin="370,80,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="382" VerticalScrollBarVisibility="Auto" SpellCheck.IsEnabled="True" Grid.ColumnSpan="2"/>
<TextBlock x:Name="FirstNameLabel" HorizontalAlignment="Left" Margin="260,67,0,0" TextWrapping="Wrap" Text="Required" VerticalAlignment="Top" Height="22"/>
<TextBox x:Name="LastName" HorizontalAlignment="Left" Height="22" Margin="10,90,0,0" TextWrapping="Wrap" Text="Last Name" VerticalAlignment="Top" Width="222" TabIndex="1"/>
<TextBlock x:Name="LastNameLabel" HorizontalAlignment="Left" Margin="260,94,0,0" TextWrapping="Wrap" Text="Required" VerticalAlignment="Top" Height="22"/>
<TextBox x:Name="Position" HorizontalAlignment="Left" Height="22" Margin="10,117,0,0" TextWrapping="Wrap" Text="Position" VerticalAlignment="Top" Width="222" TabIndex="2"/>
<TextBlock x:Name="PositionLabel" HorizontalAlignment="Left" Margin="261,118,0,0" TextWrapping="Wrap" Text="Required" VerticalAlignment="Top" Height="22"/>
<ComboBox x:Name="EmploymentType" HorizontalAlignment="Left" Margin="10,144,0,0" VerticalAlignment="Top" Width="222" Text="Employment Type" TabIndex="4"/>
<TextBlock x:Name="EmploymentTypeLabel" HorizontalAlignment="Left" Margin="260,145,0,0" TextWrapping="Wrap" Text="Employment Type" VerticalAlignment="Top" Height="22"/>
<ComboBox x:Name="Classification" HorizontalAlignment="Left" Margin="10,171,0,0" VerticalAlignment="Top" Width="222" Text="Classification" TabIndex="5"/>
<TextBox x:Name="PhoneNumber" HorizontalAlignment="Left" Height="22" Margin="10,198,0,0" TextWrapping="Wrap" Text="Phone Number" VerticalAlignment="Top" Width="222" TabIndex="6"/>
<ComboBox x:Name="Office" HorizontalAlignment="Left" Margin="10,225,0,0" VerticalAlignment="Top" Width="222" Text="Office" TabIndex="7"/>
<ComboBox x:Name="Department" HorizontalAlignment="Left" Margin="10,252,0,0" VerticalAlignment="Top" Width="222" Text="Department" TabIndex="8"/>
<TextBox x:Name="CompanyName" HorizontalAlignment="Left" Height="22" Margin="10,279,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="222" TabIndex="9"/>
<TextBlock x:Name="ClassificationLabel" HorizontalAlignment="Left" Margin="261,172,0,0" TextWrapping="Wrap" Text="Classification" VerticalAlignment="Top" Height="22" Width="76"/>
<TextBlock x:Name="PhoneNumberLabel" HorizontalAlignment="Left" Margin="260,199,0,0" TextWrapping="Wrap" Text="Required" VerticalAlignment="Top" Height="22"/>
<TextBlock x:Name="OfficeLabel" HorizontalAlignment="Left" Margin="260,225,0,0" TextWrapping="Wrap" Text="Office" VerticalAlignment="Top" Height="22"/>
<TextBlock x:Name="DepartmentLabel" HorizontalAlignment="Left" Margin="260,252,0,0" TextWrapping="Wrap" Text="Department" VerticalAlignment="Top" Height="22"/>
<TextBlock x:Name="CompanyNameLabel" HorizontalAlignment="Left" Margin="260,279,0,0" TextWrapping="Wrap" Text="Optional" VerticalAlignment="Top" Height="22" RenderTransformOrigin="0.5,1.5"/>
<TextBox x:Name="Password" HorizontalAlignment="Left" Height="22" Margin="10,326,0,0" TextWrapping="Wrap" Text="Random Password" VerticalAlignment="Top" Width="222" TabIndex="10"/>
<TextBlock x:Name="PasswordLabel" HorizontalAlignment="Left" Margin="261,326,0,0" TextWrapping="Wrap" Text="Leave if Random Password, else clear for default" VerticalAlignment="Top" Height="22"/>
<TextBox x:Name="SecurityCard" HorizontalAlignment="Left" Height="22" Margin="10,349,0,0" TextWrapping="Wrap" Text="Security Card Number" VerticalAlignment="Top" Width="222" TabIndex="11"/>
<TextBlock x:Name="SecurityCardLabel" HorizontalAlignment="Left" Margin="261,349,0,0" TextWrapping="Wrap" Text="Optional" VerticalAlignment="Top" Height="22"/>
</Grid>
</Window>
"@
# Remove XML attributes that break a couple things.
# Without this, you must manually remove the attributes
# after pasting from Visual Studio. If more attributes
# need to be removed automatically, add them below.
$AttributesToRemove = @(
'x:Class',
'mc:Ignorable'
)
foreach ($Attrib in $AttributesToRemove) {
if ( $xaml.Window.GetAttribute($Attrib) ) {
$xaml.Window.RemoveAttribute($Attrib)
}
}
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$syncHash.Window=[Windows.Markup.XamlReader]::Load( $reader )
[xml]$XAML = $xaml
$xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object{
# Find all of the form types and add them as members to the synchash
$syncHash.Add($_.Name,$syncHash.Window.FindName($_.Name) )
}
$Script:JobCleanup = [hashtable]::Synchronized(@{})
$Script:Jobs = [system.collections.arraylist]::Synchronized((New-Object System.Collections.ArrayList))
# region Background runspace to clean up jobs
$jobCleanup.Flag = $True
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("jobCleanup",$jobCleanup)
$newRunspace.SessionStateProxy.SetVariable("jobs",$jobs)
$jobCleanup.PowerShell = [PowerShell]::Create().AddScript({
# Routine to handle completed runspaces
Do {
Foreach($runspace in $jobs) {
If ($runspace.Runspace.isCompleted) {
[void]$runspace.powershell.EndInvoke($runspace.Runspace)
$runspace.powershell.dispose()
$runspace.Runspace = $null
$runspace.powershell = $null
}
}
#Clean out unused runspace jobs
$temphash = $jobs.clone()
$temphash | Where-Object {$_.runspace -eq $Null} | ForEach-Object {$jobs.remove($_)}
Start-Sleep -Seconds 1
} while ($jobCleanup.Flag)
})
$jobCleanup.PowerShell.Runspace = $newRunspace
$jobCleanup.Thread = $jobCleanup.PowerShell.BeginInvoke()
# endregion Background runspace to clean up jobs
$syncHash.Create.Add_Click({
#region Boe's Additions
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("SyncHash",$SyncHash)
$PowerShell = [PowerShell]::Create().AddScript({
Function Update-Window {
Param (
$Control,
$Property,
$Value,
[switch]$AppendContent
)
# This is kind of a hack, there may be a better way to do this
If ($Property -eq "Close") {
$syncHash.Window.Dispatcher.invoke([action]{$syncHash.Window.Close()},"Normal")
Return
}
# This updates the control based on the parameters passed to the function
$syncHash.$Control.Dispatcher.Invoke([action]{
# This bit is only really meaningful for the TextBox control, which might be useful for logging progress steps
If ($PSBoundParameters['AppendContent']) {
$syncHash.$Control.AppendText($Value)
} Else {
$syncHash.$Control.$Property = $Value
}
}, "Normal")
}
Function Convert-FormFieldValues() {
[CmdletBinding()]
Param(
[Parameter(Mandatory)][string] $Control,
[string] $Property,
[switch]$Length,
[switch]$Selected
)
$syncHash.Window.Dispatcher.Invoke([System.Func[String]]{
If ($PSBoundParameters['Length']) {
$syncHash.$Control.$Property.Length
} elseif ($PSBoundParameters['Selected']) {
$syncHash.$Control.$Property.SelectedItem
} Else {
$syncHash.$Control.$Property
}
}, "Normal")
}
$FormValid = Test-FormField
Write-Debug $FormValid
if ($FormValid -eq "True") {
New-User
}
})
$PowerShell.Runspace = $newRunspace
[void]$Jobs.Add((
[pscustomobject]@{
PowerShell = $PowerShell
Runspace = $PowerShell.BeginInvoke()
}
))
})
# region Window Close
$syncHash.Window.Add_Closed({
Write-Verbose 'Halt runspace cleanup job processing'
$jobCleanup.Flag = $False
#Stop all runspaces
$jobCleanup.PowerShell.Dispose()
})
# endregion Window Close
# endregion Boe's Additions
# GUI update script block (this code will run for each tick in the timer below)
$GUIUpdateBlock = {
$syncHash.Office.ItemsSource = $syncHash.OfficeNameCollection
$syncHash.Classification.ItemsSource = $syncHash.ClassificationCollection
$syncHash.Department.ItemsSource = $syncHash.BUDepartmentsCollection
$syncHash.EmploymentType.ItemsSource = $syncHash.EmploymentTypeCollection
}
# Before displaying the GUI, create a DispatcherTimer running the GUI update block
$Global:DispatcherTimer = New-Object -TypeName System.Windows.Threading.DispatcherTimer
# Run 4 times every second
$DispatcherTimer.Interval = [TimeSpan]"0:0:0.01"
# Invoke the GUIUpdateBlock script block
$DispatcherTimer.Add_Tick($GUIUpdateBlock)
# Start the DispatcherTimer
$DispatcherTimer.Start()
$syncHash.Window.ShowDialog() | Out-Null
$syncHash.Error = $Error
})
$psCmd.Runspace = $newRunspace
$psCmd.BeginInvoke()
The Test-form function has the following code:
function Test-FormField {
$Msgbox = $Null
$IsValid = $True
if ((Convert-FormFieldValues -Control FirstName -Property Text -Length) -eq 0){
$Msgbox = $Msgbox + "The first name cannot be empty. `r`n"
$IsValid = $False
}
Update-Window -Control OutputBox -Property Text -Value $Msgbox
Return $IsValid
}
Any suggestions where I am going wrong? Thanks for the assistance