call OpenFileDialog from powershell - winforms

When I run the following, PowerShell hangs waiting for the dialog to close, even though the dialog is never displayed:
[void] [Reflection.Assembly]::LoadWithPartialName( 'System.Windows.Forms' )
$d = New-Object Windows.Forms.OpenFileDialog
$d.ShowDialog( )
Calling ShowDialog on a Windows.Forms.Form works fine. I also tried creating a Form and passing it as the parent to $d.ShowDialog, but the result was no different.

I was able to duplicate your problem and found a workaround. I don't know why this happens, but it has happened to others.
If you set the ShowHelp property to $true, you will get the dialog to come up properly.
Example:
[void] [Reflection.Assembly]::LoadWithPartialName( 'System.Windows.Forms' )
$d = New-Object Windows.Forms.OpenFileDialog
$d.ShowHelp = $true
$d.ShowDialog( )
Good Luck!

It appears to me that the dialog is actually opening just fine, but it's behind the powershell console window. Unfortunately it doesn't show in the taskbar, so there's no indication that it's there unless you move the powershell window or Alt+Tab. It also appears that the ShowHelp workaround didn't have any effect for me.
EDIT Here's a way to do do it using your secondary-form idea. The basic idea is to create a new form which opens the OpenFileDialog from inside its Shown event. The key is calling Activate on the form before opening the dialog, so that the form comes to the front and the dialog appears. I moved the form offscreen by setting the Location to an offscreen value, but you could alternatively set Form.Visible = $false from inside the Shown event.
[void] [Reflection.Assembly]::LoadWithPartialName( 'System.Windows.Forms' )
$ofn = New-Object System.Windows.Forms.OpenFileDialog
$outer = New-Object System.Windows.Forms.Form
$outer.StartPosition = [Windows.Forms.FormStartPosition] "Manual"
$outer.Location = New-Object System.Drawing.Point -100, -100
$outer.Size = New-Object System.Drawing.Size 10, 10
$outer.add_Shown( {
$outer.Activate();
$ofn.ShowDialog( $outer );
$outer.Close();
} )
$outer.ShowDialog()

Apparently this has something to do with Multi-Threaded Apartment (MTA) mode.
It appears to work fine in Single-Threaded Apartment (-STA) mode.
See also: Could you explain STA and MTA?

Related

How would I open a manpage in GridView by clicking on a WinForms button?

I am trying to open up a manpage (Get-Help alias) when I click on a button using Powershell and WinForms.
I have a text box that allows you to input a cmdlet or help topic and when you press a button, it should open up the manpage documentation in GridView. Currently, it opens the GridView and grabs the correct help docs but something messes up along the way and I think it has to do with interpretation before it is passed off to GridView.
Here is what I have:
[void][Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
$form = New-Object Windows.Forms.Form
$input = New-Object Windows.Forms.TextBox
$input.Size = '100,20'
$input.Location = '10,20'
$button = New-Object Windows.Forms.Button
$button.Size = '100,20'
$button.Location = '10,60'
$button.Add_Click({
Invoke-Expression ("man " + ($global:input.Text)) | Out-GridView
})
$form.Controls.AddRange(#($input, $button)
$form.Add_Shown({$form.Activate()})
$form.ShowDialog()
What happens is the GridView opens but the title shows Invoke-Expression ("man " + ($input.Text)) | Out-GridView and the contents are the generic default information for manpages.
I tried to attach the Invoke-Expression to a variable and then pipe the variable out to GridView. I tried to set (Get-Help ($input.Text)) to a variable and then pipe it to GridView. I even tried to initialize the $input.Text property by putting $input.Text = '' just after the $input.Location property.
I really think its how the Powershell engine is interpreting the expression but I don't know how to tell it to work the way I am wanting it to.
What am I doing wrong here?
EDIT: Ok, I just realized something. I think the $input.Text property is not getting populated correctly.
What I did was added [Windows.Forms.MessageBox]::Show($input.Text) in the Click event for $button and commented out the Invoke-Expression. What it should do is open a message box and place within it what is typed in the text box ($input.Text). The message box is blank. I'm thinking that it may have to do with scoping but the $input.Text should be $global and accessible from within the Click event on the button control item.
I messed around with it after typing that last paragraph and I realized that the $input.Text property is populated correctly and is accessible in the $global scope. What I did was add [Windows.Forms.MessageBox]::Show($input.Text) at the very end of the script (after $form.ShowDialog()) and it showed exactly what I typed in the text box.
So, why is it that I can't see the $input text box properties? I haven't had this issue with some of the other WinForms apps I have built in Powershell.
Thanks for the insight.
$input is an automatic variable used in the context of the Powershell pipeline, which is why it was empty. Powershell populates it by itself, therefore overwriting anything you put in it. Rename $input to any other available name and it should work.
E.g.:
[void][Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
$form = New-Object Windows.Forms.Form
$box = New-Object Windows.Forms.TextBox
$box.Size = '100,20'
$box.Location = '10,20'
$button = New-Object Windows.Forms.Button
$button.Size = '100,20'
$button.Location = '10,60'
$button.Add_Click({
[Windows.Forms.MessageBox]::Show($box.Text)
Invoke-Expression ("man " + ($box.Text)) | Out-GridView
})
$form.Controls.AddRange(#($box, $button))
$form.Add_Shown({$form.Activate()})
$form.ShowDialog()

Using Windows Forms Locks up PowerShell ISE minutes after script has terminated

I have an interesting issue here. I'm creating a calendar picker for use when we create accounts. It works fine and is still in progress but I have noticed that when I run the script in powershell ISE, after a few minutes it locks up (I am able to edit and save the code for a few minutes prior to that). There is nothing in the event log. I get a dialog box saying that powershell is non responsive. Memory usage seems normal as well. I do not know what is happening.
This occurs no matter how I run Powershell ISE (Run as Administrator, Run as another account, and normal ISE) I am running windows 8.1.
A coworker suggested it may be the apartment model, so I've tried STA and MTA, but the problem occurs either way. It does not happen when the same code is run from the console host.
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
$objForm = New-Object Windows.Forms.Form
$objForm.Text = "Select a Date"
$objForm.Size = New-Object Drawing.Size #(490,250)
$objForm.StartPosition = "CenterScreen"
$objForm.KeyPreview = $True
$objForm.Add_KeyDown({
if ($_.KeyCode -eq "Enter")
{
$script:dtmDate=$objCalendar.SelectionStart
$objForm.Close()
}
})
$objForm.Add_KeyDown({
if ($_.KeyCode -eq "Escape")
{
$objForm.Close()
}
})
$objCalendar = New-Object System.Windows.Forms.MonthCalendar
$objCalendar.Text = "Start"
$objCalendar.ShowTodayCircle = $False
$objCalendar.MaxSelectionCount = 1
$objForm.Controls.Add($objCalendar)
$objForm.Topmost = $True
$objForm.Add_Shown({$objForm.Activate()})
[void] $objForm.ShowDialog()
if ($dtmDate)
{
Write-Host "Date selected: $dtmDate"
}
$objForm.Dispose()
In Response to #The Unique Paul Smith
function Find-CalenderDateTest {
[CmdletBinding()]
param(
[Parameter(
Mandatory=$false
)]
[ValidateSet('long','short','powerpoint')]
[ValidateNotNullOrEmpty()]
[string]
$DateFormat
)
Begin{
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
$objForm = New-Object Windows.Forms.Form
$objForm.Text = "Select a Date"
$objForm.Size = New-Object Drawing.Size #(243,250)
$objForm.StartPosition = "CenterScreen"
$objForm.KeyPreview = $True
$dtmDate = $null
$objForm.Add_KeyDown( {
if ($_.KeyCode -eq "Enter")
{
$dtmDate=$objCalendar.SelectionStart
$objForm.Close()
}
})
$objForm.Add_KeyDown({
if ($_.KeyCode -eq "Escape")
{
$objForm.Close()
}
})
#region OK Button
$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Size(20,175)
$OKButton.Size = New-Object System.Drawing.Size(75,23)
$OKButton.Text = "OK"
# Got rid of the Click event for OK Button, and instead just assigned its DialogResult property to OK.
$OKButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
$objForm.Controls.Add($OKButton)
# Setting the form's AcceptButton property causes it to automatically intercept the Enter keystroke and
# treat it as clicking the OK button (without having to write your own KeyDown events).
$objForm.AcceptButton = $OKButton
#endregion
#region Cancel Button
$CancelButton = New-Object System.Windows.Forms.Button
$CancelButton.Location = New-Object System.Drawing.Size(80,175)
$CancelButton.Size = New-Object System.Drawing.Size(75,23)
$CancelButton.Text = "Cancel"
# Got rid of the Click event for Cancel Button, and instead just assigned its DialogResult property to Cancel.
$CancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$objForm.Controls.Add($CancelButton)
# Setting the form's CancelButton property causes it to automatically intercept the Escape keystroke and
# treat it as clicking the OK button (without having to write your own KeyDown events).
$objForm.CancelButton = $CancelButton
#endregion
$objCalendar = New-Object System.Windows.Forms.MonthCalendar
$objCalendar.ShowTodayCircle = $False
$objCalendar.MaxSelectionCount = 1
$objForm.Controls.Add($objCalendar)
$objForm.Topmost = $True
$objForm.Add_Shown({$objForm.Activate()})
$Results = $objForm.ShowDialog()
}
Process{}
End{
if ($Results -eq "OK")
{
$objCalendar.SelectionStart
}
$objForm.Dispose()
}
}
The error is MTA/STA
Don't use
$form.showDialog()
Use
[system.windows.forms.application]::run($form)
instead
and it works fine every time
Another way is to put it in another thread:
$code
{
//form code here
$form.showDialog()
}
$newThread = [Powershell]::Create()
$newThread.AddScript($code)
$handle = $newThread.BeginInvoke()
Provide variables from the calling script:
$newThread.Runspace.SessionStateProxy.SetVariable("variablenname",value)
before the BeginInvoke use variablenname without $...
It's a long shot but the problem might be that powershell is not closing the $objForm object correctly, leaving it running in memory while the ISE waits for input after the script has terminated. If you check your taskmanager, is the form still running in the background? You could also try adding 'Remove-Variable objForm' (no $) after the dispose() and see if that helps.
More information: https://technet.microsoft.com/en-us/library/ff730962.aspx
As I say, it's a long shot.
I was using combobox.items.add:
$configCombo.Items.Add($wks)
and I looked up how to keep the keys from printing to the console - and changed the add to:
[void]$configCombo.Items.Add($wks)
Since then I have added the void - I have been running it in ISE and it has not hung since.
Ran into this issue too. Generally occurs when I lock my workstation and return. After a bit of poking about and googleing, I found this
https://support.microsoft.com/en-us/help/943139/windows-forms-application-freezes-when-system-settings-are-changed-or, which seems like the issue at hand.
Issue
The application will not respond and the UI thread will hang in an
Invoke call while handling the OnUserPreferenceChanged notification
Cause
This occurs if a control is created on a thread which doesn't pump
messages and the UI thread receives a WM_SETTINGCHANGE message.
Resolution
Applications should never leave Control objects on threads without an
active message pump. If Controls cannot be created on the main UI
thread, they should be created on a dedicated secondary UI thread and
Disposed as soon as they are no longer needed.
I had the same issue, but the solution is: Always clean up right after the form is done.
In this case:
$objForm.Dispose()
Up to now (a few hours) I didn't have that issue again. Previously it locked up after > 10 Minutes.

Use of CheckedChanged using Windows Forms and Powershell assistance

I'm using WinForms with PowerShell. In my tool, I'd like a checkbox that when checked, will display a message next to it, and when unchecked, it will remove the message.
I've gotten this far (I'm sure there's a much better way to do it). This makes the message pop-up, but it doesn't go away when you uncheck the box. Any help would be appreciated. Thanks!
$Checkbox_Errors.Add_CheckStateChanged({ ### Checkbox_Errors is the name of the checkbox
if ($Checkbox_Errors.Checked -eq $true)
{
$ErrorWarning1 = New-Object System.Windows.Forms.Label
$ErrorWarning1.Text = "WARNING: May take 3-5 Minutes" ### When checked, this is what it should display
$ErrorWarning1.ForeColor = "Red"
$ErrorWarning1.AutoSize = $True
$ErrorWarning1.Location = new-object System.Drawing.Point(170,13)
$groupbox.Controls.Add($ErrorWarning1)
}
})
$Checkbox_Errors.Add_CheckStateChanged({
if ($Checkbox_Errors.Unchecked -eq $true)
{
$ErrorWarning1 = New-Object System.Windows.Forms.Label
$ErrorWarning1.Text = "" ### I attempted this, where it would re-write
$ErrorWarning1.ForeColor = "Red"
$ErrorWarning1.AutoSize = $True
$ErrorWarning1.Location = new-object System.Drawing.Point(170,13)
$groupbox.Controls.Add($ErrorWarning1)
}
})
It would be simpler to create the label with the rest of your form elements. If you're using a designer, you can just drag it on with the rest of the controls. Then set the label's Visible property to $False initially to hide it.
$ErrorWarning1 = New-Object System.Windows.Forms.Label
$ErrorWarning1.Text = "WARNING: May take 3-5 Minutes"
$ErrorWarning1.ForeColor = "Red"
$ErrorWarning1.AutoSize = $True
$ErrorWarning1.Location = new-object System.Drawing.Point(170,13)
$ErrorWarning1.Visible = $False # This line hides the label initially
$groupbox.Controls.Add($ErrorWarning1)
Now in your event handler, instead of generating the label, just show or hide it based on the state of the checkbox:
$ErrorWarning1.Visible = $Checkbox_Errors.Checked
The label will always exist, but it will only be visible when the checkbox is checked.

Need help regarding creating dynamic forms in powershell

Hi i am trying to create a dynamic form in pwoershell , this is a form which has 5 buttons(color names) and each button opens a different text file(such as if red button is clicked,it should open red.txt; here is the full code;
Script Start
$var = "Red","Blue","Yellow","Black","White"
$testForm = New-Object System.Windows.Forms.Form
$testForm.Text = "Color List"
$testForm.AutoSize = $True
$testForm.AutoSizeMode = "GrowAndShrink"
$Font = New-Object System.Drawing.Font("Times New Roman",24, [System.Drawing.FontStyle]::Bold)
$testForm.Font = $Font
$Label = New-Object System.Windows.Forms.Label
$Label.Text = "Select the Color"
$Label.AutoSize = $True
$testForm.Controls.Add($Label)
$x=100
$y=50
foreach($color in $var)
{
$run = New-Object System.Windows.Forms.Button
$run.Location = New-Object System.Drawing.Size($x,$y)
$run.Size = New-Object System.Drawing.Size(100,50)
$run.Text = "$Color"
$run.Add_Click({ Invoke-Expression "notepad C:\Users\User\$color.txt" })
$testForm.Controls.Add($run)
$Font = New-Object System.Drawing.Font("Times New Roman",14,[System.Drawing.FontStyle]::Regular)
$run.font = $Font
$run.AutoSize = $True
$y+=50
}
$testForm.ShowDialog()
END Script
Everything went fine until, when the form opens with buttons, and all the buttons when clicked, open the file "White.txt" since its the last element in the array; is there any way to change the script to make each button open only their respective files and not the last color file?
please do let me know if any further questions or clarifications needed.
Looks like the problem is the Add_Click line. The script block contains a link to the variable $color, rather than evaluating it immediately to create a new "notepad..." string for Invoke-Expression and linking to that. The string for Invoke-Expression will be created when the button is clicked. By this time $color is White 'cos the loop has finished, so all the buttons end up creating a string using White.
You can fix it with a call to GetNewClosure() which will build cause the string for the Invoke-Expression to be created during the loop, not later on when the button is clicked. So change the line to:
$run.Add_Click({ Invoke-Expression "notepad C:\Users\User\$color.txt" }.GetNewClosure())
And it should work as expected.

PowerShell: Job Event Action with Form not executed

If I run the following code, the Event Action is executed:
$Job = Start-Job {'abc'}
Register-ObjectEvent -InputObject $Job -EventName StateChanged `
-Action {
Start-Sleep -Seconds 1
Write-Host '*Event-Action*'
}
The string 'Event-Action' is displayed.
If I use a Form and start the above code by clicking a button,
the Event Action is not executed:
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Form1 = New-Object Windows.Forms.Form
$Form1.Add_Shown({
$Form1.Activate()
})
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Text = 'Test'
$Form1.Controls.Add($Button1)
$Button1.Add_Click({
Write-Host 'Test-Button was clicked'
$Job = Start-Job {'abc'}
Register-ObjectEvent -InputObject $Job -EventName StateChanged `
-Action {
Start-Sleep -Seconds 1
Write-Host '*Event-Action*'
}
})
$Form1.ShowDialog()
Only when I click the button again, the first Event Action is executed.
With the third click the second Event Action is executed and so on.
If I do multiple clicks in rapid succession, the result is unpredictable.
Furthermore when I close the form with the button in the upper right corner,
the last "open" Event Action is executed.
Note: For testing PowerShell ISE is to be preferred, because PS Console displays
the string only under certain circumstances.
Can someone please give me a clue what's going on here?
Thanks in advance!
nimizen.
Thanks for your explanation, but I don't really understand, why the StateChanged event is not fired or visible to the main script until there is some action with the Form. I'd appreciate another attempt to explain it to me.
What I want to accomplish is a kind of multithreading with PowerShell and Forms.
My plan is the following:
'
The script shows a Form to the user.
The user does some input and clicks a button.
Based on the user's input a set of Jobs are started with Start-Job and a StateChanged event is registered for each job.
While the Jobs are running, the user can perform any action on the Form (including stop the Jobs via a button) and the Form is repainted when necessary.
The script reacts to any events which are fired by the Form or its child controls.
Also the script reacts to each job's StateChanged event.
When a StateChanged event occurs, the state of each job is inspected, and if all jobs have the state 'Completed', the jobs' results are fetched with Receive-Job and displayed to the user.
'
All this works fine except that the StateChanged event is not visible to the main script.
The above is still my favorite solution and if you have any idea how to implement this, please let me know.
Otherwise I'll most likely resort to a workaround, which at least gives the user a multithreading feeling. It is illustrated in the following example:
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Form1 = New-Object Windows.Forms.Form
$Form1.Add_Shown({
$Form1.Activate()
})
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Text = 'Test'
$Form1.Controls.Add($Button1)
$Button1.Add_Click({
$Form1.Focus()
Write-Host 'Test-Button was clicked'
$Job = Start-Job {Start-Sleep -Seconds 1; 'abc'}
Do {
Start-Sleep -Milliseconds 100
Write-Host 'JobState: ' $Job.State
[System.Windows.Forms.Application]::DoEvents()
}
Until ($Job.State -eq 'Completed')
Write-Host '*Action*'
})
$Form1.ShowDialog()
There are a lot of (StackOverflow) questions and answers about this ‘enduring mystique’ of combining form (or WPF) events with .NET events (like EngineEvents, ObjectEvents and WmiEvents) in PowerShell:
Do Jobs really work in background in powershell?
WPF events not working in Powershell - Carousel like feature in multi-threaded script
is it possible to control WMI events though runspace and the main form?
Is there a way to send events to the parent job when using Start-WPFJob?
Update WPF DataGrid ItemSource from Another Thread in PowerShell
They are all come down two one point: even there are multiple threads setup, there are two different 'listeners' in one thread. When your script is ready to receive form events (using ShowDialog or DoEvents) it can’t listen to .NET events at the same time. And visa versa: if script is open for .NET events while processing commands (like Start-Sleep or specifically listen for .NET events using commands like Wait-Event or Wait-Job), your form will not be able to listen to form events. Meaning that either the .NET events or the form events are being queued simply because your form is in the same thread as the .NET listener(s) your trying to create.
As with the nimizen example, with looks to be correct at the first glans, your form will be irresponsive to all other form events (button clicks) at the moment you’re checking the backgroundworker’s state and you have to click the button over and over again to find out whether it is still ‘*Doing Stuff’. To work around this, you might consider to combine the DoEvents method in a loop while you continuously checking the backgroundworker’s state but that doesn’t look to be a good way either, see: Use of Application.DoEvents()
So the only way out (I see) is to have one thread to trigger the form in the other thread which I think can only be done with using [runspacefactory]::CreateRunspace() as it is able to synchronize a form control between the treats and with that directly trigger a form event (as e.g. TextChanged).
(if there in another way, I eager to learn how and see a working example.)
Form example:
Function Start-Worker {
$SyncHash = [hashtable]::Synchronized(#{TextBox = $TextBox})
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ThreadOptions = "UseNewThread" # Also Consider: ReuseThread
$Runspace.Open()
$Runspace.SessionStateProxy.SetVariable("SyncHash", $SyncHash)
$Worker = [PowerShell]::Create().AddScript({
$ThreadID = [appdomain]::GetCurrentThreadId()
$SyncHash.TextBox.Text = "Thread $ThreadID has started"
for($Progress = 0; $Progress -le 100; $Progress += 10) {
$SyncHash.TextBox.Text = "Thread $ThreadID at $Progress%"
Start-Sleep 1 # Some background work
}
$SyncHash.TextBox.Text = "Thread $ThreadID has finnished"
})
$Worker.Runspace = $Runspace
$Worker.BeginInvoke()
}
[Void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Form = New-Object Windows.Forms.Form
$TextBox = New-Object Windows.Forms.TextBox
$TextBox.Visible = $False
$TextBox.Add_TextChanged({Write-Host $TextBox.Text})
$Form.Controls.Add($TextBox)
$Button = New-Object System.Windows.Forms.Button
$Button.Text = "Start worker"
$Button.Add_Click({Start-Worker})
$Form.Controls.Add($Button)
$Form.ShowDialog()
For a WPF example, see: Write PowerShell Output (as it happens) to WPF UI Control
The state property of Powershell jobs is read-only; this means that you can't configure the job state to be anything before you actually start the job. When you're monitoring for the statechanged event, it doesn't fire until the click event comes around again and the state is 'seen' to change from 'running' to 'completed' at which point your script block executes. This is also the reason why the scriptblock executes when closing the form.
The following script removes the need to monitor the event and instead monitors the state. I assume you want to fire the on 'statechanged' code when the state is 'running'.
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Form1 = New-Object Windows.Forms.Form
$Form1.Add_Shown({
$Form1.Activate()
})
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Text = 'Test'
$Form1.Controls.Add($Button1)
$Button1.Add_Click({
$this.Enabled = $false
Write-Host $Job.State " - (Before job started)"
$Job = Start-Job {'abc'}
Write-Host $Job.State " - (After job started)"
If ($Job.State -eq 'Running') {
Start-Sleep -Seconds 1
Write-Host '*Doing Stuff*'
}
Write-Host $Job.State " - (After IF scriptblock finished)"
[System.Windows.Forms.Application]::DoEvents()
$this.Enabled = $true
})
$Form1.ShowDialog()
In addition, note the lines:
$this.Enabled = $false
[System.Windows.Forms.Application]::DoEvents()
$this.Enabled = $true
These lines ensure the button doesn't queue click events. You can obviously remove the 'write-host' lines, I've left those in so you can see how the state changes as the script executes.
Hope this helps.

Resources