ClickOnce, in general, is a great way to deploy a program. I could set it to check for updates before starting, easily publish a new version of a program, etc. But uninstalling ClickOnce programs cannot be done automatically as they pop up a “Maintenance” window when you try to uninstall that needs someone to click on it:

Well I was in a situation where I needed to uninstall two different ClickOnce programs on 200+ machines and manually uninstalling on all of them wasn’t really a option. So I searched and found a 13 year old post about the same issue with a neat PowerShell answer here: https://stackoverflow.com/a/24440119/5061596 It’s slightly hacky, using SendKeys to acknowledge the uninstall screen, but it worked. I then customized it more to look for the original executable and wait until it was removed before moving on as ClickOnce wouldn’t let me try uninstalling another program before the first one was done. Here is the PowerShell function:
Function UninstallClickOnceProgram {
Param(
[Parameter(Mandatory=$true)][string]$ProgramName,
[Parameter(Mandatory=$true)][string]$Image
)
<#
.SYNOPSIS
Checks for the existence of a ClickOnce program and if found uninstalls it using the uninstall string in the registry. While it's uninstalling it will
look for the existence of the executable for up to 1 minute
.PARAMETER ProgramName
Name of the program being checked. This is the name as it exists in the standard Add/Remove programs dialog.
.PARAMETER Image
Name of the executable itself. This is used to kill the process if it's open.
.OUTPUTS
None
#>
$InstalledApplicationNotMSI = Get-ChildItem HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall | ForEach-Object {Get-ItemProperty $_.PsPath}
$UninstallString = $InstalledApplicationNotMSI | ? { $_.displayname -match $ProgramName } | select UninstallString
If (-not [String]::IsNullOrEmpty($UninstallString)){
$MyPID = (Get-Process $Image -ea SilentlyContinue | select -ExpandProperty id)
IF ($MyPID -ne $null) {
Write-Host "$ProgramName is running. Attempting to kill process...."
Stop-Process -id $MyPID -Force
Start-Sleep -s 5
}
Write-Host "Attempting to uninstall the ClickOnce application $ProgramName"
$wshell = new-object -com wscript.shell
$selectedUninstallString = $UninstallString.UninstallString
$wshell.run("cmd /c $selectedUninstallString")
Start-Sleep 5
$wshell.sendkeys("`"OK`"~")
Write-Host "Checking status...."
For ($i=1; $i -le 10; $i++) {
$MyExecutable = Get-ChildItem "$env:LOCALAPPDATA\Apps\2.0" -Filter "$Image.exe" -Recurse | % { $_.FullName } | Select-Object -First 1
If ($MyExecutable -eq $null) {
Write-Host "$ProgramName was not found."
break
} Else {
If (Test-Path $MyExecutable) {
Write-Host "$ProgramName still found. Waiting.... ($i/10)."
} Else {
Write-Host "$ProgramName was uninstalled."
break
}
}
Start-Sleep -s 6
}
Start-Sleep -s 5
} else {
Write-Host "Did not find the ClickOnce application $ProgramName"
}
}
And here is how I call it:
UninstallClickOnceProgram -ProgramName "My Super Database" -Image "MSD"
I still sent out a notification email letting people know this was happening and not to click anything and give the login scripts a extra 60 seconds. But the end result was a relatively automated uninstall of our ClickOnce programs that we needed to uninstall.