I'm writing a powershell script, which installes various drivers.
My script is working fine, so I want to add a gui using WPF.
First I created a gui using WPF, nothing spectacular, just a window with a label.
I would like to update this label from my install script. So I create two runspaces, one that creates and shows the WPF gui and the other one executes my install script.
That works fine as long as I'm using powershell version 3 or higher. With powershell 2, which I had to use with a new Windows 7 installation, the wpf runspace crashes down.
I hope there is a way to get this working with powershell version 2.
Here is a sample script, demonstating what I'm doing.
#########################################################################################
#
# W P F - R U N S P A C E
#
#########################################################################################
$syncHashWpfLuaNotification = [hashtable]::Synchronized(#{})
$runspaceWpfLuaNotification =[runspacefactory]::CreateRunspace()
$runspaceWpfLuaNotification.ApartmentState = "STA"
$runspaceWpfLuaNotification.ThreadOptions = "ReuseThread"
$runspaceWpfLuaNotification.Open()
$runspaceWpfLuaNotification.SessionStateProxy.SetVariable("syncHashWpfLuaNotification",$syncHashWpfLuaNotification)
$psCmd = [PowerShell]::Create().AddScript({
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
[xml]$xaml = #"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="TreiberInstaller" Height="431" Width="626" Background="Black"
WindowStyle="None" ResizeMode="NoResize" WindowStartupLocation="CenterScreen">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<Label Name="lblProgress" Content="Progress" HorizontalAlignment="Center" Grid.Column="1" Grid.Row="2" VerticalAlignment="Top" Foreground="White" FontFamily="Calibri" FontSize="18" HorizontalContentAlignment="Center"/>
</Grid>
</Window>
"#
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$syncHashWpfLuaNotification.Window = [Windows.Markup.XamlReader]::Load( $reader )
$syncHashWpfLuaNotification.lblProgress = $syncHashWpfLuaNotification.window.FindName("lblProgress")
$syncHashWpfLuaNotification.Window.ShowDialog() | Out-Null
$syncHashWpfLuaNotification.Error = $Error
})
$psCmd.Runspace = $runspaceWpfLuaNotification
$data = $psCmd.BeginInvoke()
Sleep -Milliseconds 450 # Wait a moment that the gui is ready
#########################################################################################
#
# W O R K E R - R U N S P A C E
#
#########################################################################################
$ScriptBlock = {
#----------------------------------------------------------------------
# SetLabelText: Sets the lable-text
#----------------------------------------------------------------------
function SetLabelText
{
param(
[Parameter(Position=0, Mandatory = $true, ValueFromPipeline = $false)]
[ValidateNotNullOrEmpty()]
[string]$Text
)
if(-not $syncHashWpfLuaNotification) {return}
try
{
$syncHashWpfLuaNotification.Window.Dispatcher.invoke(
[action]{$syncHashWpfLuaNotification.lblProgress.Content = $Text},
"Normal"
)
}
catch {}
}
#----------------------------------------------------------------------
# CloseProgressWindow: Closes the window
#----------------------------------------------------------------------
function CloseProgressWindow()
{
if(-not $syncHashWpfLuaNotification) {return}
try
{
$syncHashWpfLuaNotification.Window.Dispatcher.invoke(
[action]{$syncHashWpfLuaNotification.Window.Close()},
"Normal"
)
}
catch{}
}
#Starting here
SetLabelText -Text "Starting installation..."
Sleep 2
for($i=1;$i -le 19; $i++)
{
SetLabelText -Text ("Progress Step " + $i)
}
for($i=20;$i -le 24; $i++)
{
SetLabelText -Text ("Progress Step " + $i)
Sleep 1
}
CloseProgressWindow
} #End of $ScriptBlock
$syncHash1 = [hashtable]::Synchronized(#{})
$workerRunspace =[runspacefactory]::CreateRunspace()
$workerRunspace.ApartmentState = "STA"
$workerRunspace.ThreadOptions = "ReuseThread"
$workerRunspace.Open()
$workerRunspace.SessionStateProxy.SetVariable("syncHash1",$syncHash1)
$workerRunspace.SessionStateProxy.SetVariable("syncHashWpfLuaNotification",$syncHashWpfLuaNotification)
$psCmd1 = [PowerShell]::Create().AddScript($ScriptBlock)
$psCmd1.Runspace = $workerRunspace
$data = $psCmd1.BeginInvoke()
#########################################################################################
#
# S C R I P T E N D
#
#########################################################################################
#Wait for end of both runspaces
while(($runspaceWpfLuaNotification.RunspaceAvailability -eq "Busy") -or ($workerRunspace.RunspaceAvailability -eq "Busy"))
{
if($runspaceWpfLuaNotification.RunspaceAvailability -eq "Busy") { Write-Host "Window is open" }
if($workerRunspace.RunspaceAvailability -eq "Busy") { Write-Host "Worker is running" }
Sleep 1
}
Write-Host "Script ended"
There are some problems with how your pass delegates to Dispatcher.Invoke call.
ScriptBlock literals are bounded to current session state. If you pass bounded ScriptBlock to different Runspace, then it can cause some undesired effects, like code not invoked in target Runspace or deadlocks. See my other answer about this. So, first thing you need to do is to create new not bounded ScriptBlock. You can do that with [ScriptBlock]::Create method.
Since ScriptBlock is no more bounded to current session state, you can not refer variables from it, like you do with $Text in your code. You should pass them as additional parameters to Dispatcher.Invoke.
In .NET Framework 3.5, which will be used with PowerShell v2, there is no Invoke(Action, DispatcherPriority) overload in Dispatcher class, so that, Invoke([Action]{...}, "Normal") will be resolved to this Invoke(Delegate, Object[]) method instead. You should use different overload: Invoke(DispatcherPriority, Delegate), which exists in .NET Framework 3.5.
Try downloading WPFRunspace, which should work with PS V2. It provides a background worker for WPF and Forms based scripts.
Related
I am building a dashboard (WinForm) for internal usage, written in PS, and have a section where users can query sites using various methods/tools for inspection.
All the other methods/tools (IWR, nslookup via CMD call, OpenSSL via CMD call, CURL via CMD call, etc.) have no issues displaying their query results within a Form TextBox... and results for CURL commands seem to execute, return and save results properly in a PS string variable fine when run just within PS (see demo), but when running from the Form, PS seems to want console input for some reason and never displays the stored string in the Form (I presume because it isn't populating the same when called from the Form).
To minimally reproduce the issue, here is the code...
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void] [System.Windows.Forms.Application]::EnableVisualStyles()
$Form = New-Object system.Windows.Forms.Form
$Form.Size = New-Object System.Drawing.Size(600,880)
$form.MaximizeBox = $false
$Form.StartPosition = "CenterScreen"
$Form.FormBorderStyle = 'Fixed3D'
$Form.Text = "Query Dashboard"
$Font = New-Object System.Drawing.Font("Arial",9,[System.Drawing.FontStyle]::Bold)
$form.Font = $Font
#####RESULTSBOX and OUTPUT BUTTONS####
$Results_txtbox = New-Object System.Windows.Forms.RichTextBox
$Results_txtbox.Multiline = $true
$Results_txtbox.ScrollBars = "Vertical"
$Results_txtbox.Location = New-Object System.Drawing.Size(10,490)
$Results_txtbox.Size = New-Object System.Drawing.Size(485,270)
$Form.Controls.Add($Results_txtbox)
##########CURL#############
#divider
$CURLLinediv = New-Object System.Windows.Forms.Label
$CURLLinediv.Text = ""
$CURLLinediv.BorderStyle = "Fixed3D"
$CURLLinediv.AutoSize = $false
$CURLLinediv.height = 2
$CURLLinediv.width = 550
$CURLLinediv.Location = New-Object System.Drawing.Size(20,340)
$Form.Controls.Add($CURLLinediv)
$LabelCURL = New-Object System.Windows.Forms.Label
$LabelCURL.Text = "CURL"
$LabelCURL.AutoSize = $true
$LabelCURL.Location = New-Object System.Drawing.Size(30,342)
$Form.Controls.Add($LabelCURL)
$CURL_query_txtbox = New-Object System.Windows.Forms.TextBox
$CURL_query_txtbox.Location = New-Object System.Drawing.Size(20,360)
$CURL_query_txtbox.Size = New-Object System.Drawing.Size(300,20)
$CURL_query_txtbox.Text = "-s www.google.ca"
$CURL_query_txtbox.add_MouseHover($CURLquery_Help)
$Form.Controls.Add($CURL_query_txtbox)
$CURL_query_tip = New-Object System.Windows.Forms.ToolTip
$CURLquery_Help ={ $tip = "Enter entire CURL CMD here starting after curl"
$CURL_query_tip.SetToolTip($this,$tip)
}
$CURL_button = New-Object System.Windows.Forms.Button
$CURL_button.Location = New-Object System.Drawing.Size(325,355)
$CURL_button.Size = New-Object System.Drawing.Size(40,22)
$CURL_button.Text = "GET"
$CURL_button.Add_Click({CURL})
$Form.Controls.Add($CURL_button)
function CURL
{
#MOCK
#$CURL_query_txtbox.Text = "-s www.google.ca"
###$Results_txtbox.Text = (curl.exe $($CURL_query_txtbox.Text) 2>&1 | % ToString) -join "`n"
###$Results_txtbox.Text = (curl.exe -s www.google.ca 2>&1 | % ToString) -join "`n"
$Results_txtbox.Text = Invoke-Expression "curl.exe $($CURL_query_txtbox.Text) 2>&1"
}
$result = $Form.ShowDialog()
If you hit the GET button, the results should show in the Results textbox, but instead of returning the result to the form, PS wants an IWR parameter (if you enter www.google.ca or another Uri, it will return back to the application but does nothing with the input, and doesn't update the textfield... but you can now close the Form.
Now if in your PS window you run
$Results_txtbox.Text = (curl.exe -s www.google.ca 2>&1 | % ToString) -join "`n"
you can see that it populates $Results_txtbox.Text correctly when running from PS, and if you mock (ie - $CURL_query_txtbox.Text = "-s www.google.ca" ) first, you will see that
$Results_txtbox.Text = Invoke-Expression "curl.exe $($CURL_query_txtbox.Text) 2>&1"
works perfectly in PS alone, but when called from the Form it doesn't return any results, and the same cmdlet Invoke-WebRequest... message shows up in the PS console looking for a parameter to be entered.
The error message suggests that you accidentally called the curl alias, which is Windows PowerShell's built-in alias for the Invoke-WebRequest cmdlet (note that PowerShell [Core] 7+ no longer has this alias).
Even though you meant to call your custom CURL function, the built-in alias took precedence, because aliases have higher precedence than functions - see about_Command_Precedence.
A simple solution is to give your function a different name.
Also, you don't need cmd.exe to make your curl.exe call:
$Results_txtbox.Text = (curl.exe -s www.google.ca 2>&1 | % ToString) -join "`n"
If the target command - curl.exe -s www.google.ca - comes from a text box filled by the user and you trust that input, use Invoke-Expression, though note that, in general, it should be avoided.
$Results_txtbox.Text = (
Invoke-Expression "$($CURL_query_txtbox.Text) 2>&1" | % ToString
) -join "`n"
If only the arguments come from the text box:
Invoke-Expression "curl.exe $($CURL_query_txtbox.Text) 2>&1" | % ToString
Note:
% ToString (short for: ForEach-Object { $_.ToString() }) is only needed in Windows PowerShell, to ensure that stderr lines - which PowerShell wraps in ErrorRecord instances - are represented as-is in a string context - this is no longer necessary in PowerShell [Core] 7+.
-join "`n" (or -join [Environment]::NewLine, if you want to use the platform appropriate newline [sequence], though that is usually not necessary) is used to join the array of output lines to form a single, multi-line string; while piping to Out-String works similarly, it - unfortunately - always adds a trailing newline.
This unfortunate aspect of Out-String's behavior is discussed in GitHub issue #14444.
Also note that the stream-merging redirection, 2>&1, is embedded in the string passed to Invoke-Expression.
One would expect the following form to work as well:
Invoke-Expression $CURL_query_txtbox.Text 2>&1
However, as of PowerShell 7.1 it does not; this unexpected behavior is discussed in GitHub issue #14503.
I'm trying to search a WPF DataGrid via PowerShell, select and highlight the cells, each one in turn, that contains the search keyword. The goal is to 1) search the DataGrid, 2) select and highlight the cell that contains search keyword while moving the focus/scrolling to the row that contains column/cell and 3) repeat the same for any other columns/cells that may contain the same keyword.
I put together a sample script (see below) to demonstrate what I have done so far. In the sample script shown below, I was able to search and find the row contains the keyword and scroll to that row. However, I couldn't figure out how to highlight the cell that contains the keyword on that row.
The sample data shown below has pre-defined columns for demonstration purpose. However, the actual data returned from backend is dynamic such that the number of columns, title of the columns would vary and any column may contain the keyword. How do we select the cell that contains the keyword? Is there any better approach to achieve this overall goal? Thanks in advance for the help.
[xml]$xaml=#"
<Window
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:Test"
Title="MainWindow" Height="175" Width="550">
<Grid>
<TextBox x:Name="tb_Search" HorizontalAlignment="Left" Height="23" Margin="10,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="149"/>
<Button x:Name="bt_Search" Content="Search" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" IsDefault="True" Height="22" Margin="165,10,0,0" />
<DataGrid x:Name="dg" Margin="10,45,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinHeight="100" Height="Auto" Width="Auto" ColumnWidth="Auto" AlternationCount="1" IsReadOnly="True" SelectionMode="Extended" SelectionUnit="Cell" Background="White" />
</Grid>
</Window>
"#
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load($reader)
#Turn XAML into PowerShell objects
$xaml.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'x:Name')]]") | ForEach-Object{
Set-Variable -Name ($_.Name) -Value $Window.FindName($_.Name)
}
#sample data
$DataSet = New-Object System.Data.DataSet
$Table = $DataSet.Tables.Add("Table")
$Properties = #("Country","Capital","Population")
$Properties | foreach {
$Column = New-Object System.Data.DataColumn($_)
$Table.Columns.Add($Column)
}
$Null=$Table.Rows.Add("China PR","Beijing","20,693,000")
$Null=$Table.Rows.Add("India","New Delhi","16,787,949")
$Null=$Table.Rows.Add("Japan","Tokyo","13,189,000")
$Null=$Table.Rows.Add("Philippines","Manila","12,877,253")
$Null=$Table.Rows.Add("Russia","Moscow","11,541,000")
$Null=$Table.Rows.Add("Egypt","Cairo","10,230,350")
$Null=$Table.Rows.Add("USA","Washington, D.C","658,893")
$Null=$Table.Rows.Add("China PR","Beijing","20,693,000")
$Null=$Table.Rows.Add("India","New Delhi","16,787,949")
$Null=$Table.Rows.Add("Japan","Tokyo","13,189,000")
$Null=$Table.Rows.Add("Philippines","Manila","12,877,253")
$Null=$Table.Rows.Add("Russia","Moscow","11,541,000")
$Null=$Table.Rows.Add("Egypt","Cairo","10,230,350")
$Null=$Table.Rows.Add("USA","Washington, D.C","658,893")
#populate datagrid
$DataView = New-Object System.Data.DataView($Table)
$array = New-Object System.Collections.ArrayList
[void] $array.AddRange($DataView)
$dg.clear()
$dg.ItemsSource = $array
$dg.IsReadOnly = $true
$bt_Search.Add_Click({
$SearchValue = $tb_Search.text
for ($i = 0; $i -lt $dg.Items.Count; $i++)
{
if ($dg.Items[$i].Row[$dg.Columns.DisplayIndex] -eq "$SearchValue")
{
[System.Windows.Forms.MessageBox]::Show("Keyword Found")
$dg.ScrollIntoView($dg.Items[$i]) #scroll to the row that contains the keyword searched
}
}
})
#Display Form
$Window.ShowDialog() | Out-Null
Using a very similar method to what you're doing I was able to get it to highlight all cells that contain the search phrase (since I misunderstood the question at first). What I had to do was iterate through columns instead of searching all columns at once for each row. That way we know what column the desired info is in, and then we can select the cell in that column after we scroll the correct row into view.
$bt_Search.Add_Click({
$SearchValue = $tb_Search.text
$dg.SelectedCells.Clear()
for ($i = 0; $i -lt $dg.Items.Count; $i++)
{
0..($dg.Columns.Count-1)|?{$dg.Items[$i].Row[$_] -eq "$SearchValue"}|%{
$dg.ScrollIntoView($dg.Items[$i],$dg.Columns[$_])
$DGCell = $dg.Columns[$_].GetCellContent($dg.Items[$i]).Parent
$DGCellInfo = New-Object System.Windows.Controls.DataGridCellInfo($DGCell)
$dg.SelectedCells.add($DGCellInfo)
}
}
})
This at least gets you the groundwork to select a cell, you'll just need to figure out how to track the current cell to be able to move on to the next one since it sounds like you want to be able to move cell to cell by clicking the Search button over and over.
Edit: In regards to setting it up to do a Find/Find Next, you could setup a global variable, set $i to that, and then set that global variable inside your loop each time. Then anytime you get a match run a break to kill the loop, and it should just pick up where it left off. Might also want to add a line to reset the global variable before the loop so that it starts over if it hits the end. Something like this should do it:
$global:SearchIndex = 0
$bt_Search.Add_Click({
$SearchValue = $tb_Search.text
$dg.SelectedCells.Clear()
for ($i = $global:SearchIndex; $i -lt $dg.Items.Count; $i++)
{
0..($dg.Columns.Count-1)|?{$dg.Items[$i].Row[$_] -eq "$SearchValue"}|%{
$dg.ScrollIntoView($dg.Items[$i],$dg.Columns[$_])
$DGCell = $dg.Columns[$_].GetCellContent($dg.Items[$i]).Parent
$DGCellInfo = New-Object System.Windows.Controls.DataGridCellInfo($DGCell)
$dg.SelectedCells.add($DGCellInfo)
$global:SearchIndex = $i
break
}
}
#check if we hit the end of the table. If we did display a notice and reset the search index.
If($global:SearchIndex -ge $dg.Items.Count){
[System.Windows.Forms.MessageBox]::Show("No more matches found, resetting search to begining of table.")
$global:SearchIndex=0
}
})
Edit2: Ok, you're right, the break stops increments, so it will just find the same result each time. In order to fix that we need to also track the column. If we change the internal loop to a For loop as well we can just increment the internal loop each time we track. We also need to track breaks, so if the inner loop breaks the outer loop will too. So, something like this should do that:
$global:SearchIndex = 0
$global:SearchIndex2 = 0
$bt_Search.Add_Click({
$SearchValue = $tb_Search.text
$dg.SelectedCells.Clear()
for ($i = $global:SearchIndex; $i -lt $dg.Items.Count; $i++)
{
$global:BreakPoint = $false
For($j=$global:SearchIndex2;$j -le 2;$j++){
If($dg.Items[$i].Row[$j] -eq "$SearchValue"){
$dg.ScrollIntoView($dg.Items[$i],$dg.Columns[$j])
$DGCell = $dg.Columns[$j].GetCellContent($dg.Items[$i]).Parent
$DGCellInfo = New-Object System.Windows.Controls.DataGridCellInfo($DGCell)
$dg.SelectedCells.add($DGCellInfo)
$global:SearchIndex = $i
$global:SearchIndex2 = $j+1
$global:BreakPoint = $true
break
}
}
If($global:BreakPoint){break}
$global:SearchIndex2=0
}
#check if we hit the end of the table. If we did display a notice and reset the search index.
If($global:SearchIndex -ge $dg.Items.Count){
[System.Windows.Forms.MessageBox]::Show("No more matches found, resetting search to begining of table.")
$global:SearchIndex=0
$global:SearchIndex2=0
}
})
I created a PowerShell script that uses a WPF GUI. It has the option to choose a server from a combobox (more servers will be added later). The chosen server can be rebooted by clicking on the reboot button. Then a script, that will check the reboot procedure, will run in Button.Add_Click and the results will be shown in a textbox.
The problem is that the GUI freezes until Button.Add_Click is finished and will then show the info in the textbox.
I tried to solve this by implementing a runspace. But now I run into new problems.
In the ComboBox.add_SelectionChanged section I cannot get the selected combobox content. I want to store the content in a variable $computer.
In the Button.Add_Click section I can’t write to the textbox. When I use $Global:uiHash.Window.Dispatcher.Invoke([action]
$Global:uiHash.TextBox.AppendText("Check that reboot is initiated properlyn")},"Normal")` then the GUI freezes.
Here is the full code:
$Global:uiHash = [hashtable]::Synchronized(#{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("uiHash",$Global:uiHash)
$psCmd = [PowerShell]::Create().AddScript({
$Global:uiHash.Error = $Error
Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase
$xaml = #"
<Window
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"
Title="CSS Server Reboot" Height="450" Width="800">
<Grid>
<ComboBox Name="Server_Combobox" HorizontalAlignment="Left" Margin="240,107,0,0" VerticalAlignment="Top" Width="120">
<ComboBoxItem Name="Server1">10.15.12.148</ComboBoxItem>
</ComboBox>
<Label Name="Title_Label" Content="CSS – Console Quentris, Server Reboot
" HorizontalAlignment="Left" Margin="240,41,0,0" VerticalAlignment="Top" Height="34" Width="284" FontSize="16"/>
<Button Name="Reboot_Button" Content="Reboot" HorizontalAlignment="Left" Margin="449,107,0,0" VerticalAlignment="Top" Width="75"/>
<TextBox Name="Reboot_Textbox" Grid.Column="1" HorizontalAlignment="Left" Height="173" Margin="81,180,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="294"/>
</Grid>
"#
$Global:uiHash.Window=[Windows.Markup.XamlReader]::Parse($xaml )
$Global:uiHash.TextBox = $Global:uiHash.window.FindName("Reboot_Textbox")
$Global:uiHash.Button = $Global:uiHash.window.FindName("Reboot_Button")
$Global:uiHash.ComboBox = $Global:uiHash.window.FindName("Server_Combobox")
$Global:uiHash.Window.ShowDialog() | out-null
})
$psCmd.Runspace = $newRunspace
$handle = $psCmd.BeginInvoke()
Start-Sleep -Milliseconds 100
$computer = ""
$Global:uiHash.ComboBox.add_SelectionChanged({
$Script:computer = $Global:uiHash.Combobox.SelectedItem.Content
})
$Global:uiHash.Button.Add_Click({
$Username = 'xxxx'
$Password = 'xxxx'
$pass = ConvertTo-SecureString -AsPlainText $Password -Force
$SecureString = $pass
$MySecureCreds = New-Object -TypeName
System.Management.Automation.PSCredential -ArgumentList
$Username,$SecureString
Restart-Computer -ComputerName $computer -Force -Credential $MySecureCreds
$timeout=5
$MAX_PINGTIME = $timeout * 60
$max_iterations = $MAX_PINGTIME/5
$Notification_timeout = 10 # in seconds
function ping-host {
param($pc)
$status = Get-WmiObject -Class Win32_PingStatus -Filter "Address='$pc'"
if( $status.statuscode -eq 0) {
return 1
} else {
return 0
}
}
if(ping-host -pc $computer) {
$status = "online`n"
for ($i=0; $i -le $max_iterations; $i++) {
if (!(ping-host -pc $computer )) {
break
}
Start-Sleep -Seconds 5
if($i -eq $max_iterations) {
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText("$computer never went down in last $timeout minutesn")},"Normal")
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText("Check that reboot is initiated properlyn")},"Normal")
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText("$computer is still ONLINE; Check that reboot is initiated properly`n")},"Normal")
exit
}
}
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText("$computer is offline now; monitoring for online status`n")},"Normal")
} else {
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText("$computer is offline; Monitoring for online statusn")},"Normal")
$status = "offlinen"
}
for ($i=0; $i -le $max_iterations; $i++) {
if ((ping-host -pc $computer )) {
break
}
Start-Sleep -Seconds 5
if($i -eq $max_iterations) {
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText("Your computer never came back online in last $MAX_PINGTIME secondsn")},"Normal")
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText("Check that nothing is preventing starupn")},"Normal")
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText("$Computer is NOT coming online; Something is preventing its startup`n")},"Normal")
exit
}
}
$Global:uiHash.Window.Dispatcher.Invoke([action]
{$Global:uiHash.TextBox.AppendText("Your computer is Online Now; Task done;
exiting")},"Normal")
})
The reason that the GUI freeze is that the job you are running is in the same instance.
Since the GUI runs in the same instance it will have to wait until whatever code is running is done before it can go back to taking new input.
You could try and solve this running the code you want to execute when you click the button as a job, this way the code will run in a new process on the machine and not block the session the GUI is running in.
This can be done with the Start-Job cmdlet
This is regarding Windows Forms in PowerShell and the System.Windows.Forms.ProgressBar.
I am looking all over and cannot find anything that allows the progress bar to fill vertically. I've considered alternatives (such as resizing a label with a background color), but I prefer to use something that already has a class if possible. I could have sworn I had seen something out there like this before, even if it wasn't a true progress bar. I am not using it to track progress, but more for CPU usage, RAM usage, and drive space for server statuses. This is for a useful GUI for a quick report of servers from a dropdown list, something where I don't have to open another complete shell session (I have enough shells open as it is between O365 eDiscovery, data analysis, and other needs). Thanks for any suggestions in advance.
Here is a very good C# answer How do I make a winforms progress bar move vertically in C#?
It overrides the CreateParams method to set the PBS_VERTICAL flag in Style.
To make it work in PowerShell you will unfortunately have to use a bit of C# code.
This works for me:
$type = #'
using System;
using System.Windows.Forms;
public class VerticalProgressBar : ProgressBar {
protected override CreateParams CreateParams {
get {
CreateParams cp = base.CreateParams;
cp.Style |= 0x04;
return cp;
}
}
}
'#
Add-Type -TypeDefinition $type -ReferencedAssemblies System.Drawing,System.Data,System.Windows.Forms
$userForm = New-Object System.Windows.Forms.Form
$userForm.Text = "$title"
$userForm.Size = New-Object System.Drawing.Size(230,300)
$userForm.StartPosition = "CenterScreen"
$userForm.AutoSize = $False
$userForm.MinimizeBox = $False
$userForm.MaximizeBox = $False
$userForm.SizeGripStyle= "Hide"
$userForm.WindowState = "Normal"
$userForm.FormBorderStyle="Fixed3D"
$progressbar = New-Object 'VerticalProgressBar'
$progressbar.Location = New-Object System.Drawing.Point(180, 50);
$progressbar.Width = 20
$progressbar.Height = 200
$userForm.Controls.Add($progressbar)
$TrackBar = New-Object 'System.Windows.Forms.TrackBar'
$TrackBar.Location = New-Object System.Drawing.Point(10, 10);
$TrackBar.Width = 200
$TrackBar.add_ValueChanged({$progressbar.Value = $this.value*10})
$userForm.Controls.Add($TrackBar)
$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Size(10,220)
$OKButton.Size = New-Object System.Drawing.Size(75,23)
$OKButton.Text = "OK"
$OKButton.Add_Click({$userForm.Close()})
$userForm.Controls.Add($OKButton)
$userForm.ShowIcon = $False
$userForm.Add_Shown({$userForm.Activate()})
$userForm.AcceptButton = $OKButton
[void] $userForm.ShowDialog()
I am building a WPF UI using Powershell and XAML. The particular feature I am having problems with is setting the style on the columns. I am able to set the style within the XAML, but there are 9 columns (3 columns each in 3 data grids) so I feel it would be neater to set the properties in the code rather than repeat the same block 9 times.
This is how I set the style on a single column through XAML:
<DataGridTextColumn Header="Attribute" Binding="{Binding Attribute}">
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<Trigger Property="Text" Value="">
<Setter Property="Background" Value="LightGreen"/>
</Trigger>
</Style.Triggers>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
This sets blank cells to light green. However, my preferred method would be to set this in Powershell.
So far I have figured out how to manually create the columns in Powershell if they are not explicitly added in XAML. I do this like so (after binding to the controls):
$arrAllDataGrids = #($dataGridUser, $dataGridHome, $dataGridWWID)
ForEach ($objDataGrid in $arrAllDataGrids)
{
$objDataGrid.AutoGenerateColumns = $false
$colAttribute = New-Object System.Windows.Controls.DataGridTextColumn -Property #{"Header" = "Attribute"; "Binding" = New-Object System.Windows.Data.Binding "Attribute"}
$colValue = New-Object System.Windows.Controls.DataGridTextColumn -Property #{"Header" = "Value"; "Binding" = New-Object System.Windows.Data.Binding "Value"}
$colDate = New-Object System.Windows.Controls.DataGridTextColumn -Property #{"Header" = "Date"; "Binding" = New-Object System.Windows.Data.Binding "Date"}
$arrColumns = #($colAttribute, $colValue, $colDate)
ForEach ($objColumn in $arrColumns) {$objDataGrid.Columns.Add($objColumn)}
}
However, I if I try and add a style through Powershell I come up against Dependency Properties. I've tried to examine them by creating the style in XAML and then trying to reverse engineer it in Powershell, this is what I've worked out so far:
$dataGridUser.Columns[0].ElementStyle is Type System.Windows.Style
$dataGridUser.Columns[0].ElementStyle.Triggers is Type System.Windows.Trigger
$dataGridUser.Columns[0].ElementStyle.Triggers.Property is Type Selected.System.Windows.Trigger
$dataGridUser.Columns[0].ElementStyle.Triggers.Property.Property is Type System.Windows.DependencyProperty
I'm unable to create an object of type System.Windows.DependencyProperty which should hold a Name property of Text. The best I have managed so far is to create one column through XAML and then extract the Dependency property object like this:
$objDepend = $($dataGridUser.Columns[0].ElementStyle.Triggers | Select Property).Property
Using this object, I can then create a new column object with the correct trigger property (I appreciate this is still missing the 'Setter' property but I think the same principles will apply) like this:
$objSelect = $(New-Object System.Windows.Trigger | Select Property)
$objSelect.Property = $objDepend
$objTrigger = New-Object System.Windows.Trigger
$objTrigger.Value = ""
$objTrigger.Property = $objSelect.Property
$objStyle = New-Object System.Windows.Style
$objStyle.Triggers.Add($objTrigger)
$colAttribute = New-Object System.Windows.Controls.DataGridTextColumn -Property #{"Header" = "Attribute"; "Binding" = New-Object System.Windows.Data.Binding "Attribute"; "ElementStyle" = $objStyle}
This is clearly not the right way to go about this though, I am obviously missing something, presumably around Dependency properties which I suspect of being derived rather than simply set.
I hope that all makes sense, can anyone help?
I hope it is acceptable to answer my own (and only) question but I thought I would share the solution I developed in case anyone else is in a similar position.
Simply write a function that creates the Windows.Style.Object and passes it back. I've started with two default strings like so:
$strXAMLStart = #"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<DataGrid x:Name="dataGridTrigger">
<DataGrid.Columns>
<DataGridTextColumn>
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>`n
"#
$strXAMLEnd = #"
</Style.Triggers>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
"#
So the function briefly creates and loads an XAML object using the style parameters passed to it and then extracts the style object and passes it back as the result of the function. This can then be applied to the column in the UI.
The function is as follows:
Function Set-ColumnStyle ($hashStyle)
{
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
Add-Type -AssemblyName System.Windows.Forms
$strXAMLTrigger = $null
ForEach ($key in $hashStyle.Keys)
{
$strValue = $hashStyle[$key]
If ($key -eq "Blank") {$strTrigger = ""}
Else {$strTrigger = $key}
$strXAMLTrigger += " <Trigger Property=`"Text`" Value=`"$strTrigger`">`n"
$strXAMLTrigger += " <Setter Property=`"Background`" Value=`"$strValue`"/>`n"
$strXAMLTrigger += " </Trigger>`n"
}
[xml]$xamlContent = $strXAMLStart + $strXAMLTrigger + $strXAMLEnd
$xamlReader = (New-Object System.Xml.XmlNodeReader $xamlContent)
$xamlWindow = [Windows.Markup.XamlReader]::Load($xamlReader)
$dataGridTrigger = $xamlWindow.FindName("dataGridTrigger")
$objStyle = $dataGridTrigger.Columns[0].ElementStyle
Return $objStyle
}
I've used the string "blank" to represent actual blank cells, so this is replaced by "" if detected.
You can now call this function and pass it a hashtable of column contents to desired colours and use the returned object to set the style on the column:
$hashStyle = #{"Blank" = "#FFFF5050"; "0" = "#FF50FF50"; "267009" = "#FF50FF50"}
$objStyle = Set-ColumnStyle $hashStyle
$colDay = New-Object System.Windows.Controls.DataGridTextColumn -Property #{"Header" = "Day"; "Binding" = New-Object System.Windows.Data.Binding "Day"; "ElementStyle" = $objStyle}
$dataGridScripts.Columns.Add($colDay)
Hope that all makes sense!