Why my ApplicationContext is null inside class CustomApplicationContext when I run it

18 views Asked by At

I have this script that should initialize a context containing a form, a notify icon, a timer and some methods.

using module .\..\tm


# Start-ProcessMonitor function using ApplicationContext
function Start-ProcessMonitor {
    param (
        [string] $processNameToMonitor,
        [int] $intervalInSec,
        [string] $logFilePath = '',
        [switch] $noGraphic
    )

    # Initialize your custom ApplicationContext
    if ($logFilePath -ne '') {
        $context = [CustomApplicationContext]::new($processNameToMonitor, $intervalInSec, $logFilePath)
    } else {
        $context = [CustomApplicationContext]::new($processNameToMonitor, $intervalInSec)
    }

    # Show form if graphic mode 
    if (!$noGraphic.IsPresent) {
        $context.ShowForm() # Show the form if not in graphic mode
    }
    
    # Run the application using the custom ApplicationContext
    [System.Windows.Forms.Application]::Run($context)
}

Start-ProcessMonitor -processNameToMonitor 'notepad' -intervalInSec 2

I don't understand why, once it reach the [System.Windows.Forms.Application]::Run($context), $context is null inside the class methods. To be more specific, the run triggers the timer loop. It runs crazy since the interval has been erased. I think it does nothing inside the Add_Tick block. I'm quit in a learning progress and I can't figure out what's happening. The class code is arround 300 lines, so I don't know if it's good to post it entirely.

EDIT : as the comment suggested it, here is the code for the class CustomApplicationContext. I think the issue comes from how I manage $context and the timer Add_Tick part.

# Define your custom ApplicationContext
class CustomApplicationContext : System.Windows.Forms.ApplicationContext {
    [System.Windows.Forms.Form] $form
    [System.Windows.Forms.NotifyIcon] $notifyIcon
    [System.Windows.Forms.ListBox] $logListBox
    [System.Windows.Forms.ContextMenu] $contextMenu
    [System.Windows.Forms.MenuItem] $menuItemChangeProcessName
    [System.Windows.Forms.MenuItem] $menuItemChangeCheckInterval
    [System.Windows.Forms.MenuItem] $menuItemExit
    [System.Windows.Forms.Timer] $timer
    [System.Array] $previousProcesses
    [System.Array] $processStartTime
    [string] $logFilePath
    [string] $processNameToMonitor
    [string] $caption
    [bool] $menuAskClosing
    [int] $intervalInSec

    CustomApplicationContext() {
        $this.processNameToMonitor = "explorer"
        $this.intervalInSec = "10"
        $this.Init($this.processNameToMonitor, $this.intervalInSec)
    }
    
    CustomApplicationContext([string] $processNameToMonitor, [int] $intervalInSec) {
        $this.logFilePath = "$env:TEMP\ProcessMonitor_$processNameToMonitor.log"
        $this.Init($processNameToMonitor, $intervalInSec, $this.logFilePath)
    }
    
    CustomApplicationContext([string] $processNameToMonitor, [int] $intervalInSec, [string] $logFilePath) {
        $this.Init($processNameToMonitor, $intervalInSec, $logFilePath)
    }
    
    [void] Init([string] $processNameToMonitor, [int] $intervalInSec, [string] $logFilePath) {
        # Store parameters
        $this.processNameToMonitor = $processNameToMonitor
        $this.intervalInSec = $intervalInSec
        $this.caption = "Process Monitor - {0} - {1} s" -f $processNameToMonitor, $intervalInSec
        $this.menuAskClosing = $false
        $this.previousProcesses = @()
        $this.processStartTime = @{}
        $this.logFilePath = $logFilePath

        # Create a form
        $this.form = New-Object System.Windows.Forms.Form
        $this.form.Text = $this.caption
        $this.form.Size = New-Object System.Drawing.Size(800, 300)
        $this.form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
        $this.form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::Sizable
        
        # Create a list box to display the log
        $this.logListBox = New-Object System.Windows.Forms.ListBox
        $this.logListBox.Dock = [System.Windows.Forms.DockStyle]::Fill
        $this.logListBox.SelectionMode = [System.Windows.Forms.SelectionMode]::MultiExtended
        $this.logListBox.FormattingEnabled = $true
        $this.logListBox.Font = "Courier"
        $this.form.Controls.Add($this.logListBox)

        # Create a NotifyIcon for the system tray
        $this.notifyIcon = New-Object System.Windows.Forms.NotifyIcon
        $icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$env:SystemRoot\System32\SHELL32.dll")
        $this.notifyIcon.Icon = $icon
        $this.notifyIcon.Text = $this.caption
        $this.notifyIcon.Visible = $true

        # Create a context menu to the NotifyIcon
        $this.contextMenu = New-Object Windows.Forms.ContextMenu
        # MenuItem to change the searched process name
        $this.menuItemChangeProcessName = New-Object Windows.Forms.MenuItem
        $this.menuItemChangeProcessName.Text = "Change Process Name"
        # MenuItem to exit the application
        $this.menuItemExit = New-Object Windows.Forms.MenuItem
        $this.menuItemExit.Text = "Exit"
        # MenuItem to change perdiode between 2 checks
        $this.menuItemChangeCheckInterval = New-Object Windows.Forms.MenuItem
        $this.menuItemChangeCheckInterval.Text = "Change Checking Interval"
        # Complete context menu
        $this.contextMenu.MenuItems.Add($this.menuItemChangeProcessName)
        $this.contextMenu.MenuItems.Add($this.menuItemChangeCheckInterval)
        $this.contextMenu.MenuItems.Add($this.menuItemExit)
        # Add contexte menu to Tray Icon and Form
        $this.notifyIcon.ContextMenu = $this.contextMenu
        $this.form.ContextMenu = $this.contextMenu

        # Create a timer to periodically check for processes
        $this.timer = New-Object System.Windows.Forms.Timer
        $this.timer.Interval = $this.intervalInSec * 1000
        $this.timer.Add_Tick({
            $this.TimerProcess()
        })

        # Handle the FormClosing event to dispose of resources
        $this.form.Add_FormClosing({
            if ($this.menuAskClosing) {
                $this.timer.Stop()
                $this.timer.Dispose()
                $this.notifyIcon.Dispose()
                $this.form.Dispose()
                $this.ExitThread()
            }
            else {
                $this.form.Hide()
                $this.notifyIcon.ShowBalloonTip(2000, "Process Monitor", "The application has been minimized to the system tray.", [System.Windows.Forms.ToolTipIcon]::Info)
                $_.Cancel = $true # Cancel the default close action
            }
        })
        
        # Handle the FormKeyPress event to check for the shortcuts
        $this.form.KeyPreview = $true
        $this.form.Add_KeyDown({
            if ($_.Control -and $_.KeyValue -eq 80) { # Check for Ctrl+P (ASCII code 80)
                $this.menuItemChangeProcessName.PerformClick() # Trigger the menu item click event
            }
            if ($_.Control -and $_.KeyValue -eq 73) { # Check for Ctrl+I (ASCII code 73)
                $this.menuItemChangeCheckInterval.PerformClick() # Trigger the menu item click event
            }
            if ($_.Control -and $_.KeyValue -eq 67) { # Check for Ctrl+C (ASCII code 67)
                $this.CopySelectedItemsToClipboard()
            }
            if ($_.Control -and $_.KeyValue -eq 65) { # Check for Ctrl+A (ASCII code 65)
                $this.SelectAllListItems()
            }
        })

        
        # Handle the NotifyIcon Click event to show/hide the form
        $this.notifyIcon.Add_Click({
            if ($_.Button -ne [System.Windows.Forms.MouseButtons]::Left) {
                return
            }
            if ($this.form.Visible) {
                $this.form.Hide()
            } else {
                $this.form.Show()
                $this.form.Activate()
            }
        })
        
        # Handle the Change Process Name menu button click event
        $this.menuItemChangeProcessName.Add_Click({
            $this.ChangeProcessName($this.AskNewProcessName())
            $this.ChangeCaption()
        })
        
        # Handle the Change Interval menu button click event
        $this.menuItemChangeCheckInterval.Add_Click({
            $this.ChangeInterval($this.AskNewInterval())
            $this.ChangeCaption()
        })

        $this.menuItemExit.Add_Click({
            $this.menuAskClosing = $true
            $this.form.Close()
        })

        # Start the timer
        $this.timer.Start()
    }
    
    ###########################################################################
    # Constructor - End                                                       #
    ###########################################################################

    # Core process of the timer
    [void] TimerProcess() {
        $changedProcesses = $this.UpdateProcesses()
        foreach ($changedProcess in $changedProcesses) {
            $logEntry = $this.PrintLog($changedProcess)
            $this.PrintInForm($logEntry)
            $this.PrintInFile($logEntry)
        }
    }

    # List the latest created and stoped processes
    [Hashtable] UpdateProcesses() {
        # Your logic for updating and logging processes here
        # You can use $this.logListBox to update the list box and log to a file if needed
        $processStatuses = @{}
        
        $currentProcesses = Get-Process -Name $this.processNameToMonitor -ErrorAction SilentlyContinue
    
        if ($currentProcesses -eq $null -and $this.previousProcesses.Count -eq 0) {
            return $processStatuses
        }
    
        if ($currentProcesses -ne $null) {
            $newProcesses = Compare-Object -ReferenceObject $this.previousProcesses -DifferenceObject $currentProcesses -Property "Id" -PassThru | Where-Object { $_.SideIndicator -eq "=>" }
            $endedProcesses = Compare-Object -ReferenceObject $this.previousProcesses -DifferenceObject $currentProcesses -Property "Id" -PassThru | Where-Object { $_.SideIndicator -eq "<=" }
            # Update the previous Xxxx processes
            $this.previousProcesses = $currentProcesses
        }
        else {
            $endedProcesses = $this.previousProcesses
            $newProcesses = @()
            $this.previousProcesses = @()
        }
    
        foreach ($newProcess in $newProcesses) {
            $processStatuses += [PSCustomObject]@{
                Status = "Started"
                Process = $newProcess.Name
            }
        }
    
        foreach ($endedProcess in $endedProcesses) {
            $processStatuses += [PSCustomObject]@{
                Status = "Ended"
                Process = $endedProcess.Name
            }
        }
        
        return $processStatuses
    }
    
    # Format the process info to log string
    # Then send to printer with form or file
    [string] PrintLog([PSCustomObject] $processInfo) {
        $process = $processInfo.Process
        $status = $processInfo.Status
        
        $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        $startTime = $process.StartTime
        if (-not $process.HasExited) {
            $this.processStartTime[$process.Id] = $process.StartTime
        }
        else {
            $startTime = $this.processStartTime[$process.Id]
            $this.processStartTime.Remove($process.Id)
        }
        $processTime = ((Get-Date) - $startTime).ToString("hh\:mm\:ss")
        $privateBytes = [int]($process.PrivateMemorySize64 / 1MB)
        $logEntry = "[{0}] {1} | PID: {2,-6}| Status: {3,-8}| Run Time: {4}s | Private Bytes: {5,4} MB" -f $timestamp, $process.Name, $process.Id, $status, $processTime, $privateBytes
        
        return $logEntry
    }

    # Print log entry in form
    [void] PrintInForm([string] $logEntry) {
        $this.logListBox.Items.Add($logEntry)
        $this.logListBox.ClearSelected()
        $this.logListBox.SelectedIndex = $this.logListBox.Items.Count - 1 # Focus on the last element
    }

    # Print log entry in file
    [void] PrintInFile([string] $logEntry) {
        if (-not (Test-Path($this.logFilePath)) -and $this.logFilePath -ne '') {
            New-Item -Path $this.logFilePath -ItemType File
        }
        # Append the log entry to a file
        $logEntry | Out-File -Append -FilePath $this.logFilePath
    }

    # Show dialog to input new process name to monitor
    [string] AskNewProcessName(){
        # Show a dialog to input the new process name
        $title = "Change Process Name"
        $msg = "Enter the new process name:"
        $newProcessName = [Microsoft.VisualBasic.Interaction]::InputBox($msg, $title, $this.processNameToMonitor)
    
        if (-not [string]::IsNullOrWhiteSpace($newProcessName)) {
            return $newProcessName
        }
        return $this.processNameToMonitor
    }

    # Show dialog to input new check interval
    [int] AskNewInterval(){
        # Show a dialog to input the new periode
        $title = "Change Checking Interval"
        $msg = "Enter the new periode in secondes:"
        $newInterval = [Microsoft.VisualBasic.Interaction]::InputBox($msg, $title, $this.intervalInSec)
        
        if (![string]::IsNullOrEmpty($newInterval) -and [decimal]::TryParse($newInterval, [ref]$null) -and $newInterval -gt 0) {
            return $newInterval
        }
        return $this.intervalInSec
    }

    # Set process name property and clear the previous processes pile
    [void] ChangeProcessName([string] $newProcessName) {
        if ($newProcessName -ne $this.processNameToMonitor){
            $this.processNameToMonitor = $newProcessName
            $this.previousProcesses = @()
        }
    }

    # Set interval property and update timer interval
    [void] ChangeInterval([int] $newInterval) {
        if ($newInterval -ne $this.intervalInSec){
            $this.intervalInSec = [int]$newInterval
            $this.timer.Interval = $this.intervalInSec *1000
        }
    }
    
    # Set caption of tray icon and form according to process name and interval properties
    [void] ChangeCaption() {
        $this.caption = "Process Monitor - $this.processNameToMonitor - $this.intervalInSec s"
        $this.notifyIcon.Text = $this.form.Text = $this.caption
    }

    # Set clipboard with seleted lines of listbox
    [void] CopySelectedItemsToClipboard() {
        # Get the selected items
        $selectedItems = $this.logListBox.SelectedItems
    
        # Convert the selected items to a plain text string
        $textToCopy = $selectedItems -join "`r`n"
    
        # Copy the text to the clipboard
        [System.Windows.Forms.Clipboard]::SetText($textToCopy)
    }
    
    # Select all lines of listbox
    [void] SelectAllListItems() {
        for ($i = 0; $i -lt $this.logListBox.Items.Count; $i++) {
            $this.logListBox.SetSelected($i, $true)
        }
    }

    # Show form 
    [void] ShowForm() {
        $this.form.Show()
    }
}
0

There are 0 answers