Marquee Progress Bar freezes in Powershell

2019-08-18 17:22发布

问题:

I have been working with Powershell for a time now, it is not the ideal programming environment, but got stuck with my program.

My program is an GUI with a marquee progressbar and a search job.

What my program does: After you run the script with Powershell it will restart Powershell in STA mode if the mode is MTA. After that it will ask for a folder location. After you entered the folder location it will start the search job and will search the location for files. Every file will be stored into an array. That array will be printed out into the tempfile.txt that will be saved on your desktop. Meanwhile the job is searching for the files the GUI will display a form with a Marquee Progress bar.

What my program needs to do: After the job is finished with searching and storing the files it must close the form.

I have tried it to do with the $formSearchingFiles.Close() command but I have noticed Jobs can not close their 'parent' thread, so this job will not be able to close the form.

I have also tried to solve it with the Wait-Job cmdlet but then the Marquee Progress bar will freeze, or the form will not show up at all.

I have looked alot around internet for solutions but I can not find one that suits this problem. I was thinking about multi-processing, but I do not know if this is possible in Powershell 2.0 (I am limited to 2.0 or lower).

I also do not know if there is a way that the Search-Job can notify the main thread that it is finished with the task, so that the main thread can continue with the program, without freezing the progress bar.

I hope I have explained enough about the program and my problem.

# Get the path of the script
$scriptPath = ((Split-Path $script:MyInvocation.MyCommand.Path) + "\")
$scriptName = $MyInvocation.MyCommand.Name
$script = $scriptPath + $scriptName

# Check if powershell is running in STA(Single Threaded Apartment) or MTA(Multi Threaded Apartment) mode.
# If it is running in MTA mode then restart Powershell in STA mode.
if ([threading.thread]::CurrentThread.GetApartmentState() -eq "MTA") 
{
    Write-Host Restarting Powershell in STA mode
    & $env:SystemRoot\system32\WindowsPowerShell\v1.0\powershell.exe -sta "& {&'$script'}"
}
else
{
    $folderPath = $currentFolderLocation.Text
    $tempFile = $currentStagingLocation.Text
    $tempFile += "\fileArray.txt"

    function OnApplicationLoad {    
        return $true #return true for success or false for failure
    }

    function OnApplicationExit {
        $script:ExitCode = 0 #Set the exit code for the Packager
    }

    function Call-Searching_pff {
        [void][reflection.assembly]::Load("mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
        [void][reflection.assembly]::Load("System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
        [void][reflection.assembly]::Load("System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
        [void][reflection.assembly]::Load("System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
        [void][reflection.assembly]::Load("System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
        [void][reflection.assembly]::Load("System.Xml, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
        [void][reflection.assembly]::Load("System.DirectoryServices, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
        [void][reflection.assembly]::Load("System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")
        [void][reflection.assembly]::Load("System.ServiceProcess, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")

        [System.Windows.Forms.Application]::EnableVisualStyles()
        $formSearchingFiles = New-Object 'System.Windows.Forms.Form'
        $label = New-Object 'System.Windows.Forms.Label'
        $progressbar = New-Object 'System.Windows.Forms.ProgressBar'
        $InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'

        $FormEvent_Load={

        $folderPath = &read-host "Enter path"
        $tempFile = (([Environment]::GetFolderPath("Desktop")) + "\tempfile.txt" )

            $SearchJob = Start-Job -scriptblock {
                param ($folderPath, $tempFile)
                $fileArray = @()
                # Get all files and folders under the specified path
                $items = Get-ChildItem -Path $folderPath -Recurse
                foreach ($item in $items)
                {
                    # Check if the item is a file or a folder
                    if (!($item.PSIsContainer)) 
                    {
                        # Extract path of file with path of entered folder
                        $extractedPath = $item.FullName
                        $extractedPath = $extractedPath.Replace($folderPath, "")
                        $fileArray += $extractedPath
                    }
                }
                # Save array in temporary file
                $fileArray | out-file $tempFile
                $formSearchingFiles.Close() #Does not work inside job :(

            } -ArgumentList @($folderPath, $tempFile)       
        }

        $Form_StateCorrection_Load=
        {
            #Correct the initial state of the form to prevent the .Net maximized form issue
            $formSearchingFiles.WindowState = $InitialFormWindowState
        }

        $Form_Cleanup_FormClosed=
        {
            #Remove all event handlers from the controls
            try
            {
                $formSearchingFiles.remove_Load($FormEvent_Load)
                $formSearchingFiles.remove_Load($Form_StateCorrection_Load)
                $formSearchingFiles.remove_FormClosed($Form_Cleanup_FormClosed)
            }
            catch [Exception]{ }
        }

        # formSearchingFiles
        $formSearchingFiles.Controls.Add($label)
        $formSearchingFiles.Controls.Add($progressbar)
        $formSearchingFiles.ClientSize = '394, 122'
        $formSearchingFiles.FormBorderStyle = 'FixedDialog'
        $formSearchingFiles.MaximizeBox = $False
        $formSearchingFiles.Name = "formSearchingFiles"
        $formSearchingFiles.StartPosition = 'CenterScreen'
        $formSearchingFiles.Text = "Compatibility Checker"
        $formSearchingFiles.add_Load($FormEvent_Load)

        # label
        $label.Location = '12, 27'
        $label.Name = "label"
        $label.Size = '368, 26'
        $label.TabIndex = 1
        $label.Text = "Searching for files, please wait.."
        $label.TextAlign = 'MiddleCenter'

        # progressbar
        $progressbar.Location = '12, 68'
        $progressbar.MarqueeAnimationSpeed = 40
        $progressbar.Name = "progressbar"
        $progressbar.Size = '370, 30'
        $progressbar.Style = 'Marquee'
        $progressbar.TabIndex = 0

        #Save the initial state of the form
        $InitialFormWindowState = $formSearchingFiles.WindowState
        #Init the OnLoad event to correct the initial state of the form
        $formSearchingFiles.add_Load($Form_StateCorrection_Load)
        #Clean up the control events
        $formSearchingFiles.add_FormClosed($Form_Cleanup_FormClosed)
        #Show the Form
        return $formSearchingFiles.ShowDialog()
    } #End Function

    #Call OnApplicationLoad to initialize
    if((OnApplicationLoad) -eq $true)
    {
        #Call the form
        Call-Searching_pff | Out-Null
        #Perform cleanup
        OnApplicationExit
    }
}

回答1:

I've found the solution for my own problem. Solution: synchronized hash table as "communication link" between threads.

After you created a hash table you can add variables and objects to it. All threads (that you give access to the hash) can read/write to those variables and objects.

Create sync. hash table:

$syncHash = [hashtable]::Synchronized(@{})
#Where $syncHash is the name of your hash table

Add Variables and Objects to the hash table:

$syncHash.ProgressBar = $progressBar
#Create new variable ProgressBar in hash table and assign $progressBar to it

Create new thread and allow the use of the hash table:

$processRunspace =[runspacefactory]::CreateRunspace()
$processRunspace.ApartmentState = "STA"
$processRunspace.ThreadOptions = "ReuseThread"          
$processRunspace.Open()
$processRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)   

$psCmd = [PowerShell]::Create().AddScript({
     #Your Thread Code Here
})
$psCmd.Runspace = $processRunspace
$data = $psCmd.BeginInvoke() 

Change value of $progressBar from new thread:

$syncHash.ProgressBar.Value = 1

Thanks to: http://learn-powershell.net/2012/10/14/powershell-and-wpf-writing-data-to-a-ui-from-a-different-runspace/