7

Consider this code:

PS> $timer = New-Object Timers.Timer
PS> $timer.Interval = 1000
PS> $i = 1;
PS> Register-ObjectEvent $timer Elapsed -Action { write-host 'i: ' $i }.GetNewClosure()
PS> $timer.Enabled = 1
i:  1
i:  1
i:  1
 ...
# wait a couple of seconds and change $i
PS> $i = 2
i:  2
i:  2
i:  2

I assumed that when I create new closure ({ write-host 'i: ' $i }.GetNewClosure()) value of $i will be tied to this closure. But not in this case. Afer I change the value, write-host takes the new value.

On the other side, this works:

PS> $i = 1;
PS> $action = { write-host 'i: ' $i }.GetNewClosure()
PS> &$action
i:  1
PS> $i = 2
PS> &$action
i:  1

Why it doesn't work with the Register-ObjectEvent?

Jon Seigel
  • 12,251
  • 8
  • 58
  • 92
stej
  • 28,745
  • 11
  • 71
  • 104
  • It would help if you specified what result you expected and what result you got. – Richard Mar 10 '10 at 09:58
  • added outputs so I hope it is clear.. – stej Mar 10 '10 at 10:21
  • It looks like a bug to me or at least we should have a way to configure this. I found it already submitted: https://connect.microsoft.com/PowerShell/feedback/details/541754/getnewclosure-doesnt-work-on-register-objectevent# – Roman Kuzmin May 04 '10 at 11:33
  • Yes, I submitted it as a bug, because I haven't found any solution and the behaviour looks strange. – stej May 04 '10 at 12:15

3 Answers3

3

Jobs are executed in a dynamic module; modules have isolated sessionstate, and share access to globals. PowerShell closures only work within the same sessionstate / scope chain. Annoying, yes.

-Oisin

p.s. I say "jobs" because event handlers are effectively local jobs, no different than script being run with start-job (local machine only, implicit, not using -computer localhost)

x0n
  • 51,312
  • 7
  • 89
  • 111
  • Makes sense, accepted. How did you find that info about jobs and event handlers? Other source than Reflector? :) – stej Jun 30 '10 at 05:56
  • 1
    No, I don't have source code. Only microsoft staff have that. Event handlers are jobs, because get-job will return them. – x0n Jun 30 '10 at 15:31
0

Use global variables in that case:

PS> $global:i = 1
PS> $timer = New-Object Timers.Timer
PS> $timer.Interval = 1000
PS> Register-ObjectEvent $timer Elapsed -Action { write-host 'i: ' $global:i }.GetNewClosure()
PS> $timer.Enabled = 1
i:  1
i:  1
i:  1

PS> Set-Variable -Name i -Value 2 -Scope Global

i:  2
i:  2
i:  2

Source:

http://stackoverflow.com/q/12535419/1287856
Mikaël Mayer
  • 10,425
  • 6
  • 64
  • 101
0

I think you are making assumptions that don't hold. PSH is interpreted, so when a code block is created it just holds the source code. When it is later evaluated any variables it uses will be looked up in the normal PSH way: first in the current scope, and then in each outer scope until a variable with a matching name if found.

When the timer fires its event, it executes the code block and thus looks up $i. Which is found in the outer scope with a value of 2.

In the second case, if you just use the code block directly (remove call to GetNewClosure) then the second execution gives 2.

Richard
  • 106,783
  • 21
  • 203
  • 265
  • And that's what I want - in the second example I want to get 1, not 2. It works there, but not in the first example. – stej Mar 10 '10 at 10:25
  • GetNewClosure is supposed to change the behavior you mention in your first paragraph. – Segfault Mar 10 '10 at 10:28