0

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:

  1. Check the form that all the fields have been filled in correctly (Test-FormField);
  2. 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.&#xD;&#xA;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

ppaniker
  • 1
  • 1
  • I suggest simplifying your approach as described in [this answer](https://stackoverflow.com/a/65531439/45375), which shows the window _non_-modally and then enters a single event-processing loop in which background thread jobs / runspaces can be monitored, without the need for synchronization code. – mklement0 Jul 16 '21 at 15:11

0 Answers0