Powershell - Replace add_Click link on LinkLabel - powershell

I am creating a PowerShell GUI that uses a link label. My code for this link is
$ExLinkLabel = New-Object System.Windows.Forms.LinkLabel
$ExLinkLabel.Location = New-Object System.Drawing.Size(15,130)
$ExLinkLabel.Size = New-Object System.Drawing.Size(150,20)
$ExLinkLabel.LinkColor = "BLUE"
$ExLinkLabel.ActiveLinkColor = "RED"
$ExLinkLabel.Text = "Link Example"
$ExLinkLabel.add_Click({[system.Diagnostics.Process]::start("https://google.com")})
$Form.Controls.Add($ExLinkLabel)
Now say I want to change it another website later in the code based on certain conditions, I tried doing this:
$ExLinkLabel.add_Click({[system.Diagnostics.Process]::start("https://yahoo.com")})
The problem that this now has two links open, both google and then yahoo.
Is there a way to clear or just replace that first link with my new one?
Thank you

Adding an event handler with an .add_<EventName>() method call does just that: It adds an additional event handler, of which there can be many.
In order to replace an event handler, you must first remove its old incarnation with .remove_<EventName>(), and then add the new incarnation with .add_<EventName>().
To that end, you must store the original incarnation in a variable that you can later pass to the .remove_EventName>() call:
# Define the original event handler, store it in a variable,
# and add it to the control.
$currEventHandler = { Start-Process https://google.com }
$ExLinkLabel.add_Click($currEventHandler)
# ...
# Remove the current event handler...
$ExLinkLabel.remove_Click($currEventHandler)
# ... and add the new one:
$currEventHandler = { Start-Process https://yahoo.com }
$ExLinkLabel.add_Click($currEventHandler)
Note that I've replaced [system.Diagnostics.Process]::start($url) with a simpler, PowerShell-idiomatic call to the Start-Process cmdlet.
In your simple case, where the two event handlers only differ by the URL they open, consider the alternative recommended by Theo:
Retain the original event handler and make it retrieve the URL to open from a variable defined outside the event handler, namely in the script scope. That way, all you need to do is to update the variable.
# Set the original URL
$url = 'https://google.com'
# Due to PowerShell's dynamic scoping, the event-handler script
# block - even though it runs in a *child* scope - sees the
# script scope's definition of variable $url
# You can make this cross-scope access explicit by using $script:url
# (If you wanted to *update* the variable from inside the child
# scope $script:url *must* be used.)
$ExLinkLabel.add_Click({ Start-Process $url })
# ...
# Update the variable, after which the event handler will
# use the new URL.
$url = 'https://yahoo.com'

Related

Disabling Installshield's Next button with powershell

I have an Installshield project that uses powershell custom actions.
In one of my dialogs, I'm asking from the user to enter username and password, then I validate the credentials (with powershell) and I want to enable the Next button only if the credentials were correct.
Can this be achieved with powershell action item? The reason I'm using powershell is that I don't know InstallScript at all.
Here is my powershell script so far:
Function Test-UserCredential {
Param($username, $password)
Add-Type -AssemblyName System.DirectoryServices.AccountManagement
$ct = [System.DirectoryServices.AccountManagement.ContextType]::Machine, $env:computername
$opt = [System.DirectoryServices.AccountManagement.ContextOptions]::SimpleBind
$pc = New-Object System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList $ct
$Result = $pc.ValidateCredentials($username, $password).ToString()
$Result
}
$comp_username = Get-Property -Name COMPUTER_USERNAME
$comp_password = Get-Property -Name COMPUTER_PASSWORD
$result = Test-UserCredential -username $comp_username -password $comp_password
if ($result)
{
#Enable "Next" button
}
else
{
#Disable "Next" button
}
Thanks.
There are three things you will have to do.
Choose a property that tracks whether the Next button should be enabled, and set that from your PowerShell. Typically you will set it to "1" or "" (empty string) for ease in the next step.
Create Control Conditions in the dialog editor that Enable and Disable the Next button referencing the property as your condition.
Separately trigger the UI to update after the powershell action completes. Unfortunately the UI does not evaluate control conditions after all property changes; it only does so after it changes a property it thinks is related. So the easiest way to do this is to add a Set Property control event that sets the property.
Note that for clarity of step 3's relevance, it can be useful to split this into two separate properties; set one in the powershell, reflect that into another in the Set Property control event, and have control conditions that read the latter.

Cannot get Powershell GUI to modify variable

I'm creating a GUI to enter in data then store into some variables to work with later. The problem I'm having is modifying the variables once the user clicks the OK button. I have the following code:
$button4 = New-Object system.windows.Forms.Button
$button4.Text = "button"
$button4.Width = 60
$button4.Height = 30
$button4.Add_Click({
$variable = "test"
})
$variable gets assigned with the Add_Click function but I'm not able to access it anywhere else. This makes sense as I read about Powershell scopes. But how am I supposed to access the information I set in there? I'm using this Microsoft guide as to build the GUI. In their example I should be able to return $x at the end of the script. But when I do it the variable isn't available.

Using querySelectorAll on an mshtml.HTMLDocumentClass object in PowerShell causes a crash

I'm trying to do some web-scraping via PowerShell, as I've recently discovered it is possible to do so without too much trouble.
A good starting point is to just fetch the HTML, use Get-Member, and see what I can do from there, like so:
$html = Invoke-WebRequest "https://www.google.com"
$html.ParsedHtml | Get-Member
The methods available to me for fetching specific elements appear to be the following:
getElementById()
getElementsByName()
getElementsByTagName()
For example I can get the first IMG tag in the document like so:
$html.ParsedHtml.getElementsByTagName("img")[0]
However after doing some more research in to whether I could use CSS Selectors or XPath I discovered that there are unlisted methods available, since we are just using the HTML Document object documented here:
querySelector()
querySelectorAll()
So instead of doing:
$html.ParsedHtml.getElementsByTagName("img")[0]
I can do:
$html.ParsedHtml.querySelector("img")
So I was expecting to be able to do:
$html.ParsedHtml.querySelectorAll("img")
...in order to get all of the IMG elements. All the documentation I've found and googling I've done supports this. However, in all my testing this function crashes the calling process and reports a heap corruption exception code in the Event Log (0xc0000374).
I'm using PowerShell 5 on Windows 10 x64. I've tried it in a Win10 x64 VM that is a clean build and just patched up. I've also tried it in Win7 x64 upgraded to PowerShell 5. I haven't tried it on anything prior to PowerShell 5 as all our systems here are upgraded, but I probably will once I have time to spool a new vanilla VM for testing.
Has anyone run in to this issue before? All my research so far is a dead end. Are there alternatives to querySelectorAll? I need to scrape pages that will have predictable sets of tags inside unpredictable layouts and potentially no IDs or classes assigned to the tags, so I want to be able to use selectors that allow structure/nesting/wildcards.
P.S. I've also tried using the InternetExplorer.Application COM object in PowerShell, the result is the same, except instead of PowerShell crashing Internet Explorer crashes. This was actually my original approach, here's the code:
# create browser object
$ie = New-Object -ComObject InternetExplorer.Application
# make browser visible for debugging, otherwise this isn't necessary for function
$ie.Visible = $true
# browse to page
$ie.Navigate("https://www.google.com")
# wait till browser is not busy
Do { Start-Sleep -m 100 } Until (!$ie.Busy)
# this works
$ie.document.getElementsByTagName("img")[0]
# this works as well
$ie.document.querySelector("img")
# blow it up
$ie.document.querySelectorAll("img")
# we wanna quit the process, but since we blew it up we don't really make it here
$ie.Quit()
Hope I'm not breaking any rules and this post makes sense and is relevant, thanks.
UPDATE
I tested earlier PowerShell versions. v2-v4 crash using the InternetExplorer.Application COM method. v3-4 crash using the Invoke-WebRequest method, v2 doesn't support it.
I ran into this problem, too, and posted about it on reddit. I believe the problem happens when Powershell tries to enumerate the HTML DOM NodeList object returned by querySelectorAll(). The same object is returned by childNodes() which can be enumerated by PS, so I'm guessing there's some glue code written for .ParsedHtml.childNodes but not .ParsedHtml.querySelectorAll(). The crash can be triggered by Intellisense trying to get tab-complete help for the object, too.
I found a way around it, though! Just access the native DOM methods .item() and .length directly and emit the node objects into a PowerShell array. The following code pulls the newest page of posts from /r/Powershell, gets the post list anchors via querySelectorAll() then manually enumerates them using the native DOM methods into a Powershell-native array.
$Result = Invoke-WebRequest -Uri "https://www.reddit.com/r/PowerShell/new/"
$NodeList = $Result.ParsedHtml.querySelectorAll("#siteTable div div p.title a")
$PsNodeList = #()
for ($i = 0; $i -lt $NodeList.Length; $i++) {
$PsNodeList += $NodeList.item($i)
}
$PsNodeList | ForEach-Object {
$_.InnerHtml
}
Edit .Length seems to work capitalized or lower-case. I would have expected the DOM to be case-sensitive, so either there's some things going on to help translate or I'm misunderstanding something. Also, the CSS selector is grabbing the source links (self.PowerShell mostly), but that it my CSS selector logic error, not a problem with querySelectorAll(). Note that the results of querySelectorAll() are not live, so modifying them won't modify the original DOM. And I haven't tried modifying them or using their methods yet, but clearly we can grab at the very least .InnerHtml.
Edit 2: Here is a more-generalized wrapper function:
function Get-FixedQuerySelectorAll {
param (
$HtmlWro,
$CssSelector
)
# After assignment, $NodeList will crash powershell if enumerated in any way including Intellisense-completion while coding!
$NodeList = $HtmlWro.ParsedHtml.querySelectorAll($CssSelector)
for ($i = 0; $i -lt $NodeList.length; $i++) {
Write-Output $NodeList.item($i)
}
}
$HtmlWro is an HTML Web Response Object, the output of Invoke-WebReqest. I originally tried to pass .ParsedHtml but then it would crash on assignment. Doing it this way returns the nodes in a Powershell array.
The #midnightfreddie's solution worked fine for me before, but now it throws Exception from HRESULT: 0x80020101 when calling $NodeList.item($i).
I found the following workaround:
function Invoke-QuerySelectorAll($node, [string] $selector)
{
$nodeList = $node.querySelectorAll($selector)
$nodeListType = $nodeList.GetType()
$result = #()
for ($i = 0; $i -lt $nodeList.length; $i++)
{
$result += $nodeListType.InvokeMember("item", [System.Reflection.BindingFlags]::InvokeMethod, $null, $nodeList, $i)
}
return $result
}
This one works for New-Object -ComObject InternetExplorer.Application as well.

creating GUI forms without variables

I am trying to find a way to create a form in PowerShell without using any variables unless they are temporarily or virtually assigned. I want to be able to run a command similar to this:
(New-Object System.Windows.Forms.Form).ShowDialog()
where I can enter in a code into an event that is triggered once the form is created. That event will then be responsible for creating all the objects and other events inside the form. Once the form is launched, I will not need any variables accept for the ones that are virtually assigned within the events.
This to avoid using too much system resources from assigning and endless amount of variables for each object in the form. The script that I am currently working on in PowerShell is very possibly going to be really big, and even if it is not a very large script, efficiency and clean code is always the key to writing a good program or script.
add-type -ass System.Windows.Forms
$x = (New-Object System.Windows.Forms.Form)
$x.Text = 'Message Box'
$x.Size = '300,150'
$x.Font = $x.Font.Name + ',12'
$x.Controls.Add((New-Object System.Windows.Forms.Label))
$x.Controls[-1].Size = $x.Size
$x.Controls[-1].Text = 'Here is a message for you'
$x.ShowDialog()
Remove-Variable x
It is very possible to access these objects still with the exact same kind of access when you define each object with a variable. It cost me many hours of research and just simply attempting random commands to find out how to do this. Here is all the commands you may need to relearn if you are interested in my solution:
# create item in form:
$x.Controls.Add((New-Object System.Windows.Forms.Button))
# access the last created item in the form:
$x.Controls[-1]
# change it's name to identify it easier
$x.Controls[-1].Name = 'button1'
# access the item by it's new name:
$x.Controls['button']
# delete the item by it's name:
$x.Controls.Remove($x.Controls['button1'])
If your familiar with form creation in PowerShell then this should all make sense to you and you should be familiar with how the rest of it works. Also, another note to make for those who are interested in what I am trying to do is that any of these commands can be done within an event by replacing $x with $this. If it is inside an event of an object inside the "controls" section of the form, then you would use $this.parent.
This is exactly what I mean by having the ability to create a form with virtually no variables. The only problem I am having with this is that I am unsure how to assign an event and call the method ShowDialog() at the same time.
I found an a very interesting solution to this, however I am not sure to what the limits are to this solution and it dose not quite work in the way that I would personally like it to.
file.ps1:
add-type -ass System.Windows.Forms
$x = (New-Object System.Windows.Forms.Form)
$x.Text = 'Message Box'
$x.Size = '300,150'
$x.Font = $x.Font.Name + ',12'
$x.Controls.Add((New-Object System.Windows.Forms.Label))
$x.Controls[-1].Size = $x.Size
$x.Controls[-1].Text = 'Here is a message for you'
$x
remove-variable x
command to execute the code:
(iex(Get-Content 'file.ps1'|out-string)).ShowDialog()

Read missing Word template from Powershell

I have a series of Word documents which link to templates which no longer exist. This is causing problems for users trying to open them. I can get a list of the documents, loop through each one, and set the tempalte to null. While this will solve the problem, I can't determine what the template was before I changed it.
In cases where the template is not available on open, Word will replace the attached template with Normal.dot(x). However, the template I'm trying find is located in the document's Tempaltes dialog. Both AttachedTempalte() and get_AttachedTemplate().Name return Normal.dot when I know the document in question has a different template listed in the Templates dialog in word.
I can access this in VBA, and it's fustrating to not be able to do this in PS. Can anyone see where I'm messing up?
$word = new-object -comobject "Word.Application"
$doc = $word.Documents.Open({document path})
$word.Dialogs(Microsoft.Office.Interop.Word.WdWordDialog.wdDialogToolsTemplates).Template()
Returns:
Missing ')' in method call.
At :line:1 char:15
+ $word.Dialogs(M <<<< icrosoft.Office.Interop.Word.WdWordDialog.wdDialogToolsTemplates).Template()
Working VBA:
Dim doc as Word.Document
Dim strTemplate as String
Set doc = Documents.Open(Filename:=filename, Visible:=False)
doc.Activate
strTemplate = Word.Dialogs(wdDialogsToolsTemplates).Template
After which I can see the template name and path I should see in strTemplate.
I checked the ps script and adding $doc.Activate doesn't seem to help. I also noticed that the interop and VBA do not use the same wdDialog. PS uses wdDialogToolsTemplates and VBA using wdDialogsToolsTemplates. I checked the assembly in PS with the following
[Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Interop.Word") | out-null
[Enum]::GetNames("Microsoft.Office.Interop.Word.WdDialogs")
and confirmed the correct option is wdDialogToolsTemplates.
In powershell you must use the [] brackets to specify types and then the :: to specify the type member, so your 3rd line of powershell code should look like this:
$word.Dialogs([Microsoft.Office.Interop.Word.WdWordDialog]::wdDialogToolsTemplates).Template()
See these blog posts about powershell enums: Jeffrey Snover or Richard Siddaway.
I am trying to do something similar and the main aim is not to store any code inside a Word document.
PowerShell
I made a tiny bit of progress with the PowerShell route but I can't find a way to extract the path from the dialog boxes.
$objWord = New-Object -ComObject "Word.Application"
$objWord.Visible = $True
$objDoc = $objWord.Documents.Open("C:\path\to\document.doc")
$objToolsTemplates = $objWord.Dialogs.Item([Microsoft.Office.Interop.Word.WdWordDialog]::wdDialogToolsTemplates)
$objDocStats = $objWord.Dialogs.Item([Microsoft.Office.Interop.Word.WdWordDialog]::wdDialogDocumentStatistics)
Now $objToolsTemplates.Show() and $objToolsTemplates.Show() both cause the GUI to display dialogs containing the path to the original (unavailable) template but I can't find any way to extract that information programatically. According to MSDN WdWordDialog Enumeration documentation both of the dialogs should have a Template property.
VBScript
In the end I had to go with VBS instead. The following code will give you the path of the original (unavailable) template. It's still a script at least.
Option Explicit
Const wdDialogToolsTemplates = 87
Const wdDoNotSaveChanges = 0
Dim objWordApp
Dim objDoc
Dim dlgTemplate
Set objWordApp = CreateObject("Word.Application")
objWordApp.Visible = False
Set objDoc = objWordApp.Documents.Open("C:\path\to\document.doc")
Set dlgTemplate = objWordApp.Dialogs(wdDialogToolsTemplates)
Wscript.Echo dlgTemplate.Template
objDoc.Close(wdDoNotSaveChanges)
objWordApp.Quit