I have been playing with some WPF in PowerShell, with the goal of creating a messaging system and a resizable, scrollable log viewer.
I have the basics of a messaging system working here.
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$pshome\powershell.exe")
[xml]$xaml = '<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="window" WindowStyle="None" Height="200" Width="400"
ResizeMode="NoResize" ShowInTaskbar="False">
<Grid Name="grid" Background="#313130" Height="200" Width="400">
<Label Name="label" Content="Messenger Test" Foreground="White" FontSize="18" Margin="10,10,0,15"/>
<TextBox x:Name="Message" Height = "50" FontSize="18" Margin="10,10,0,15" />
</Grid>
</Window>'
$window = [Windows.Markup.XamlReader]::Load([System.Xml.XmlNodeReader]::New($xaml))
$window.Left = [System.Windows.SystemParameters]::WorkArea.Width-$window.Width
$window.Top = 0
$window.Topmost = $true
$message = $Window.FindName('Message')
$timer = [System.Windows.Forms.Timer]::new()
$timer.Add_Tick({
$timer.Stop()
})
# Close the window if it's double clicked
$window.Add_MouseDoubleClick({
$timer.Stop()
})
$maxLoops = 5
for ($messageCount = 1; $messageCount -le $maxLoops; $messageCount++) {
$messageString = "($messageCount) $(Get-Date -format 'HH:mm:ss')"
$message.Text = $messageString
$window.Show()
# start the timer to fire after 10 seconds and then disable itself
$timer.Stop()
$timer.Interval = 5000
$timer.Start()
# while the Tick event did not occur, respond to other events
# such as mouse double-click on the window
while ($timer.Enabled) { [System.Windows.Forms.Application]::DoEvents() }
# the timer tick event happened or the user double-clicked the window
$window.Hide()
# end of the loop reached, no use waiting some more..?
if ($messageCount -ge $maxLoops) {
break
}
# start the timer to fire after (random) 1 - 5 seconds
$interval = (Get-Random -Minimum 1 -Maximum 5)
$timer.Stop()
$timer.Interval = $interval * 10000
$timer.Start()
while ($timer.Enabled) { [System.Windows.Forms.Application]::DoEvents() }
}
$timer.Dispose()
$window.Close()
However, this code will cause a single core to go to 100% utilization. Which doesn't make sense to me. I thought the timer was supposed to be super light weight from a resource utilization standpoint, and that I would get near 0% CPU utilization, with tiny spikes as messages where added. Is there something I am missing here? And in fact, is it really the timer that is the issue, or is there a performance issue with WPF, either in general or in how I have implemented this?
The idea is to have the messaging system run as a service (either a real service, or as a scheduled task that restarts itself) and it would monitor for incoming messages. The messages are added to a queue in the registry, so I could likely use an event rather than a timer. But still, it seems (to me) like this code SHOULDN'T be such a resource hog, and I will learn something useful if I understand why it IS a resource hog.
Related
I am trying to enter a 10 second wait in order to execute a command in my script.
The problem is that when I run start-sleep my interface flickers or blocks the window.
I tried to do a special function without using start-sleep but the same thing happens.
First solution (Freeze/Block interaction windows)
if ($Timeout -gt 0) {
Start-Sleep -seconds $Timeout
}
Second solution (Freeze/Block interaction windows)
if ($Timeout -gt 0) {
Timeout $Timeout
}
function Timeout($seconds) {
$doneDT = (Get-Date).AddSeconds($seconds)
while($doneDT -gt (Get-Date)) {
$secondsLeft = $doneDT.Subtract((Get-Date)).TotalSeconds
write-host $secondsLeft
}
}
How could I do to timeout code execution without freezing my winforms? Thank you
I would suggest you to do a Background Job using Start-Job (easiest) or Runspace (a little hard to do but efficient) for your task so u do not have to rely on the sleep timer to make sure ur command completed. But that is your prerogative.
However, you can use this neat little trick although not ideal, to counter your non-responsive form problem. Use the below block to introduce the 10s delay.
for ($i = 0; $i -lt 50; $i++)
{
Start-Sleep -Milliseconds 200
[System.Windows.Forms.Application]::DoEvents()
}
EDIT: Including suggestions.
Sure you can do this inside WinForms. I have done this before. I just moved onto Runspaces later for reasons of efficiency and resources. But for simple purposes, Start-Job is the easiest way to go.
$ScriptBlock = {
#super lengthy code enclosed in paranthesis to create a scriptblock
#say takes a large amount of time to complete
}
$myJob = Start-Job -ScriptBlock $ScriptBlock
Do
{
Start-Sleep -Milliseconds 100
[System.Windows.Forms.Application]::DoEvents()
}
While ($myJob.State -ne "Completed")
$myJob | Receive-Job
Now you can include contingencies for when the job fails and stuff, but I will let u figure that out.
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
})
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! >_>
I'm trying to use PowerShell to find the total time elapsed for a Silverlight web application (IE10) to load. Since this is a Silverlight app, I wished to find the time elapsed AFTER Silverlight loads, not just when the web page loads. I've tried the following but Silverlight always crashes.
$ie = New-Object -ComObject internetexplorer.application
$ie.visible = $true
while($ie.busy){}#wait for browser to load
$time = measure-command -expression{
$ie.navigate('http://www.mysilverlightapp.com')
while($ie.readyState -ne 4){}
}
write-host "$($time.totalseconds)"
Please help me on what I'm doing wrong.
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()