I am trying to do grouped items in a WPF ComboBox using XAML, and I found this that talks about using a list to drive the grouping. But the example Code Behind is of course not Powershell, and I am having a heck of a time groking how to build the list. Can someone point me at a good resource on CollectionViews in Powershell? I have zero experience in C/C++/C#, though I hope Powershell ends up being a gateway drug. ;)
BTW, I did find this, that talks about doing the collection view in XAML, but the content of my collectionView is going to be dynamic, so I have to build it in code.
BTW, the ultimate goal is a combo box that looks like this...
Sets
Arch
MEP
Viz
Packages
RVT_2015
RVT_2016
MAX_2016
... where the actual sets and packages are dynamically pulled from some user customized XML, and the user can pick one set, or one package. Trying to do two lists and enforce one choice with a UX that isn't frustrating has been, less than effective. So I thought a single grouped combo box would address the problem nicely.
EDIT: Woot to progress!
$locations = #('Amsterdam', 'Berlin', 'London')
$sets = #('Arch', 'MEP', 'Viz')
$packages = #('RVT_2015', 'RVT_2016', 'MAX_2016')
[xml]$xaml = #"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="Window" Title="Initial Window" WindowStartupLocation = "CenterScreen" ResizeMode="NoResize"
Width = "300" Height = "200" ShowInTaskbar = "True" Background = "lightgray">
<StackPanel>
<Label Content="Define your job:" VerticalAlignment="Center" HorizontalAlignment="Left" />
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="Location" VerticalAlignment="Center" HorizontalAlignment="Left" />
<ComboBox Name="Location" Width="200"
Grid.Row="0" Grid.Column="1"
VerticalAlignment="Center" HorizontalAlignment="Right"
VerticalContentAlignment="Center"
Margin="0,0,0,0">
</ComboBox>
<Label Grid.Row="1" Grid.Column="0" Content="Target" VerticalAlignment="Center" HorizontalAlignment="Left" />
<ComboBox Name="Target"
Grid.Row="1" Grid.Column="1"
VerticalAlignment="Center" HorizontalAlignment="Right"
VerticalContentAlignment="Center"
Width="200" Margin="0,0,0,0">
<ComboBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ComboBox.GroupStyle>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</StackPanel>
</Window>
"#
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$window=[Windows.Markup.XamlReader]::Load($reader)
$data = #()
foreach ($target in $sets) {
$data += New-Object PSObject -prop #{Name="$target";Category="Sets"}
}
foreach ($target in $packages) {
$data += New-Object PSObject -prop #{Name="$target";Category="Packages"}
}
$listView = [System.Windows.Data.ListCollectionView]$data
$listView.GroupDescriptions.Add((new-object System.Windows.Data.PropertyGroupDescription "Category"))
$location = $window.findname("Location")
$location.ItemsSource = $locations
$target = $window.findname("Target")
$target.ItemsSource = $listView
$window.ShowDialog() | out-null
Based on the first example you have linked in your question, here is a simple implementation in PowerShell
Add-Type –assemblyName WindowsBase
Add-Type –assemblyName PresentationCore
Add-Type –assemblyName PresentationFramework
[xml]$xaml = #"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<ComboBox Name="comboBox" Width="200" VerticalAlignment="Top" HorizontalAlignment="Left"
Margin="10,20,0,0">
<ComboBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ComboBox.GroupStyle>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</Window>
"#
$reader = (New-Object Xml.XmlNodeReader $xaml)
$GUIWindow = [Windows.Markup.XamlReader]::Load( $reader )
$xaml.SelectNodes("//*[#Name]") | % {Set-Variable -Name ($_.Name) -Value $GUIWindow.FindName($_.Name)}
$data = #()
$data += New-Object PSObject -prop #{Name="Arch";Category="Sets"}
$data += New-Object PSObject -prop #{Name="MEP";Category="Sets"}
$data += New-Object PSObject -prop #{Name="Viz";Category="Sets"}
$data += New-Object PSObject -prop #{Name="RVT_2015";Category="Packages"}
$data += New-Object PSObject -prop #{Name="RVT_2016";Category="Packages"}
$data += New-Object PSObject -prop #{Name="MAX_2016";Category="Packages"}
$lview = [System.Windows.Data.ListCollectionView]$data
$lview.GroupDescriptions.Add((new-object System.Windows.Data.PropertyGroupDescription "Category"))
$comboBox.ItemsSource = $lview
$GUIWindow.ShowDialog() | out-null
This is very basic, but you can build on that.
EDIT:
Original answer was using the following two lines:
$lview = [System.Windows.Data.ListCollectionView]::new($data)
$lview.GroupDescriptions.Add([System.Windows.Data.PropertyGroupDescription]::new("Category"))
They work in PS5, but will produce an error in all previous version of PowerShell. The workaround is to use Typecasting for ListCollectionView. This works in PS2.
$lview = [System.Windows.Data.ListCollectionView]$data
$lview.GroupDescriptions.Add((new-object System.Windows.Data.PropertyGroupDescription "Category"))
I have edited the code with this workaround.
Related
I am just about finished with program and need some help getting over the finish line. What is supposed to do is search through log files in a predefined directory. Nothing happens with the text I enter in the search box nor when I use the button that I created to execute the search. I have searched all over the Internet which has me as far as I am. I see lot's of examples in Winforms but I don't know the "translation" between Winforms and WPF.
I think that if someone can get me passed this hurdle I can implement the other features I want to add.
PS: I really have no programming skills, just tenacity.
Thanks in advance to all who help.
'''
# Import needed Assemblies
Add-Type -AssemblyName PresentationCore, PresentationFramework, WindowsBase,
System.Drawing, System.Windows.Forms, WindowsFormsIntegration
[void][System.Reflection.Assembly]::LoadWithPartialName('PresentationFramework')
[Windows.Forms.Application]::EnableVisualStyles()
#XAML form designed using Visual Studio
# Build the GUI
[xml]$Form = #"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Log Crawler"
HorizontalAlignment="Center"
FontSize="11" Height="700" Width="1300" FontFamily="Calibri"
WindowStartupLocation="CenterScreen"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Disabled" WindowStyle="ThreeDBorderWindow">
<Grid Margin="0,0,0,0">
<Grid.Background>
<LinearGradientBrush EndPoint="0.8,1" StartPoint="0.8,0">
<GradientStop Color="Black"/>
<GradientStop Color="White" Offset="1"/>
</LinearGradientBrush>
</Grid.Background>
<TextBox x:Name="IntegrationsLogCrawler" Text="Integration Log Crawler"
HorizontalAlignment="Center" VerticalAlignment="Top" TextAlignment="Center" TextWrapping="NoWrap"
Margin="0,20,0,0 " Height="37" Width="260"
FontSize="24" FontFamily="Calibri" FontWeight="Bold" FontStyle="Normal" FlowDirection="LeftToRight"/>
<GroupBox x:Name="SearchBox" Header="SearchBox"
HorizontalAlignment="Center" VerticalAlignment="Top"
FontSize="12" FontFamily="Calibri" FontWeight="Bold" Foreground="#FFE4E41A"
Margin="0,100,0,0" Height="40" Width="400">
<TextBox x:Name="InputBox" TextWrapping="NoWrap"/>
</GroupBox>
<Button x:Name="LogCrawl" Content="Log Crawl"
HorizontalAlignment="Center" VerticalAlignment="Top"
Margin="0,150,0,0" Height="40" Width="190"
FontSize="18" FontFamily="Calibri" FontWeight="Bold" Foreground="#FFEE0C3F"/>
<Button x:Name="Exit" Content="Exit"
HorizontalAlignment="Center" VerticalAlignment="Top"
Margin="0,200,0,0" Height="39" Width="110"
FontSize="16" FontFamily="Calibri" FontWeight="Bold" Foreground="#FFE4E41A" Background="#FF040404"/>
<GroupBox x:Name="Results" Header="Results"
HorizontalAlignment="Center" VerticalAlignment="Top"
FontSize="12" FontFamily="Calibri" FontWeight="Bold" Foreground="#FFE4E41A"
Margin="0,250,0,0 " Height="250" Width="1000">
<TextBox x:Name="OutputBox" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"/>
</GroupBox>
</Grid>
</Window>
"#
#Create a form
$XMLReader = (New-Object System.Xml.XmlNodeReader $Form)
$XMLForm = [Windows.Markup.XamlReader]::Load($XMLReader)
#Connect to Controls
$xaml.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach {
New-Variable -Name $_.Name -Value $XMLForm.FindName($_.Name) -Force
}
# Load Log Crawl Menu Button Controls
$InputBox = $XMLForm.FindName('InputBox')
$OutputBox = $XMLForm.FindName('OutputBox')
$LogCrawl = $XMLForm.FindName('LogCrawl')
$Exit = $XMLForm.FindName('Exit')
# Logfile Location
$LogFiles = "$($ENV:USERPROFILE)\Documents\_data\logfiles"
# Log Crawl Button Action
$LogCrawl1.Add_Click({
Select-String -Path "$LogFiles\*.*" -Pattern $InputBox -Context 10, 20 | Write-Host
})
# EXIT
$Exit.Add_Click({
$XMLForm.Close()
})
#Show XMLform
$null = $XMLForm.ShowDialog()
'''
I'm not sure about the workings of this part (gave me exception "You cannot call a method on a null-valued expression.") :
#Connect to Controls
$xaml.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach {
New-Variable -Name $_.Name -Value $XMLForm.FindName($_.Name) -Force
}
and I think you should remove it.
Then you made a typo in the button variable name $LogCrawl1 --> $LogCrawl, but also change the Add_Click() method to:
# Log Crawl Button Action
$LogCrawl.Add_Click({
# Logfile Location
$LogFiles = "$($ENV:USERPROFILE)\Documents\_data\logfiles"
$OutputBox.Text = Select-String -Path "$LogFiles\*.*" -Pattern $InputBox.Text -SimpleMatch -Context 10, 20
})
P.S. It is unclear if this is also in your real code, but the final closing "# for the XAML here-string must not have whitespace to the left.
I'm trying to make a remote copy tool, which works fine in the console but I'm having trouble creating the GUI for it.
When I populate my Listbox with the list of files to copy they just don't display as intended...
My problem is in the ChooseFiles function
#xaml Code
$inputXML = #"
<Window x:Class="RemoteCopy.MainWindow"
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:RemoteCopy"
mc:Ignorable="d"
Title="Remote Copy" Height="556.841" Width="800">
<Grid>
<Label Content="Remote Computer's IP:" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
<TextBox x:Name="RemoteIPTextbox" HorizontalAlignment="Left" Height="20" Margin="238,16,0,0" TextWrapping="Wrap" Text="IP address" VerticalAlignment="Top" Width="128"/>
<Label Content="Username: " HorizontalAlignment="Left" Height="26" Margin="10,40,0,0" VerticalAlignment="Top" Width="131"/>
<Label Content="Password: " HorizontalAlignment="Left" Height="26" Margin="10,70,0,0" VerticalAlignment="Top" Width="131"/>
<TextBox x:Name="UsernameTextbox" HorizontalAlignment="Left" Height="20" Margin="238,46,0,0" TextWrapping="Wrap" Text="Username" VerticalAlignment="Top" Width="128"/>
<Button x:Name="CheckConnectionButton" Content="Check Connection" HorizontalAlignment="Left" Margin="389,46,0,0" VerticalAlignment="Top" Width="107" Height="20"/>
<Button x:Name="ChooseFilesButton" Content="Choose Files" HorizontalAlignment="Left" Height="80" Margin="535,16,0,0" VerticalAlignment="Top" Width="122"/>
<ListView x:Name="FilesListView" HorizontalAlignment="Left" Height="234" Margin="33,128,0,0" VerticalAlignment="Top" Width="720">
<ListView.View>
<GridView>
<GridViewColumn Header="File"/>
<GridViewColumn Header="Size"/>
</GridView>
</ListView.View>
</ListView>
<Button x:Name="StartCopyButton" Content="Start Copy" HorizontalAlignment="Left" Height="24" Margin="33,374,0,0" VerticalAlignment="Top" Width="720"/>
<ProgressBar x:Name="ProgressBar" HorizontalAlignment="Left" Height="20" Margin="113,418,0,0" VerticalAlignment="Top" Width="572"/>
<Label Content="Progress:" HorizontalAlignment="Left" Height="28" Margin="33,418,0,0" VerticalAlignment="Top" Width="67"/>
<Label Content="100%" HorizontalAlignment="Left" Height="28" Margin="714,418,0,0" VerticalAlignment="Top" Width="39"/>
<PasswordBox x:Name="Passwordbox" HorizontalAlignment="Left" Margin="238,78,0,0" VerticalAlignment="Top" Width="128"/>
<Button x:Name="ExitButton" Content="Exit" HorizontalAlignment="Left" Height="21" Margin="283,455,0,0" VerticalAlignment="Top" Width="213"/>
</Grid>
</Window>
"#
$inputXML = $inputXML -replace 'mc:Ignorable="d"','' -replace "x:N",'N' -replace '^<Win.*', '<Window'
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
[xml]$XAML = $inputXML
#Read XAML
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
try{
$Form=[Windows.Markup.XamlReader]::Load( $reader )
}
catch{
Write-Warning "Unable to parse XML, with error: $($Error[0])`n Ensure that there are NO SelectionChanged or TextChanged properties in your textboxes (PowerShell cannot process them)"
throw
}
#===========================================================================
# Load XAML Objects In PowerShell
#===========================================================================
$xaml.SelectNodes("//*[#Name]") | %{"trying item $($_.Name)";
try {Set-Variable -Name "WPF$($_.Name)" -Value $Form.FindName($_.Name) -ErrorAction Stop}
catch{throw}
}
Function Get-FormVariables{
if ($global:ReadmeDisplay -ne $true){Write-host "If you need to reference this display again, run Get-FormVariables" -ForegroundColor Yellow;$global:ReadmeDisplay=$true}
write-host "Found the following interactable elements from our form" -ForegroundColor Cyan
get-variable WPF*
}
Get-FormVariables
#===========================================================================
# Use this space to add code to the various form elements in your GUI
#===========================================================================
#my test folder
c:
Set-Location c:\temp
function DisplayInBytes($num)
#reference: https://stackoverflow.com/questions/24616806/powershell-display-files-size-as-kb-mb-or-gb
{
$suffix = "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"
$index = 0
while ($num -gt 1kb)
{
$num = $num / 1kb
$index++
}
"{0:N1} {1}" -f $num, $suffix[$index]
}
function ChooseFiles
{
$WPFFilesListView.Clear()
$filesToCopy = Get-Files
$filesToCopy.FileNames | ForEach-Object {
$currentfilesize = (Get-Item $_).Length
$currentfilesize = DisplayInBytes -num $currentfilesize
$row = New-Object System.Windows.Forms.ListViewItem($_)
[void]$row.SubItems.Add("$currentfilesize")
[void]$WPFFilesListView.Items.Add($row)
}
}
Function Get-Files($initialDirectory="")
#Reference to https://stackoverflow.com/questions/15885132/file-folder-chooser-dialog-from-a-windows-batch-script
{
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")|Out-Null
$myFile = new-object Windows.Forms.OpenFileDialog
$myFile.InitialDirectory = Get-Location
$myFile.Filter = "Text Files (*.txt)|*.txt|Powershell Script Files (*.ps1)|*.ps1|Batch Files (*.bat)|*.bat|All Files (*.*)|*.*"
$myFile.ShowHelp = $true
$myFile.Multiselect = $true
[void]$myfile.ShowDialog()
if ($myFile.Multiselect) { $myFile.FileNames } else { $myFile.FileName }
return $myFile
}
$WPFExitButton.Add_Click({$form.close()})
$WPFChooseFilesButton.Add_Click({ChooseFiles})
$Form.ShowDialog() | out-null
The Function to Choose my Files and Populate my Listbox is this one:
function ChooseFiles
{
$WPFFilesListView.Clear()
$filesToCopy = Get-Files
$filesToCopy.FileNames | ForEach-Object {
$currentfilesize = (Get-Item $_).Length
$currentfilesize = DisplayInBytes -num $currentfilesize
$row = New-Object System.Windows.Forms.ListViewItem($_)
[void]$row.SubItems.Add("$currentfilesize")
[void]$WPFFilesListView.Items.Add($row)
}
}
But my Result looks like this:
I intended to have the Filename and Filesize displayed,
If I test the Variables they work
Write-host $_
write-host $currentfilesize
One in each column
You need to change two things to fix the display:
Add a DisplayMemberBinding to the ListView
<ListView x:Name="FilesListView" HorizontalAlignment="Left" Height="234" Margin="33,128,0,0" VerticalAlignment="Top" Width="720">
<ListView.View>
<GridView>
<GridViewColumn Header="File" DisplayMemberBinding="{Binding File}"/>
<GridViewColumn Header="Size" DisplayMemberBinding="{Binding Size}"/>
</GridView>
</ListView.View>
</ListView>
Pass a pscustomobject with the specified binding properties to the ListView
function ChooseFiles
{
$WPFFilesListView.Clear()
$filesToCopy = Get-Files
$filesToCopy.FileNames | ForEach-Object {
$currentfilesize = (Get-Item $_).Length
$currentfilesize = DisplayInBytes -num $currentfilesize
$WPFFilesListView.Items.Add([pscustomobject]#{File=$_;Size=$currentfilesize})
}
}
Quick one. I want to add Add_DoubleClick() to ListBox in PowerShell, but this doesn't work with implemented ListBox via XAML file... and I'm stuck
I will be very grateful for any help here...
I still have msg warning, that my post is mostly code, so I need to write something here...
# $ErrorActionPreference= 'silentlycontinue'
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase, System.Windows.Forms, System.Drawing
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
$AssemblyLocation = Join-Path -Path $ScriptPath -ChildPath .\themes
foreach ($Assembly in (Dir $AssemblyLocation -Filter *.dll)) {
[System.Reflection.Assembly]::LoadFrom($Assembly.fullName) | out-null
}
[xml]$xaml = #"
<Controls:MetroWindow
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:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
Title="ddd"
Height="500"
Width="800"
BorderThickness="0"
GlowBrush="{DynamicResource AccentColorBrush}"
ResizeMode="CanResizeWithGrip"
WindowStartupLocation="CenterScreen">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Colors.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Accents/Cyan.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.AnimatedTabControl.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Accents/BaseLight.xaml" />
</ResourceDictionary.MergedDictionaries>
</Window.Resources>
<Grid>
<ListBox SelectionMode="Single" ItemsSource="{Binding}" x:Name="ListBox" HorizontalAlignment="Right" Height="100" Margin="244,144,0,0" VerticalAlignment="Top" Width="112">
<ListBoxItem Content="Coffie"></ListBoxItem>
</ListBox>
</Grid>
</Controls:MetroWindow>
"#
#Read the form
$Reader = (New-Object System.Xml.XmlNodeReader $xaml)
$Form = [Windows.Markup.XamlReader]::Load($reader)
#AutoFind all controls
$xaml.SelectNodes("//*[#*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object {
New-Variable -Name $_.Name -Value $Form.FindName($_.Name) -Force
}
$ListBox.Items.AddRange()
$ListBox.Add_DoubleClick({
$TextBox2.AppendText("dddd`r`n")
})
...`
I used different approach to my problem:
$smth.Add_IsMouseCapturedChanged({
$smth.SelectedIndex = -1
})
Im a beginner and is needing help running an XAML into powershell.
Im trying to follow some format from the net but cant seem to convert my xaml GUI to powershell.
Any help will do. Heres my code:
<Window x:Class="WpfApp3.MainWindow"
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:WpfApp3"
mc:Ignorable="d"
Title="Strike Email" Height="137" Width="274" Background="White">
<Grid Background="#FF005D80">
<Button Content="1st Strike" HorizontalAlignment="Left" Height="50" Margin="10,30,0,0" VerticalAlignment="Top" Width="70" Background="White" Click="Button_Click"/>
<Button Content="3rd Strike" HorizontalAlignment="Left" Height="50" Margin="180,30,0,0" VerticalAlignment="Top" Width="70" Background="White"/>
<Button Content="2nd Strike" HorizontalAlignment="Left" Height="50" Margin="95,30,0,0" VerticalAlignment="Top" Width="70" Background="White"/>
</Grid>
For that, you will need to:
Cast your xaml string as a xml
Load presentationframework assembly
Load the XAML into a xmlNodeReader
Load the form
Powershell must be running in STA (Single threaded) mode for this to work
(Powershell ISE launch in STA mode by default.)
[xml]$xaml = 'ValidXAMLStringHere'
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Form=[Windows.Markup.XamlReader]::Load( $reader )
$Form.ShowDialog() | out-null
Something like the snippet would be the end result.
I even added a button click event for fun.
[xml]$xaml = #'
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Strike Email" Height="137" Width="274" Background="White">
<Grid Background="#FF005D80">
<Button Content="1st Strike" HorizontalAlignment="Left" Height="50" Margin="10,30,0,0" VerticalAlignment="Top" Width="70" Background="White" x:Name="FirstStrike" />
<Button Content="3rd Strike" HorizontalAlignment="Left" Height="50" Margin="180,30,0,0" VerticalAlignment="Top" Width="70" Background="White"/>
<Button Content="2nd Strike" HorizontalAlignment="Left" Height="50" Margin="95,30,0,0" VerticalAlignment="Top" Width="70" Background="White"/>
</Grid>
</Window>
'#
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
try
{
$Form=[Windows.Markup.XamlReader]::Load( $reader )
}
catch
{
Write-Host "Unable to load Windows.Markup.XamlReader. Some possible causes for this problem include: .NET Framework is missing PowerShell must be launched with PowerShell -sta, invalid XAML code was encountered."t
}
cls
$Button = $Form.FindName('FirstStrike')
$Button.Add_Click({ Write-Host "First Strike Button Clicked" -ForegroundColor Cyan})
$Form.ShowDialog() | out-null
References:
Technet - Integrating XAML into PowerShell
Learn Powershell - PowerShell and WPF: Buttons
I am trying to implement background commands that would initiate after the user clicks a button, and so far I have always ended up with my UI locking up while the job is on-going. I am currently using a somewhat time consuming for loop to check if my UI locks up. What can I do to let the job work in the background as the UI is freed up. I am not sure I want to complicate the code by adding runspaces. What am I doing incorrectly?
$inputXML = #"
<Window x:Class="WpfApplication2.MainWindow"
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:WpfApplication2"
mc:Ignorable="d"
Title="Create User" Height="448.05" Width="656.017" ResizeMode="NoResize">
<Grid Margin="0,0,-6.8,-0.8" Background="#FFD7D7D7">
<Grid.RowDefinitions>
<RowDefinition Height="403*"/>
<RowDefinition Height="18*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button x:Name="Create_User" Content="Create User" HorizontalAlignment="Left" Margin="67,224,0,0" VerticalAlignment="Top" Width="300" Height="26" RenderTransformOrigin="0.551,-0.671" IsEnabled="True">
<Button.Background>
<LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#FFF3F3F3" Offset="0"/>
<GradientStop Color="#FFEBEBEB" Offset="0.5"/>
<GradientStop Color="#FFDDDDDD" Offset="0.5"/>
<GradientStop Color="#FFCDCDCD" Offset="1"/>
</LinearGradientBrush>
</Button.Background>
</Button>
<Label x:Name="fname" Content="" HorizontalAlignment="Left" Margin="43,11,0,0" VerticalAlignment="Top" Height="26" Width="10"/>
<Label x:Name="fname1" Content="First Name" HorizontalAlignment="Left" Margin="88,54,0,0" VerticalAlignment="Top" Width="72" RenderTransformOrigin="0.513,1.469" Height="26"/>
<Label x:Name="lname" Content="Last Name" HorizontalAlignment="Left" Margin="88,83,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.167,-0.458" Width="72" Height="25"/>
<TextBox x:Name="fnametxt" Height="23" Margin="167,54,0,0" TextWrapping="Wrap" VerticalAlignment="Top" RenderTransformOrigin="0.501,0.452" HorizontalAlignment="Left" Width="136"/>
<Button x:Name="exitbtn" Content="Exit" HorizontalAlignment="Left" VerticalAlignment="Top" Width="135" Margin="447,365,0,0" Height="38" RenderTransformOrigin="0.489,0.462"/>
<Label x:Name="label" Content="Password" HorizontalAlignment="Left" Margin="92,113,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.064,0.601" Height="26" Width="68"/>
<PasswordBox x:Name="passwordBox" Margin="167,113,0,0" VerticalAlignment="Top" Height="24" HorizontalAlignment="Left" Width="136"/>
<TextBox x:Name="stsbox" HorizontalAlignment="Left" Height="62" Margin="67,267,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="300" Background="#FFDADADA" Foreground="Black" Opacity="0.45" SelectionBrush="#FF1D6EBF" RenderTransformOrigin="0.503,-0.59" IsReadOnly="True"/>
<TextBox x:Name="lnametxt" Height="23" Margin="167,85,0,0" TextWrapping="Wrap" VerticalAlignment="Top" HorizontalAlignment="Left" Width="136"/>
</Grid>
</Window>
"#
$inputXML = $inputXML -replace 'mc:Ignorable="d"','' -replace "x:N",'N' -replace '^<Win.*', '<Window'
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[xml]$xaml = $inputXML
#Read XAML
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
try{$Form=[Windows.Markup.XamlReader]::Load( $reader )}
catch{Write-Host "Unable to load Windows.Markup.XamlReader. Double-check syntax and ensure .net is installed."}
$xaml.SelectNodes("//*[#Name]") | %{Set-Variable -Name "WPF$($_.Name)" -Value $Form.FindName($_.Name) -Scope Global}
Function global:Get-FormVariables{
if ($global:ReadmeDisplay -ne $true) {$global:ReadmeDisplay=$true}
#write-host "Found the following interactable elements from our form" -ForegroundColor Cyan
get-variable WPF*
}
Get-FormVariables
$WPFCreate_User.Add_Click({
$test = { for($i=1; $i -le 100000; $i++){
$z = $i + $z
$z
}}
$job = Start-Job $test
Wait-Job $job
Receive-Job $job -OutVariable results
Remove-Job $job
$WPFstsbox.text = "$results`n"
})
$WPFexitbtn.add_click({
$form.Close() | out-null
exit})
$form.ShowDialog() | Out-null
Yes, PSJobs are truly "background" jobs.
When you call Start-Job, a separate process is started, executing your job/command.
In your script, the job itself doesn't block the calling thread, but your subsequent command (Wait-Job $job) does.
If you simply fire off Start-Job and return from the Click handler, your UI wouldn't lock up:
$button.add_Click({
$jobCode = { Start-Sleep -Seconds 10 }
$job = Start-Job $jobCode
})
you'll see that the UI becomes responsive again in way lees than 10 seconds.
The problem is that you no longer have access to the $job and no longer control over when it is to be displayed.
To compensate for this, you need background thread or a timed event that can periodically check up on the job(s) for you and output the result.
You could use a Timer and PowerShell's builtin eventing infrastructure for this:
# Create timer
$backgroundTimer = New-Object System.Timers.Timer
# Set interval (ms)
$backgroundTimer.Interval = 500
# Make sure timer stops raising events after each interval
$backgroundTimer.AutoReset = $false
# Have powershell "listen" for the event in the background
Register-ObjectEvent $backgroundTimer -EventName 'Elapsed' -SourceIdentifier 'timerElapsed' -Action {
# Loop through any completed jobs with data to return, from "oldest" to "newest"
while(($finishedJob = Get-Job |Where-Object {$_.State -eq 'Completed' -and $_.HasMoreData}|Select-Object -First 1))
{
# Update the text box on your WPF form with the results
# NOTE: the event is executed in a separate scope, thus the "global:" scope prefix
$global:WPFstsbox.Text = "$(Receive-Job $finishedJob)`n"
# Clean up the job
Remove-Job $finishedJob
}
# Restart timer
$backgroundTimer.Start()
}
# Enable the timer
$backgroundTimer.Enabled = $true