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.
Related
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()
I'm using functions to create different WinForm elements; for example, RadioButtons inside of a GroupBox. I'm trying to pass in a variable representing the currently selected RadioButton that gets updated whenever a new one is selected.
function New-RadioButton
{
[OutputType([System.Windows.Forms.RadioButton])]
param
(
[int] $LocationX,
[int] $LocationY,
[string] $Label,
[System.Windows.Forms.RadioButton] $SelectedRB
)
$newButton = New-Object System.Windows.Forms.RadioButton
$newButton.Text = $Label
$newButton.AutoSize = $false
$newButton.Bounds = New-Object System.Drawing.Rectangle($LocationX, $LocationY, $RBWidth, $RBHeight)
$newButton.Add_CheckedChanged({
if($newButton.Checked) {
$SelectedRB = $newButton
}
}.GetNewClosure())
return $newButton
}
function New-GroupBox
{
[OutputType([System.Windows.Forms.GroupBox])]
param
(
[int] $LocationX,
[int] $LocationY,
[System.Windows.Forms.RadioButton] $SelectedRB
)
$rb1 = New-RadioButton $RBx $RB1y 'One' $SelectedRB
$rb2 = New-RadioButton $RBx $RB2y 'Two' $SelectedRB
$groupBox = New-Object System.Windows.Forms.GroupBox
$groupBox.AutoSize = $false
$groupBox.Bounds = New-Object System.Drawing.Rectangle($LocationX, $LocationY, $GBWidth, $GBHeight)
$groupBox.Controls.AddRange(#($rb1, $rb2))
return $groupBox
}
$Selection1 = New-Object System.Windows.Forms.RadioButton
$GroupBox1 = New-GroupBox $GB1x $GB1y $Selection1
$Selection2 = New-Object System.Windows.Forms.RadioButton
$GroupBox2 = New-GroupBox $GB2x $GB2y $Selection2
In specific, I'm using a script block inside of $newButton.Add_CheckChanged(), and trying to access the scope of the parent function to get and set the selection variable passed by reference. I'm not super proficient with PowerShell, I'm trying to write this like a lambda in C# and capture the scope of the parent. However, I know that this doesn't necessarily work in PowerShell - I've added {}.GetNewClosure(), but this only copies the current scope at creation time, not run-time.
I know that I could create script-wide variables (i.e. a script:$Selection or storing them in a hash table), but is there a solution that is agnostic to the name of the variable itself?
Edit: For reference, I'm using PowerShell 5.1.
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()
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.
I have a simple script that will create an Outlook Calendar Item (found online) and it will create a calendar item just fine, but it puts it on my default calendar. How can I get it to put it on a specific calendar? This is what I have.
$outlook = new-object -com outlook.application
$CalItem = "1"
$newAppt = $outlook.CreateItem($CalItem)
$newAppt.Body = "Test Body222"
$newAppt.Subject = "Test Subject222"
$newAppt.Start = $OutObject.StartDate
$newAppt.End = $OutObject.ImpEndDate
$newAppt.BusyStatus = 0
$newAppt.Save()
Instead of using Application.CreateItem (whcih always uses the appropriate default folder), open the target folder programmatically and use MAPIFolder.Items.Add.
I was able to make it happen with this
# Instantiate a new Outlook object
$ol = new-object -ComObject "Outlook.Application"
# Map to the MAPI namespace
$MyNameSpace = $ol.getnamespace("mapi")
#Default Calendar Folder
$MyDefCal = $MyNameSpace.GetDefaultFolder("olFolderCalendar")
#Folder or "Calendar" I want to add the Item to
$MySharedCal = $MyDefCal.Folders.Item("TestCal")
#Create the Calendar Item
$MyItem = $MySharedCal.Items.Add(1)
$MyItem.Body = "Test"
$MyItem.Subject = "This Is A Test"
$MyItem.Start = "03/01/2019"
$MyItem.AllDayEvent = 1
$MyItem.ReminderSet = 0
$MyItem.BusyStatus = 0
$MyItem.Save()
Thanks to Dimitry for the help.