Connecting to Excel from powershell using VBA in an active excel document

Viewed 280

I have a very weird problem.

I have some vba scripts called merge1-6 which extracts data from different excel sheets. These 6 resulting text files are merged using another macro "MergeALL" into 1 text file, so everything is on one string.

So far so good, it works fine.

So I wanted to make a powershell script that access the current excel document i have open, and fetch the J29 cell data, and merge this data with the value "0|" to combine a given string.

The actual powershell script I have to pull the cell data is as follows:

Macro : Call BID_MergeXCleanup_TextOut

#This script opens H:\Temp\PublicFolder-powershell\Create-String-For-Email-Fetch.txt and replace empty parts of "domain/users/0|" with "nothing"
#stripping away all entries not needed.  Out-File used as output, with -NoNewline to prevent carriage return inserting empty line.
#$workbook = $excel.Workbooks.Open("H:\Temp\PublicFolder-powershell\Create-String-For-Email-Fetch.xlsm") #open workbook

$xl = [Runtime.InteropServices.Marshal]::GetActiveObject(Excel.Application')
$workbook = $xl.workbooks | ?{$_.FullName -eq "H:\Temp\PublicFolder-powershell\Create-String-For-Email-Fetch.xlsm"}
$workbook.sheets.item(1).activate() #opens the sheet and activates it
$WorkbookTotal=$workbook.Worksheets.item(1) #Set sheet data to variable
$value = $WorkbookTotal.Cells.Item(29, 10)  # Fetches data in Column 10, Row 29 which is the cell reference J29 where domain is. 
$domain = $value.Text  #Fetches "domain/users/" from the Fetch-tab of the excel file, to match the correct domain in use and stores result in the $domain variable
$leftoverdomain = '0|'    #last part of domain string after the domain/user part
$joinedvariables = -join($domain,$leftoverdomain,"") #Joines the above variables together to form "domain/users/0|" as searchtag to replace as leftover bits.

$b = Get-Content -Path "H:\Temp\PublicFolder-powershell\Create-String-For-Email-Fetch.txt" #Opens text file to replace in.

#Next part uses the $joinedvariables variable of merged search parametres and replaces it with "nothing" and saves the file back.

@(ForEach ($a in $b) {$a.Replace($joinedvariables, '')}) | Out-File "H:\Temp\PublicFolder-powershell\Create-String-For-Email-Fetch.txt" -NoNewline
Start-Sleep -Seconds 0.3

### INFO ###
#I used a reference to the EXCEL cell here, instead of directly from the TXT-file as given what domain has been specified, the info in the TEXT file would vary
#But text in J29 would always equal the domain being used, and thus also correnspond with the one in the text file at all times. 
#This way it will automatically always use the correct domain.

So, if I use a standalone reference to the excel document,

$workbook = $excel.Workbooks.Open("H:\Temp\PublicFolder-powershell\Create-String-For-Email-Fetch.xlsm") 

...and directly open the workbook, in powershell ISE, the command is successful. BUT does not execute correctly if I call the PS1 script or run it manually from within the excel file. Which is why I started looking for how to access the current excel session leading me to the next method...

If I use

$xl = [Runtime.InteropServices.Marshal]::GetActiveObject('Excel.Application') 

to reference the current running application, it successful from both ISE and inside Excel, when I run manually.

In both instances this work, as long as the TXT file that is to be modified exist and has the information in it that is searched for.

So given that I have an "unmodified file" I wanted to launch these macros by calling them from another macro.

This is the VBA code used from within the same excel sheet to launch the macros.

Call BID_1_TextOut 
    Application.Wait Now + TimeSerial(0, 0, 1)
Call BID_2_TextOut 
    Application.Wait Now + TimeSerial(0, 0, 1)
Call BID_3_TextOut 
    Application.Wait Now + TimeSerial(0, 0, 1)
Call BID_4_TextOut 
    Application.Wait Now + TimeSerial(0, 0, 1)
Call BID_5_TextOut 
    Application.Wait Now + TimeSerial(0, 0, 1)
Call BID_6_TextOut 
    Application.Wait Now + TimeSerial(0, 0, 1)
Call BID_MergeALL_TextOut 'Macro MergeALL - Merges all BID-TEXTOUT files to 1 file
    Application.Wait Now + TimeSerial(0, 0, 3)
Call BID_MergeXCleanup_TextOut 'Macro MergeXCleanup - This script replace empty parts of "domain/users/0|" with "nothing"
    Application.Wait Now + TimeSerial(0, 0, 1)

The two last macros are the important ones. 2nd last macro prepare the file that is to be used in "BID_MergeXCleanup" which is the PS1 script I am calling.

The macros i call are with the following code:

' Script to merge all the BID-TEXT-OUT files into 1 string with UTF32LE encoding.

Sub BID_MergeALL_TextOut()

Set objShell = CreateObject("wscript.Shell")
objShell.Run ("powershell.exe H:\Temp\PublicFolder-powershell\MergeALL_UTF32.ps1")

End Sub

and

'This script opens H:\Temp\PublicFolder-powershell\Create-String-For-Email-Fetch.txt and replace empty parts of "domain/users/0|" with "nothing"
'stripping away all entries not needed.

Sub BID_MergeXCleanup_TextOut()

Set objShell = CreateObject("wscript.Shell")
objShell.Run ("powershell.exe -noexit H:\Temp\PublicFolder-powershell\REPLACE_IN_EmptyDomainUsersPart.ps1")

End Sub

The weird thing is, if I use the "calling script" to run these commands in succession automatically, the last script ALWAYS fails

"cannot call a method or null-valued expression.

If I inspect the TXT file afterwards, I see that only the '0|' variable was removed, as it seems it was not able to fetch the J29 information from the Excel document.

If I run each of the macros 1 by 1 from within Excel after each other manually then they execute correctly and the scripts do as I want giving no errors

merge1-6-then-all-then-cleanup

So from what I can tell, my powershell script is able to call the active excel workbook, sheet, cell, get data and all that.

...but I just cant do it with calling the macro from another macro? In this example it fails if I use the "Call BID_MergeXCleanup_TextOut" reference to access the macro.

PS: I also tried to "share" the workbook, in case it was a permission issue, but it still gave error when running the Call-VBA script, but not manually running each macro in succession.

Any idea how I can fix this?

I have a feeling i might not allow the original PS1 file to have a $NULL expression exit, but i am unsure how to add it in.

The first code line that fails is

$workbook.sheets.item(1).activate() 

I tried to add $NULL to it in Powershell ISE and then it fails

$null = $workbook.sheets.item(1).activate()     <<<--- Fails with *"You cannot call a method on a null-valued expression."*

And i cannot see that i have any undecleared variables in the main script either.

Unsure how to figure this one out.

1 Answers

What i ended up with:

So @postanote up here suggested i Use COMs instead of InteropService.Marshal to access Excel.

I initially thought this would not work, since it would open a new instance of the workbook, but the COMs version have a way of hiding the instance you run with

$objExcel.Visible = $false

Now, the [Runtime.InteropServices.Marshal]::GetActiveObject version also have this value, it seems to be different somehow because, if I change my code to be COMs as follows:

$objExcel = New-Object -ComObject Excel.Application
$objExcel.Visible = $false
$WorkBook = $objExcel.Workbooks.Open("H:\Temp\PublicFolder-powershell\Create-String-For-Email-Fetch.xlsm")
$WorkSheet = $WorkBook.sheets.item("Fetch")
$domain = $worksheet.Rows.Item(29).Columns.Item(10).Text
$leftoverdomain = '0|'    #last part of domain string after the domain/user part
$joinedvariables = -join($domain,$leftoverdomain,"") #Joines the above variables together to form "domain/users/0|" as searchtag to replace as leftover bits.

$b = Get-Content -Path "H:\Temp\PublicFolder-powershell\Create-String-For-Email-Fetch.txt" #Opens text file to replace in.

@(ForEach ($a in $b) {$a.Replace($joinedvariables, '')}) | Out-File "H:\Temp\PublicFolder-powershell\Create-String-For-Email-Fetch.txt" -NoNewline
Start-Sleep -Seconds 0.3

...The script excecute without error when called from the VBA call script...

If I on the other hand try to incorporate the non-visible member in the original code as here:

$xl = [Runtime.InteropServices.Marshal]::GetActiveObject('Excel.Application') #opens an already open excel session
$xl.Visible = $false
$workbook = $xl.workbooks | ?{$_.FullName -eq "H:\Temp\PublicFolder-powershell\Create-String-For-Email-Fetch.xlsm"} 
$workbook.sheets.item(1).activate() 
$WorkbookTotal=$workbook.Worksheets.item(1) 
$value = $WorkbookTotal.Cells.Item(29, 10)  
$domain = $value.Text  
$leftoverdomain = '0|'   
$joinedvariables = -join($domain,$leftoverdomain,"") 

$b = Get-Content -Path "H:\Temp\PublicFolder-powershell\Create-String-For-Email-Fetch.txt" 

@(ForEach ($a in $b) {$a.Replace($joinedvariables, '')}) | Out-File "H:\Temp\PublicFolder-powershell\Create-String-For-Email-Fetch.txt" -NoNewline
Start-Sleep -Seconds 0.3

...then it fails with the following changed error message:

Call was rejected by callee. (Exception from HRESULT: 0x80010001 (RPC_E_CALL_REJECTED))

rpc reject error message

So i guess the end result is that COMs makes it possible to open the same document more than once with the same name, as long as you hide the new excel instance from Windows window handler.

But...it would have been nice to know why the original code worked when ran manually, but not when called from the script, so if anyone knows, I'd love to know the answer to that one.

Related