Checking what closed your Windows Form in Powershell - forms

Was wondering if there is a way to check what event closed the window, pretty much either clicking the red x in the top corner or if $form.Close() was called?
Each will automatically initiate the $form.Add_Closing({}) if I have it in my script, but I wanted to know which way of closing the window did this.

The FormClosing event argument object's .CloseReason property doesn't allow you to distinguish between the .Close() method having been called on the form and the user closing the form via the title bar / window system menu / pressing Alt+F4 - all these cases equally result in the .CloseReason property reflecting enumeration value UserClosing.
However, you can adapt the technique from Reza Aghaei's helpful C# answer on the subject, by inspecting the call stack for a call to a .Close() method:
using assembly System.Windows.Forms
using namespace System.Windows.Forms
using namespace System.Drawing
# Create a sample form.
$form = [Form] #{
ClientSize = [Point]::new(400,100)
Text = 'Closing Demo'
}
# Create a button and add it to the form.
$form.Controls.AddRange(#(
($btnClose = [Button] #{
Text = 'Close'
Location = [Point]::new(160, 60)
})
))
# Make the button call $form.Close() when clicked.
$btnClose.add_Click({
$form.Close()
})
# The event handler called when the form is closing.
$form.add_Closing({
# Look for a call to a `.Close()` method on the call stack.
if ([System.Diagnostics.StackTrace]::new().GetFrames().GetMethod().Name -ccontains 'Close') {
Write-Host 'Closed with .Close() method.'
} else {
Write-Host 'Closed via title bar / Alt+F4.'
}
})
$null = $form.ShowDialog() # Show the form modally.
$form.Dispose() # Dispose of the form.
If you run this code and try various methods of closing the form, a message indicating the method used should print (.Close() call vs. title bar / Alt+F4).
Note:
Closing the form via buttons assigned to the form's .CancelButton and .SubmitButton properties that don't have explicit $form.Close() calls still causes .Close() to be called behind the scenes.
The code requires PowerShell v5+, but it can be adapted to earlier versions.

Checking call stack works fine and you can rely on it.
Just for the sake of completeness, specially for cases that you find a C# example and you want to use it in PowerShell in an easy way, I'll share an example showing how you can handle WM_SYSCOMMAND as shown in my linked post, in PowerShell.
using assembly System.Windows.Forms
using namespace System.Windows.Forms
using namespace System.Drawing
# Create the C# derived Form
$assemblies = "System.Windows.Forms", "System.Drawing"
$code = #'
using System;
using System.Windows.Forms;
public class MyForm:Form
{
public bool ClosedByXButtonOrAltF4 { get; private set;}
public const int SC_CLOSE = 0xF060;
public const int WM_SYSCOMMAND = 0x0112;
protected override void WndProc(ref Message msg)
{
if (msg.Msg == WM_SYSCOMMAND && msg.WParam.ToInt32() == SC_CLOSE)
ClosedByXButtonOrAltF4 = true;
base.WndProc(ref msg);
}
protected override void OnShown(EventArgs e)
{
ClosedByXButtonOrAltF4 = false;
}
}
'#
Add-Type -ReferencedAssemblies $assemblies -TypeDefinition $code -Language CSharp
# Create an instance of MyForm.
$form = [MyForm] #{
ClientSize = [Point]::new(400,100)
Text = "Closing Demo"
}
# Create a button and add it to the form.
$form.Controls.AddRange(#(
($btnClose = [Button] #{
Text = "Close"
Location = New-Object System.Drawing.Point 160, 60
})
))
# Make the button call $form.Close() when clicked.
$btnClose.add_Click({
$form.Close()
})
# The event handler called when the form is closing.
$form.add_Closing({
if ($form.ClosedByXButtonOrAltF4) {
Write-Host 'Closed via title bar / Alt+F4.'
} else {
Write-Host 'Closed with .Close() method.'
}
})
$null = $form.ShowDialog()
$form.Dispose()

Related

Using Windows Forms in powershell class

I'm trying to make my current Powershell code a little cleaner by using classes. I'm trying to make a Window class using Windows.Forms but I seem to be having trouble with it. This is what I currently have so far:
Window.psm1
using assembly System.Windows.Forms;
class Window {
[System.Windows.Forms.Form]$Form;
Window() {
$this.Form = New-Object System.Windows.Forms.Form;
}
[void]BuildWindow() {
$this.Form.ClientSize = New-Object System.Drawing.Point(600, 400)
$this.Form.Text = "Orion"
$this.Form.TopMost = $false
$this.Form.FormBorderStyle = "Fixed3D"
$this.Form.MaximizeBox = $false
}
[void]ShowWindow() {
$this.Form.ShowDialog();
}
}
I'm then instantiating it in my start.ps1:
using module ".\Window.psm1";
$WindowObject = New-Object Window;
$WindowObject.BuildWindow();
$WindowObject.ShowWindow();
However, I'm getting the following error:
At C:\Orion\scripts\Window.psm1:4 char:6
+ [System.Windows.Forms.Form]$Form;
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
Unable to find type [System.Windows.Forms.Form].
+ CategoryInfo : InvalidOperation: (C:\Orion\scripts\Window.psm1:String) [], ParentContainsErrorRecordExc
eption
+ FullyQualifiedErrorId : TypeNotFound
I'm new to Powershell scripting and classes especially, so I'm not sure what is going wrong here.
To clarify, your code as-is, works perfectly fine in a newer version of PowerShell (tested on the latest, 7.2.3). The issue is exclusively on Windows PowerShell, and to be honest, it's hard to explain why the error occurs however I can provide you what worked for me on that version.
using assembly System.Windows.Forms
class Window {
[System.Windows.Forms.Form] $Form = [System.Windows.Forms.Form]::new()
Window() { } # Removing the instantiation of the form from the ctor
[void] BuildWindow() {
$this.Form.ClientSize = [System.Drawing.Point]::new(600, 400)
$this.Form.Text = "Orion"
$this.Form.TopMost = $false
$this.Form.FormBorderStyle = "Fixed3D"
$this.Form.MaximizeBox = $false
}
[void] ShowWindow() {
$this.Form.ShowDialog();
}
}
On a personal note, I don't think having classes is a good idea when working with Windows Forms, it will make your code harder to read and by the looks of it, each form control will have their own hard-coded values which will be very hard to maintain and very hard to debug.
# would make more sense:
$form = [System.Windows.Forms.Form]#{
ClientSize = [System.Drawing.Point]::new(600, 400)
Text = "Orion"
TopMost = $false
FormBorderStyle = "Fixed3D"
MaximizeBox = $false
}
$form.ShowDialog()

Open the same Form again after Closing

Either I'm too stupid to google it properly or the problem is too obvious to solve.
I'm using a second small form to display a progressbar when running a function (Loading some information takes some time and this adds some nice responsiveness)
After the function finished and data is retreived, the progressbar-form is Closed with $formbar.Close()
If I call the function in the same instance again the progressbar wont show again because it was already disposed. How do I change that? I'd prefer not to "hide" the progressbar.
$formbar = New-Object System.Windows.Forms.Form
$progressBar1 = New-Object System.Windows.Forms.ProgressBar
$formbar.ControlBox = $false
$formbar.Size = '265,45'
$formbar.StartPosition = "CenterScreen"
$progressBar1.Style = "Continuous"
$progressBar1.ForeColor = "#009374"
$ProgressRange = 1..100
$ProgressMinMax = $ProgressRange | Measure -Minimum -Maximum
$progressBar1.Location = '0,0'
$progressBar1.Size = '250,30'
$progressBar1.Visible = $True
$progressBar1.Minimum = $ProgressMinMax.Minimum
$progressBar1.Maximum = $ProgressMinMax.Maximum
$progressBar1.Step = 10
$formbar.Controls.Add($progressBar1)
$formbar.Show()
Any ideas?
You need to re-create the whole form every time:
function New-ProgressBarForm {
$null = . {
$formbar = New-Object System.Windows.Forms.Form
$progressBar1 = New-Object System.Windows.Forms.ProgressBar
$formbar.ControlBox = $false
$formbar.Size = '265,45'
$formbar.StartPosition = "CenterScreen"
$progressBar1.Style = "Continuous"
$progressBar1.ForeColor = "#009374"
$ProgressRange = 1..100
$ProgressMinMax = $ProgressRange | Measure -Minimum -Maximum
$progressBar1.Location = '0,0'
$progressBar1.Size = '250,30'
$progressBar1.Visible = $True
$progressBar1.Minimum = $ProgressMinMax.Minimum
$progressBar1.Maximum = $ProgressMinMax.Maximum
$progressBar1.Step = 10
$formbar.Controls.Add($progressBar1)
}
return [pscustomobject]#{
Form = $formbar
ProgressBar = $progressBar1
}
}
Then call:
$progress = New-ProgressBarForm
$progress.Form.Show()
When you want to display it
Mathias answered the question, but you also asked why, so here's why.
When the user closes your form, either by the form being dismissed using the X or Close button, or when the Form.Close() method is closed, the following takes place:
When a form is closed, all resources created within the object are closed and the form is disposed. You can prevent the closing of a form at run time by handling the Closing event and setting the Cancel property of the CancelEventArgs passed as a parameter to your event handler. If the form you are closing is the startup form of your application, your application ends.
We can tell if its handles are still available by looking at the object's IsDisposed property.
#before showing
PS> $formBar.IsDisposed
False
PS> $formBar.Show()
PS> $formBar.IsDisposed
True
TLDR: it has to do with memory management. Once a form is shown and then closed, its gone from memory, but the variables it touched will forever remain in our hearts.

Class based streamReader & xmlReader locks files, function doesn't

I am refactoring some function based XML reader code to class methods, and seeing some issues. With the function, I can run a test and verify the XML loaded right, then change the XML and test for error conditions. But this class based approach fails due to "the file is open in another program", forcing me to close the console before I can revise the XML.
Initially I was using the path directly in the xmlReader. So I moved to a StreamReader input to the xmlReader. And I even played with creating an all new xmlDocument and importing the root node of the loaded XML into that new xmlDocument. None works.
I suspect the reason the function based version works is because the xmlReader variable is local scope, so it goes out of scope when the function completes. But I'm grasping at straws there. I also read that Garbage Collection could be an issue, so I added [system.gc]::Collect() right after the Dispose and still no change.
class ImportXML {
# Properties
[int]$status = 0
[xml.xmlDocument]$xml = ([xml.xmlDocument]::New())
[collections.arrayList]$message = ([collections.arrayList]::New())
# Methods
[xml.xmlDocument] ImportFile([string]$path) {
$importError = $false
$importFile = ([xml.xmlDocument]::New())
$xmlReaderSettings = [xml.xmlReaderSettings]::New()
$xmlReaderSettings.ignoreComments = $true
$xmlReaderSettings.closeInput = $true
$xmlReaderSettings.prohibitDtd = $false
try {
$streamReader = [io.streamReader]::New($path)
$xmlreader = [xml.xmlreader]::Create($streamReader, $xmlReaderSettings)
[void]$importFile.Load($xmlreader)
$xmlreader.Dispose
$streamReader.Dispose
} catch {
$exceptionName = $_.exception.GetType().name
$exceptionMessage = $_.exception.message
switch ($exceptionName) {
Default {
[void]$this.message.Add("E_$($exceptionName): $exceptionMessage")
$importError = $true
}
}
}
if ($importError) {
$importFile = $null
}
return $importFile
}
}
class SettingsXML : ImportXML {
# Constructor
SettingsXML([string]$path){
if ($this.xml = $this.ImportFile($path)) {
Write-Host "$path!"
} else {
Write-Host "$($this.message)"
}
}
}
$settingsPath = '\\Mac\iCloud Drive\Px Tools\Dev 4.0\Settings.xml'
$settings = [SettingsXML]::New($settingsPath)
EDIT:
I also tried a FileStream rather than a StreamReader, with FileShare of ReadWrite, like so
$fileMode = [System.IO.FileMode]::Open
$fileAccess = [System.IO.FileAccess]::Read
$fileShare = [System.IO.FileShare]::ReadWrite
$fileStream = New-Object -TypeName System.IO.FileStream $path, $fileMode, $fileAccess, $fileShare
Still no luck.
I think you're on the right lines with Dispose, but you're not actually invoking the method - you're just getting a reference to it and then not doing anything with it...
Compare:
PS> $streamReader = [io.streamReader]::New(".\test.xml");
PS> $streamReader.Dispose
OverloadDefinitions
-------------------
void Dispose()
void IDisposable.Dispose()
PS> _
with
PS> $streamReader = [io.streamReader]::New(".\test.xml");
PS> $streamReader.Dispose()
PS> _
You need to add some () after the method name so your code becomes:
$xmlreader.Dispose()
$streamReader.Dispose()
And then it should release the file lock ok.

Get primalforms dialog box results in powershell

From a PS script, I am calling a primalforms dialog box err.ps1, but I don't know how to return to the main script which button the users clicked on err.ps1 (Yes/No).
if (.{.\errDestination.ps1})
Doesn't seem to return the value being true as yes or false as no...
Any idea where should I set the return result?
You have to edit the generated code a bit to return the DialogResult. I put this at the bottom:
#endregion Generated Form Code
# ....
#Show the Form
$form1.ShowDialog()| Out-Null
return $form1.DialogResult
} #End Function
#Call the Function
return GenerateForm
#endregion
To evaluate it form the calling script:
$result = & .\errDestination.ps1
if ($result -eq "Yes") {
# Yes
} else {
# No
}

How to troubleshoot a 'System.Management.Automation.CmdletInvocationException'

Does anyone know how best to determine the specific underlying cause of this exception?
Consider a WCF service that is supposed to use Powershell 2.0 remoting to execute MSBuild on remote machines. In both cases the scripting environments are being called in-process (via C# for Powershell and via Powershell for MSBuild), rather than 'shelling-out' - this was a specific design decision to avoid command-line hell as well as to enable passing actual objects into the Powershell script.
An abridged version of the Powershell script that calls MSBuild is shown below:
function Run-MSBuild
{
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Build.Engine")
$engine = New-Object Microsoft.Build.BuildEngine.Engine
$engine.BinPath = "C:\Windows\Microsoft.NET\Framework\v3.5"
$project = New-Object Microsoft.Build.BuildEngine.Project($engine, "3.5")
$project.Load("deploy.targets")
$project.InitialTargets = "DoStuff"
# Process the input object
while ($input.MoveNext())
{
# Set MSBuild Properties & Item
}
# Optionally setup some loggers (have also tried it without any loggers)
$consoleLogger = New-Object Microsoft.Build.BuildEngine.ConsoleLogger
$engine.RegisterLogger($consoleLogger)
$fileLogger = New-Object Microsoft.Build.BuildEngine.FileLogger
$fileLogger.Parameters = "verbosity=diagnostic"
$engine.RegisterLogger($fileLogger)
# Run the build - this is the line that throws a CmdletInvocationException
$result = $project.Build()
$engine.Shutdown()
}
When running the above script from a PS command prompt it all works fine. However, as soon as the script is executed from C# it fails with the above exception.
The C# code being used to call Powershell is shown below (remoting functionality removed for simplicity's sake):
// Build the DTO object that will be passed to Powershell
dto = SetupDTO()
RunspaceConfiguration runspaceConfig = RunspaceConfiguration.Create();
using (Runspace runspace = RunspaceFactory.CreateRunspace(runspaceConfig))
{
runspace.Open();
IList errors;
using (var scriptInvoker = new RunspaceInvoke(runspace))
{
// The Powershell script lives in a file that gets compiled as an embedded resource
TextReader tr = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream("MyScriptResource"));
string script = tr.ReadToEnd();
// Load the script into the Runspace
scriptInvoker.Invoke(script);
// Call the function defined in the script, passing the DTO as an input object
var psResults = scriptInvoker.Invoke("$input | Run-MSBuild", dto, out errors);
}
}
NOTE: The overload of the Invoke() method allows you to pass in an IEnumerable object and it takes care of instantiating an enumerator to in the Powershell variable '$input' - this then gets passed into the script via the pipeline. Here are some supporting links:
http://msdn.microsoft.com/en-us/library/ms569104(VS.85).aspx
http://knicksmith.blogspot.com/2007/03/managing-exchange-2007-recipients-with.html (jump to the 'Passing an Input Object to the Runspace' section)
Assuming that the issue was related to MSBuild outputting something that the Powershell runspace can't cope with, I have also tried the following variations to the second .Invoke() call:
var psResults = scriptInvoker.Invoke("$input | Run-MSBuild | Out-String", dto, out errors);
var psResults = scriptInvoker.Invoke("$input | Run-MSBuild | Out-Null", dto, out errors);
var psResults = scriptInvoker.Invoke("Run-MSBuild | Out-String");
var psResults = scriptInvoker.Invoke("Run-MSBuild | Out-String");
var psResults = scriptInvoker.Invoke("Run-MSBuild | Out-Null");
var psResults = scriptInvoker.Invoke("Run-MSBuild");
Note how the underlying issue still occurs irrespective of whether an input object is used.
I've also looked at using a custom PSHost (based on this sample: http://blogs.msdn.com/daiken/archive/2007/06/22/hosting-windows-powershell-sample-code.aspx), but during debugging I was unable to see any 'interesting' calls to it being made.
Do the great and the good of Stackoverflow have any insight that might save my sanity?
I can get the following code to work but I get a warning that MSBUILD engine wants to be run on a STA thread. Unfortunately the thread created by the PowerShell engine to execute the script is MTA. That said, my little sample works:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;
using System.Collections;
namespace RunspaceInvokeExp
{
class Program
{
static void Main()
{
var script = #"
function Run-MSBuild
{
[System.Reflection.Assembly]::LoadWithPartialName(""Microsoft.Build.Engine"")
$engine = New-Object Microsoft.Build.BuildEngine.Engine
$engine.BinPath = ""C:\Windows\Microsoft.NET\Framework\v3.5""
$project = New-Object Microsoft.Build.BuildEngine.Project($engine, ""3.5"")
$project.Load(""deploy.targets"")
$project.InitialTargets = ""DoStuff""
# Process the input object
while ($input.MoveNext())
{
# Set MSBuild Properties & Item
}
# Optionally setup some loggers (have also tried it without any loggers)
$consoleLogger = New-Object Microsoft.Build.BuildEngine.ConsoleLogger
$engine.RegisterLogger($consoleLogger)
$fileLogger = New-Object Microsoft.Build.BuildEngine.FileLogger
$fileLogger.Parameters = ""verbosity=diagnostic""
$engine.RegisterLogger($fileLogger)
# Run the build - this is the line that throws a CmdletInvocationException
$result = $project.Build()
$engine.Shutdown()
}
";
using (var invoker = new RunspaceInvoke())
{
invoker.Invoke(script);
IList errors;
Collection<PSObject> results = invoker.Invoke(#"$input | Run-MSBuild", new[] {0}, out errors);
Array.ForEach<PSObject>(results.ToArray(), Console.WriteLine);
}
}
}
}
Which line of your C# code fails? Also, can you post some of the specifics from the exception. You can work around the MTA thread issue by doing something like this:
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
namespace RunspaceInvokeExp
{
class Program
{
[STAThread]
static void Main()
{
var script = #"
function Run-MSBuild
{
[System.Reflection.Assembly]::LoadWithPartialName(""Microsoft.Build.Engine"")
$engine = New-Object Microsoft.Build.BuildEngine.Engine
$engine.BinPath = ""C:\Windows\Microsoft.NET\Framework\v3.5""
$project = New-Object Microsoft.Build.BuildEngine.Project($engine, ""3.5"")
$project.Load(""deploy.targets"")
$project.InitialTargets = ""DoStuff""
# Process the input object
while ($input.MoveNext())
{
# Set MSBuild Properties & Item
}
# Optionally setup some loggers (have also tried it without any loggers)
$consoleLogger = New-Object Microsoft.Build.BuildEngine.ConsoleLogger
$engine.RegisterLogger($consoleLogger)
$fileLogger = New-Object Microsoft.Build.BuildEngine.FileLogger
$fileLogger.Parameters = ""verbosity=diagnostic""
$engine.RegisterLogger($fileLogger)
# Run the build - this is the line that throws a CmdletInvocationException
$result = $project.Build()
$engine.Shutdown()
}
Run-MSBuild
";
Runspace runspace = RunspaceFactory.CreateRunspace();
Runspace.DefaultRunspace = runspace;
runspace.Open();
EngineIntrinsics engine = runspace.SessionStateProxy.
GetVariable("ExecutionContext") as EngineIntrinsics;
ScriptBlock scriptblock =
engine.InvokeCommand.NewScriptBlock(script);
Collection<PSObject> results = scriptblock.Invoke(new[] { 0 });
Array.ForEach<PSObject>(results.ToArray(), Console.WriteLine);
runspace.Close(); // Really should be in a finally block
}
}
}