PowerShell/WPF: Disable Button on Click - wpf

I'm writing a little toolbox at work for the common vSphere tasks. This time around, I wanted to dip into WPF.
I have created an initial connect-to-server GUI, and everything works as designed. But now I'm stuck at the following:
When clicking the Connect button (btnConnect), I want it to become greyed out/disabled, followed by the connection attempt. Once the attempt is done, it can become active again. This is to prevent people from clicking on it multiple times in a row.
This is my first attempt (I'm unable to show the complete thing, given that it's 4 files and there's some stuff in it that I'm not allowed to freely share, so I will post what's relevant):
$Window.btnConnect.Add_Click({
$Window.btnConnect.IsEnabled = $False
$Window.btnConnect.Content = 'Connecting...'
Connect-CompanyVIServer -VIServer 'ServerName' -VICredential 'PSCredentialObject'
$Window.btnConnect.Content = 'Connect'
$Window.btnConnect.IsEnabled = $True
})
Essentially, I disable the button, change it's text to Connecting..., and connect to the VMware vCenter Server. Afterwards I change the button label back and re-enable it.
When I click on the button, the form hangs while it's loading and making the connection attempt. This is to be expected, being that they're in the same thread. That's fine, since the form should do anything until the attempt is completed anyway (and learning both WPF AND runspaces at the same time would make it too complex).
The problem is that even though the $Window.btnConnect.IsEnabled = $False is at the top, the form doesn't actually update until the entire block is done processing. Until that time, I can continue clicking on the button and it'll just buffer the attempts and execute them after one-another.
So I figured I'd split them up into separate events:
$Window.btnConnect.Add_Click({ $Window.btnConnect.IsEnabled = $False })
$Window.btnConnect.Add_IsEnabledChanged({
If ($Window.btnConnect.IsEnabled -eq $False) {
$Window.btnConnect.Content = 'Connecting...'
Connect-CompanyVIServer -VIServer 'ServerName' -VICredential 'PSCredentialObject'
$Window.btnConnect.Content = 'Connect'
$Window.btnConnect.IsEnabled = $True
}
})
A valiant attempt, didn't work though. I get the exact same thing.
So now I'm out of ideas. I was considering writing a trigger into the WPF code to disable the button, but I have no idea if that works. If I do something to run the connection attempt asynchronously, I'd need to be able to query fir the outcome somehow. I'm still experimenting. In the meantime, I come here for help.
My goal is to open a new window once the connection is present and then let it close this window. The important part is that I prevent users from clicking on the button more than once.
Anyone?

As a comment above alludes to, turning the button off, doing work, and then turning it on again is on a particular thread, but not the thread that updates the GUI. It seems as though it happens instantaneously from the GUI thread perspective, i.e. off then on, nothing happens.
If you use the dispatcher to send a message to the GUI thread, i.e.
$Window.Dispatcher.invoke([action]{$Window.btnConnect.IsEnabled -eq $False})
you would see that now you have successfully disabled the button (grayed it out) after a single click. Unfortunately, that's now a permanent situation on the GUI.
To do the "turn off, work, turn on" in a different thread, using a runspace, would go something like (forgive me if not perfect):
function my_func {
param($Window,$btnConnect)
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ApartmentState = "STA"
$Runspace.ThreadOptions = "ReuseThread"
$Runspace.Open()
$Runspace.SessionStateProxy.SetVariable("Window",$Window)
$Runspace.SessionStateProxy.SetVariable("btnConnect",$btnConnect)
#add server name/credentials with SetVariable so they can be seen in code block
$code = {
$Window.Dispatcher.invoke(
[action] {$Window.Dispatcher.btnConnect.IsEnabled = $False})
$Window.btnConnect.Content = 'Connecting...'
Connect-CompanyVIServer -VIServer 'ServerName' -VICredential 'PSCredentialObject'
$Window.btnConnect.Content = 'Connect'
#below line throws away any clicks since we've disabled button
[System.Windows.Forms.Application]::DoEvents()
$Window.Dispatcher.invoke(
[action] {$Window.Dispatcher.btnConnect.IsEnabled = $True})
}
$PSinstance = [powershell]::Create().AddScript($code)
$PSinstance.Runspace = $Runspace
$job = $PSinstance.BeginInvoke()
}
$Window.btnConnect.Add_Click({
my_func $Window $Window.btnConnect
})

Related

using a RadDesktopAlert in a Console Application

I have got a console application that to watch for another process to be alive and otherwise send an email (this is just implemented and working) and show a notification. I've used MessageBox.Show but it makes really Win3.1, I was wondering if someone succeeded in using Telerik's \
I've tried with this piece of code but nothing happens (even an exception)
RadDesktopAlert alert = new RadDesktopAlert();
alert.CaptionText = "Critical Error";
alert.ContentText = "Some text";
alert.Show();
Any suggestion?
You still need to add a RadDesktopManager
RadDesktopAlertManager manager = new RadDesktopAlertManager ();
manager.ShowAlert (alert);

PowerShell Forms Won't Trigger DragDrop

There's nothing to suggest that a simple Windows Forms DragDrop won't work in PowerShell, and several resources explaining that it does work, however I cannot get any of them working. Even something as simple as this:
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$form = New-Object System.Windows.Forms.Form
$TBcode = New-Object System.Windows.Forms.TextBox
$form.Controls.Add($TBcode)
$TBcode.Dock = "Fill"
$TBcode.Multiline = $true
$TBCode.ScrollBars = "Vertical"
$TBCode.AllowDrop = $true
$TBcode.Add_DragEnter({ Write-Host "DragEnter"; $_.Effects = "Copy" })
$TBcode.Add_DragDrop({ Write-Host "DragDrop" })
$TBcode.Add_MouseEnter({ Write-Host "Mouse Enter" })
$form.ShowDialog()
The MouseEnter event triggers normally however when attempting to drag anything in to the TextBox nothing happens.
I had a sudden realisation and was able to confirm that this is in fact User Account Control.
Because I'm on Windows 10 I've got my PowerShell running as Administrator so I can actually do what I need to, however an object from the User-level Explorer process (or anything else running as a user) will simply refuse to interact with the Administrator-level PowerShell form.
Running PowerShell as a user allows the DragDrop to work as expected, but good luck if you need it to be admin! >_>

GUI update from another thread - WPF Powershell

I asked this question before, but I still am having issues. Really hoping I could have some assistance. all I need is to be able to add append text to my Rich TextBox from a background job. If I remove Start-Job -ScriptBlock { } it updates fine while on the GUI thread. What can I do in order to update my richtextbox from Start-Job?
Start-Job -ScriptBlock{
$richTextBox1.AppendText('++++++++++++++++')
$richTextBox1.Dispatcher.Invoke([action]{
$richTextBox1.AppendText('-------------')
},"Normal")
}
I'm not a massive user of Powershell within WPF however as I understand your post I think I can still help.
public void RunPowershell_Script()
{
Thread t = new Thread(new ThreadStart(
delegate
{
Start-Job -ScriptBlock{
this.Dispatcher.Invoke((Action)(() =>
{
//Any Element that you need to interact with variables
//or controls from the main application on the main
//dispatcher thread go here.
$richTextBox1.AppendText('++++++++++++++++')
$richTextBox1.AppendText('-------------')
}));
}
//Any final code can go here.
}
));
t.Start();
}
I believe this should work but you would have to have a play around. the above is the best way to access the main dispatcher thread in which the RTF box was originally created. Give it a try and let me know if it still doesn't work.

winform displaying in classic view outside of ISE

Not sure what is causing this issue but I have a PowerShell script that uses Windows Forms as the UI. When developing it inside of the PowerShell ISE everything looked nice with modern style buttons. When I run it from PowerShell it displays in a Windows Classic style view and crashes when I make a call to New-Object System.Windows.Forms.SaveFileDialog. Is there a fix for this that I can add to my code so my script not only looks better but actually functions outside of the ISE?
Edit:
This is my function where I call SaveFileDialog. This works inside of the ISE but when I run the script from PowerShell itself it crashes when I call this.
function exportToCSV([System.Object[]] $expArray) {
$save = New-Object System.Windows.Forms.SaveFileDialog
$save.CreatePrompt = $False
$save.SupportMultiDottedExtensions = $True
$save.DefaultExt = "csv"
$save.Filter = "CSV (*.csv) | *.csv*"
$save.Title = "Export to CSV"
if ($save.ShowDialog() -eq "OK") {
$expArray | export-csv $save.FileName
}
}
Include the following command in your script before showing the form to get modern style appearance.
[System.Windows.Forms.Application]::EnableVisualStyles()
This should however not affect the savefiledialog. I'm gonna need more to help you with that since you're saying it works in ISE, but not in normal console.
A workaround for your SaveFileDialog is adding:
$save.ShowHelp = $true
In PS3.0 everything works fine, but in PS2.0 the dialog doesn't show. The ShowHelp fixes that, but also gives you an old-style dialog. However, functionality is more imporant then appearance. :)

In PowerShell Form.Show() does not work right, but Form.ShowDialog() does

I am trying to display an image via powershell. I made a script based on this forum post.
If I use ShowDialog() it works fine, except the powershell execution stops while the dialog is up. However, that is by design for a modal dialog. If I call Form.Show() in PowershellISE the form shows up, but freezes and cannot be moved or dismissed. Behavior is similar if I copy and past the code to a powershell console.
How do I make the dialog non-modal, and not freeze.
First Answer Why it appends.
In a Windows graphic program the thread which create a Window must loop in a message pump in order to redistribute (translate) messages coming from the user action to events in his Windows.
In a modal window, the modal code that handles the window display runs its own message pump loop and doesn't return until the window is closed. That's why the code after ShowDialog() won't execute until the window is closed.
Show(), just ask to show the Window, but if there is no pump loop to manage the messages coming from user action, it just freezes.
Second a simple way to have two threads
The CmdLet start-job use another thread from the pool allocated by Powershell so it makes the dialog non-modal, and it does not freeze.
function goForm
{
[void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms")
$file = (get-item 'C:\temp\jpb.png')
#$file = (get-item "c:\image.jpg")
$img = [System.Drawing.Image]::Fromfile($file);
# This tip from http://stackoverflow.com/questions/3358372/windows-forms-look-different-in-powershell-and-powershell-ise-why/3359274#3359274
[System.Windows.Forms.Application]::EnableVisualStyles();
$form = new-object Windows.Forms.Form
$form.Text = "Image Viewer"
$form.Width = $img.Size.Width;
$form.Height = $img.Size.Height;
$pictureBox = new-object Windows.Forms.PictureBox
$pictureBox.Width = $img.Size.Width;
$pictureBox.Height = $img.Size.Height;
$pictureBox.Image = $img;
$form.controls.add($pictureBox)
$form.Add_Shown( { $form.Activate() } )
$form.ShowDialog()
}
Clear-Host
start-job $function:goForm
$name = Read-Host "What is you name"
Write-Host "your name is $name"
There are ways to make this work, but nothing is worth spending five hours explaining on an open forum. There are other free, shrink-wrapped ways to do this on powershell. Most notably with the free WPF powershell toolkit: Show-UI at http://showui.codeplex.com/ (previously known as WPK and/or PowerBoots - they are merged now.)
If your goal is actually to not block the interactive console when an image is shown then you still can use the script as it is with ShowDialog but you should start it using, for example, Start-Job. Thus, the dialog is still modal but it blocks execution in another runspace. The main runspace still can be used for invoking other commands.
Caveats: 1) You should close all opened dialogs before closing the interactive console. 2) If you care, you should remove completed jobs yourself (when a dialog is closed a job that started it still exists).
I use a similar approach in my custom host and it works fine. I also tested it with the script from your link. I changed it slightly so that it is called show-image.ps1 and accepts a file path as a parameter.
This command shows an image and blocks the calling runspace:
show-image.ps1 C:\TEMP\_110513_055058\test.png
This command shows an image and does not block the calling runspace:
Start-Job { show-image.ps1 $args[0] } -ArgumentList C:\TEMP\_110513_055058\test.png
Building on #JPBlanc's anwer, this would also be possible (and faster) using a runspace.
Here's a basic example (the rest would basically remain the same)
$ps = [PowerShell]::Create()
[void]$ps.AddScript({
Add-Type -AssemblyName System.Windows.Forms
$form = [Windows.Forms.Form]::new()
$form.ShowDialog()
})
[void]$ps.BeginInvoke()

Resources