Programmatically Remove Mailmerge Data Source using VB.NET - excel

Working on automating a mail merge using Excel as the data source and merging into multiple .doc files as templates.
The first pass works great! Here is an outline of how the code is supposed to work:
1) Data is pulled from SQL Server into Excel and saved as .xlsx on a network drive.
2) Excel sheet is attached as a datasource to the .doc file and the merge is executed successfully.
3) xlWorkbook.Close(), xlApp.Workbooks.Close(), and xlApp.Quit(). Then I call my garbage collection routine to release the COM objects using Marshal.ReleaseComObject, and it appears Excel closes properly.
4) Use the same Excel source file with different template to create the next batch of letters.
At this point it seems the Excel file isn't releasing from memory after previously being used as a datasource. When I use wdAffDoc.MailMerge.OpenDataSource, I get a popup window from Word asking me which table to use and the list of tables is blank. The source data spreadsheet is NOT listed in the "spreadsheet" window of the popup. Last time I had this issue it was because I had the source file open on a different machine, and it wouldn't merge due to the lock. When this code bombs out, I look in Task Manager and see 1 or sometimes 2 entries of "EXCEL.EXE *32" listed under my username. The code will not run until the remaining EXCEL.EXE *32 processes are terminated.
Looking for any input as to the direction I should go here. Should I suspect my garbage collection routine, or do you think it's something else?
Here is my garbage collection:
Public Sub releaseObject(ByVal obj As Object)
Try
System.Runtime.InteropServices.Marshal.ReleaseComObject(obj)
obj = Nothing
Catch ex As Exception
MsgBox(ex.Message)
Finally
GC.Collect()
End Try
End Sub
Here is the first pass after pulling the source data (works as expected):
frmPleaseWait.Label1.Text = "Now merging documents. Please wait."
frmPleaseWait.Refresh()
Dim wdApp As New Word.Application
Dim wdDoc As New Word.Document
'Select template based on Queue chosen
If Queue = "716" Then
wdDoc = wdApp.Documents.Open("X:\Admin\LEGAL\MERGE LETTERS\UPH Vfn 2 Def.doc")
End If
wdApp.Visible = False 'Set this to False before going live -- true for debugging
wdDoc.MailMerge.OpenDataSource(Name:=fileDest, SQLStatement:="SELECT * FROM [Sheet1$]") 'Add a WHERE clause for filtering for affidavits, etc.
'.Destination 0 = DOCUMENT, 1 = PRINTER
wdApp.ActiveDocument.MailMerge.Destination = 0 'send to new document
With wdApp.ActiveDocument.MailMerge.DataSource
.FirstRecord = 1 'wdDefaultFirstRecord
.LastRecord = -16 'wdDefaultLastRecord
End With
wdApp.ActiveDocument.MailMerge.Execute(Pause:=False)
wdDoc.Close(SaveChanges:=False) 'Close the original mail-merge template file
wdApp.ActiveDocument.SaveAs2(savePath & "\" & ProcessDate & " " & Queue & " Verifications.doc")
wdApp.Quit()
wdDoc = Nothing
wdApp = Nothing
And here is the second (offending) pass:
Dim wdAffApp As New Word.Application
Dim wdAffDoc As New Word.Document
If Queue = "716" Then
wdAffDoc = wdAffApp.Documents.Open("X:\Admin\LEGAL\MERGE LETTERS\Suit Affidavit 2 Def.doc")
End If
wdAffApp.Visible = False 'Set this to False before going live -- true for debugging
'****************THIS IS THE LINE THAT PRODUCES THE ERROR****************
wdAffDoc.MailMerge.OpenDataSource(Name:=fileDest, SQLStatement:="SELECT * FROM [Sheet1$] WHERE [Suit_Bal] >= 5000") 'Add a WHERE clause for filtering for affidavits, etc.
'************************************************************************
'.Destination 0 = DOCUMENT, 1 = PRINTER
wdAffApp.ActiveDocument.MailMerge.Destination = 0 'send to new document
With wdAffApp.ActiveDocument.MailMerge.DataSource
.FirstRecord = 1 'wdDefaultFirstRecord
.LastRecord = -16 'wdDefaultLastRecord
End With
wdAffApp.ActiveDocument.MailMerge.Execute(Pause:=False)
wdAffDoc.Close(SaveChanges:=False) 'Close the original mail-merge template file
wdAffApp.ActiveDocument.SaveAs2(savePath & "\" & ProcessDate & " " & Queue & " Affidavits.doc")
wdAffApp.Quit()
wdAffDoc = Nothing
wdAffApp = Nothing
'Signal the end
frmPleaseWait.Dispose()
MsgBox("Mail merge complete")

Apparently my code worked!
After reviewing further, I discovered there was still WINWORD.EXE *32 running in the task manager. When I switched to that application, a popup window asked me if I wanted to save or delete the recovered files... must have been an error from one of my prior runs that got stuck in memory. Once I told Word to drop the recovered files, the code completes as expected. Whew! Glad this one is solved!

Related

I can't update worksheets objects links on powerpoint VBA - OneDrive Folder

I've already read in the forums but noone has my exactly problem, so here we go.
I have my excel and powerpoint files in a OneDrive folder (the Powerpoint is in subfolder), the powerpoint has 100 links.
So, in a forum someone suggested that to get the local OneDrive path, you should turn off the process. I did it.
I have to have the excel file open, because the processing time is really slow if the excel is closed. So If I have opened the excel file and run the macro (in other folder diferent to OneDrive) it runs ok, but if I try to do the same but in the OneDrive folder, it generated the next error into the code line pptShape.LinkFormat.Update:
Error -2147188160 (80048240) in runtime. LinkFormat (unknown member):
Invalid request. The linked file was unavailable and could not be
updated
If I have the excel file closed, the macro runs ok, but the process is so slow (almost 30 minuts), because it open and close the excel a hundred times.
does anyone knows why it happened? How can I fix it? I'll appreaciate your help. here is the code to update the links
Sub updatelinks_1()
Call Shell("cmd.exe /S /C" & "%LOCALAPPDATA%\Microsoft\OneDrive\OneDrive.exe /shutdown")
Application.DisplayAlerts = ppAlertsNone
Dim pptPresentation As Presentation
Dim pptSlide As Slide
Dim pptShape As Shape
'Set the variable to the PowerPoint Presentation
Set pptPresentation = ActivePresentation
'Loop through each slide in the presentation
For Each pptSlide In pptPresentation.Slides
'Loop through each shape in each slide
For Each pptShape In pptSlide.Shapes
'Find out if the shape is a linked object or a linked picture
If pptShape.Type = msoLinkedOLEObject Then
Dim name, path1, path2, source, begin, search1, cells As String
Dim limit1 As Integer
name = pptShape.LinkFormat.SourceFullName
limit1 = InStr(1, name, "!")
cells = Right(name, Len(name) - limit1)
search1 = "subfoldername"
path1 = Application.ActivePresentation.FullName
begin = InStr(1, path1, search1)
begin = Left(path1, begin - 1)
file1 = Dir(begin & "*.xlsm")
source = begin & file1
End If
path2 = source & "!" & cells
pptShape.LinkFormat.SourceFullName = path2
'update method. code line where generate error
pptShape.LinkFormat.Update
End If
Next
Next
'Update the links (If I use this method on OneDrive folder, it doesn't work and broke all the links because replace the Link name with only the excel file name, not the sheets and cells)
' pptPresentation.UpdateLinks
Call Shell("cmd.exe /S /C" & "start %LOCALAPPDATA%\Microsoft\OneDrive\OneDrive.exe /background")
Set pptPresentation = Nothing
Set pptSlide = Nothing
Set pptShape = Nothing
Application.DisplayAlerts = ppAlertsAll
End Sub
Good morning everyone.
As I have not seen the solution, I'd like to add my 2 cents.
I have had a similar issue, on a win10 Platform running Office 365.
In my case both files are on the same laptop.
I have seen that the powerpoint VBA procedure to update the path takes a long time by default. ( around 4 Minutes for me as there are 22 linked Objects).
One can speed it up by manually open the target excel file before launching the Powerpoint VBA.
It becomes effectively faster but I hit the issue where for each link the ppt vba procedure tries to update, we get a pop up window telling us that Excel can't open 2 files with same name.
I've tried to add in the PowerPoint VBA procedure : Application.DisplayAlerts = False , but is logically inefficient as applies to the PPT application and not to the Excel app !
I finally found one quick (and logic) solution :
at the beginning of the PowerPoint VBA, I ask user to locate the target excel file :
Set XlApp = CreateObject("Excel.Application")
ExcelFile = XlApp.GetOpenFilename(, , "Would you please locate your excel File")
And after, I just Open the target file, and set it with displayLAerts to False.
XlApp.Visible = True
Set xlWorkbook = XlApp.Workbooks.Open(ExcelFile, True, False)
Doing so, I no longer get warnings.
Full source code available .
Wish you a nice day !

Excel Reports generated by Microsoft Access routine gets Error 1004: Method Open Object Workbooks Failed

I have several Excel reports reports that are launched on demand by buttons on a MS Access database application. The routine that launches these reports has worked fine for years with no issues, until last week when our share drive hit storage capacity.
Please note, I use a convention of a ready-made Excel Workbook that has most of the formatting to produce the final report, and adding the data to it by using VBA with the Excel Object library to build my final report. I call these "Templates" not to be associated in anyway with Microsoft Word template conventions. To avoid confusion, I will mark my reference to this convention throughout this description as Template***
The errors have become significantly less frequent since share drive space was freed up by the IT team here, but for about 30% of users, the following error is still returned when launching an excel download: "Error 1004: Method Open Object Workbooks Failed".
The line of code where the error hits has never had issues before:
Set WB = xlApp.Workbooks.Open(strPathToTemplate)
Where strPathToTemplate is the share drive path where the excel Template*** is saved.
After many calls with our IT, one help desk person applied the following solution:
Navigate to ,locate a Microsoft Macro-Enabled Word Template file titled "Normal.dotm" and rename it to "Old.Normal.dotm". This IMMEDIATELY restored the functionality of the excel report downloads from the dashboard. The help desk person couldn't/wouldn't explain how they knew this was the issue or why it affected the excel downloads.
The problem now is that although this solution works for every user I've applied it to, it's also temporary. Every time the user reboots, the normal.dotm file restores itself and has to be renamed again or the 1004 error will appear in the dashboard again.
I've called back to the help desk and haven't gotten any farther with an explanation or a more permanent solution.
My biggest question (aside from how to permanently solve this) is why does this MS Word normal.dotm file have any affect at all on excel files launched from the MS Access database? There are zero instances in the programming where we refer to this roaming templates file path and we don't use Word at all. I can find plenty online about how the normal.dotm file can cause problems in Word, but nothing on how it can affect other Microsoft applications other than Word.
Again, the convention I use to produce my Excel reports even though I call them Template*** has nothing to do with normal.dotm. I can't help but think that this IT help desk introduced a different problem.
Things I've tried:
1. Freeing more share drive space
2. Deleting all instances of temp files from the share drive
3. Compact and Repair on Access
4. using new excel Template*** files
5. Rewriting paths of excel Template***
6. ensuring there are no personal macros in MS word
7. Rewriting the procedure that creates the excel reports to do early binding instead of late binding
8. Rebooting several times on different computers to prove that restoration of the normal.dotm file is what causes the errors to return in the dashboard
9. Testing the dotm file renaming solution on other users' computers.
I provide as much of the vba code that may be in question below
Here is the main vba for the launch of our Status of Funds report where I use a formatted Excel workbook Template*** to produce the report by 'marrying' it to the data.
Sub CreateSOFRpt(strPathtoTemplate As String, bEOM As Boolean)
Dim strWHERE As String
Dim strSQL As String
Dim strSQL1 As String
Dim strSQL2 As String
Dim strSavePath As String
strSavePath = Environ$("UserProfile") & "\Documents\Status of Funds as of " & datestring & ".xlsm"
'This first part of the IF statement is launched only when bEOM (end of month reports) = true and if the user chooses to launch the reports.
'There are no data restrictions here because the only people who can launch end of month are the Comptroller's personnel
If bEOM = True Then
strSQL = "SELECT * FROM tbl_SOF_TRUECOMM IN '" & SharedRoot & "\02_Engines\SABRS.accdb';"
strSQL1 = "SELECT * FROM tbl_SOF_TRUECOMM IN '" & SharedRoot & "\02_Engines\1EXP_YR\SABRS.accdb';"
strSQL2 = "SELECT * FROM tbl_SOF_TRUECOMM IN '" & SharedRoot & "\02_Engines\2EXP_YR\SABRS.accdb';"
Call CreateExcel("Status of Funds_EndofMonth", strSavePath, strSQL, strPathtoTemplate, "PivotTable1", "MainCurrent", "Raw", _
"Raw1", "PivotTable2", "Main1EXP", strSQL1, "Raw2", "PivotTable3", "Main2EXP", strSQL2)
Else
strWHERE = GetBEA(AcquireUser)
Select Case strWHERE
Case "ALL"
strSQL = "SELECT VAL([FY FULL]) AS [FY FULL_], MRI, ARI, SRI, WCI, BEA, BESA, BSYM, SBHD, [FUND FUNC], BLI, [DIR BEA BESA RCVD BAL ITD AMT], " _
& "[TrueComm], [OBL ITD AMT], [EXP ITD AMT], [LIQ ITD AMT], [UNCMT AMT], [UNOBL AMT], WCI_Desc, Organization " _
& "FROM tbl_SOF_TrueComm;"
Case "ZZ"
MsgBox "Please see Admin to get access to section you are responsible for.", vbInformation, "Permission required"
Exit Sub
Case Else
strSQL = "SELECT VAL([FY FULL]) AS [FY FULL_], MRI, ARI, SRI, WCI, BEA, BESA, BSYM, SBHD, [FUND FUNC], BLI, [DIR BEA BESA RCVD BAL ITD AMT], " _
& "[TrueComm], [OBL ITD AMT], [EXP ITD AMT], [LIQ ITD AMT], [UNCMT AMT], [UNOBL AMT], WCI_Desc, Organization " _
& "FROM tbl_SOF_TrueComm " _
& "WHERE BEA " & strWHERE & ";"
End Select
Call CreateExcel("Status of Funds", strSavePath, strSQL, strPathtoTemplate, "PivotTable1", "Main", "Raw")
End If
End Sub
Here is the CreateExcel routine referred to above
Sub CreateExcel(strRptTitle As String, strSavePath As String, Optional strQueryName As String, Optional strPathtoTemplate As String, Optional strPivotName As String, Optional strSheetName As String, Optional strRawSheetName As String, _
Optional strRawSheetName1 As String, Optional strPivotName1 As String, Optional strSheetName1 As String, Optional strQueryname1 As String, _
Optional strRawSheetName2 As String, Optional strPivotName2 As String, Optional strSheetName2 As String, Optional strQueryname2 As String)
'strQueryName = the query the raw data is sourced from
'strRptTitle = the name of the file after it is generated
'strPathtoTemplate = the directions to the template file for the excel
'strSavePath = the final save location of the completed excel file
'strPivotName = the title of the pivot table to refresh
'strSheetname = the title of the sheet where the pivot is
'any optional variable ending in a number (e.g, strSheetName2) refers to when an excel needs to be created with multiple raw data sheets and pivot tables.
'It allows the routine to expand and be more flexible when necessary
'this routine was originally just used to add excel files to KPI emails, now we call it from Form Choose and use it to generate email reports
Dim xlApp As Object
Dim WB As Object
Dim xlSheet As Object
Dim xlSheet1 As Object
Dim intCOL As Integer
Dim rs As DAO.Recordset
Dim fld As Variant
Dim db As DAO.Database
Dim pt As PivotTable
Set db = CurrentDb
Set xlApp = CreateObject("Excel.Application")
Set WB = xlApp.Workbooks.Open(strPathtoTemplate)
xlApp.Visible = False
'Generates the initial sheet, query, etc
Set xlSheet = WB.Sheets(strRawSheetName)
Set rs = db.OpenRecordset(strQueryName)
'PLACE
intCOL = 1
For Each fld In rs.Fields
xlSheet.Cells(1, intCOL).Value = fld.Name
intCOL = intCOL + 1
Next
With xlSheet
.Rows("2:" & xlSheet.Rows.Count).ClearContents
.Range("A2").CopyFromRecordset rs
.Cells.EntireColumn.AutoFit
End With
Set xlSheet = WB.Sheets(strSheetName)
'we could set the template to refresh on opening, but it won't refresh if someone uses outlook previewer. Better to make the excel file refresh before it ever gets sent.
Set pt = xlSheet.PivotTables(strPivotName)
pt.RefreshTable
'If a second sheet and query needs to be created, then:
'The first part of this If statement checks to see if the optional variable has been provided
'If it hasn't been provided (denoted by whether strRawSheetName1 is = to nothing) then do nothing because the place it's called from doesn't require a second sheet
'If it has been provided, then place the raw data from the query and autofit everything
If strRawSheetName1 = "" Then
Else
Set xlSheet = WB.Sheets(strRawSheetName1)
Set rs = db.OpenRecordset(strQueryname1)
'PLACE
intCOL = 1
For Each fld In rs.Fields
xlSheet.Cells(1, intCOL).Value = fld.Name
intCOL = intCOL + 1
Next
With xlSheet
.Rows("2:" & xlSheet.Rows.Count).ClearContents
.Range("A2").CopyFromRecordset rs
.Cells.EntireColumn.AutoFit
End With
Set xlSheet = WB.Sheets(strSheetName1)
'we could set the template to refresh on opening, but it won't refresh if someone uses outlook previewer. Better to make the excel file refresh before it ever gets sent.
Set pt = xlSheet.PivotTables(strPivotName1)
pt.RefreshTable
End If
'If a third sheet and query needs to be created, then:
If strRawSheetName2 = "" Then
Else
Set xlSheet = WB.Sheets(strRawSheetName2)
Set rs = db.OpenRecordset(strQueryname2)
'PLACE
intCOL = 1
For Each fld In rs.Fields
xlSheet.Cells(1, intCOL).Value = fld.Name
intCOL = intCOL + 1
Next
With xlSheet
.Rows("2:" & xlSheet.Rows.Count).ClearContents
.Range("A2").CopyFromRecordset rs
.Cells.EntireColumn.AutoFit
End With
Set xlSheet = WB.Sheets(strSheetName2)
'we could set the template to refresh on opening, but it won't refresh if someone uses outlook previewer. Better to make the excel file refresh before it ever gets sent.
Set pt = xlSheet.PivotTables(strPivotName2)
pt.RefreshTable
End If
'cleanup
WB.SaveCopyAs strSavePath
WB.Close SaveChanges:=False
Set xlSheet = Nothing
Set pt = Nothing
Set rs = Nothing
Set WB = Nothing
Set xlApp = Nothing
Set db = Nothing
End Sub
(Sorry if my idea is stupid).
May-be is it related to a recent update of Windows or Office, so that the variable "strPathToTemplate" would become an internal or system variable name (for MS Word specificly), generating ambiguity with "Open" objet. Could you test just changing the name of that variable ?
(In fact, I hope this will not be the solution...).
Pierre.
I had similar issue and since than I use this snipped to open Excel (note the comma in GetObject):
'Start Excel
On Error Resume Next
Set oExcel = GetObject(, "Excel.Application") 'Bind to existing instance of Excel
If Err.Number <> 0 Then 'Could not get instance of Excel, so create a new one
Err.Clear
On Error GoTo Error_Handler
Set oExcel = CreateObject("Excel.Application")
bExcelOpened = False
Else 'Excel was already running
bExcelOpened = True
End If
On Error GoTo Error_Handler

Run Time Error 5 - Invalid Procedure Call or Argument - While Saving Word Document

I've been trying to solve this error for a while, and nothing I've found online has seemed to help. Basically I'm running a script in excel VBA that opens a word document, and then opens the Save As dialog box so that I can save the file with the name / location of my choosing. It's at this point that I get the run time error 5.
I'm using the the Do Loop to try to get around the error, and it worked for a while. But for some reason the problem is back now and I have no idea why.
I've added a Do Loop that keeps the code from moving forward until the file has a name. This worked for a while but mysteriously stopped working after a few hours
Dim wdApp As Word.Application
Dim wdDoc As Word.Document
Set wdApp = GetObject(, "Word.Application")
If Err.Number <> 0 Then 'Word isn't already running
Set wdApp = CreateObject("Word.Application")
End If
On Error GoTo 0
Set wdDoc = wdApp.Documents.Open(OFile)
'Clear the variable that contains the file path/name
SveReportName = ""
'Save word document as a new file
Set SveReport = wdApp.ActiveDocument.Application.FileDialog(msoFileDialogSaveAs)
With SveReport
' 3 is for 97-2003 - include for 2010, remove for 2003
.FilterIndex = 3
.Show
SveReportName = SveReport.SelectedItems.Item(1)
'This Do statement is here so that VBA just keeps adding 1+1 until the user has had time to name the tech report file, it should stop Run Time Error 5 from appearing
Do
a = 1 + 1
Loop Until IsNull(SveReportName) = False
wdDoc.SaveAs SveReportName
End With
Any idea what else might be going on?
Thanks in advance!
Have a read of the MS docs on the object you're using, and specifically its note about the execute method which should be used right after the show method for a Save As:
https://learn.microsoft.com/en-us/office/vba/api/office.filedialog.show
I think it reduces your 'save' code to something like this, though I haven't tried it:
'Save word document as a new file
Set SveReport = wdApp.ActiveDocument.Application.FileDialog(msoFileDialogSaveAs)
With SveReport
.FilterIndex = 3
if .Show = -1 then .Execute
End With

VBA does not save changes to Outlook Template that show up with .Display

I am working on generating OFT files that will be e-mailed to customers who will then fill the To: and Subject: in and send them as e-mails to their clients.
My data comes from an Excel Workbook with one sheet containing static data (Books) and another information pasted in by the user (Pins). I've got a basic template that has placeholder text which gets replaced by the data in the aforementioned Excel sheets.
One important part of this is that I need the changed template to get saved to it's own file, so it can be stored for reference later. Originally I had the code below setup to open the template and call .SaveAs myFilename, olTemplate but that just made a broken 3KB file. You will notice I am copying the template to the actual destination file and operating on that instead.
My problem is that if I have the template item call .Display, everything is perfect. I see my image in the right place and all of the text is properly replaced. If I call .Save it saves out a copy of the original OFT template with no changes present.
Can anyone tell me what I'm doing wrong here? I've been searching here and google for hours trying to find some indication of what I'm missing. I'm trying to automate this thing as much as possible. Resaving the new OFT with Outlook's UI is a real time sink for a coworker and I'd like to eliminate that if possible. They're going to be generating dozens of these OFTs every day, so the work seems worth it in my opinion.
UPDATE
I have managed to get this to work but the solution feels like a half-answer. The code below has been updated with changes that properly save the OFT.
Here is my sub:
Sub OutlookTemplate(ByVal pins As Range, ByVal book As Range, ByVal ImageLocation As String)
Dim myolapp As Object
Dim myItem As Object
Set myolapp = CreateObject("Outlook.Application")
'myolapp.Session.Logon
For Each p In pins.Cells
If Not IsEmpty(p.Value) Then
Dim myFilename As String
myFilename = "c:\temp\" & Worksheets("PINS").Range("A2") & "-" & p.Value & ".oft"
FileCopy "c:\template.oft", myFilename
Set myItem = myolapp.CreateItemFromTemplate(myFilename)
myItem.Save <- Added immediate save after creation of myItem
myItem.Attachments.Add ImageLocation, olByValue, 0
myItem.HTMLBody = Replace(myItem.HTMLBody, "THEIMAGE", "<img src='cid:" & book.Cells(2).Value & "'" & "width='154'>")
myItem.HTMLBody = Replace(myItem.HTMLBody, "PINHERE", p.Value)
myItem.HTMLBody = Replace(myItem.HTMLBody, "THETITLE", book.Cells(1).Value)
myItem.HTMLBody = Replace(myItem.HTMLBody, "THESUBTITLE", book.Cells(3).Value)
myItem.HTMLBody = Replace(myItem.HTMLBody, "THEAUTHORS", book.Cells(4).Value)
myItem.HTMLBody = Replace(myItem.HTMLBody, "THEDESCRIPTION", book.Cells(5).Value)
' Leaving the next line off results in a broken image
' when .SaveAs is called
myItem.Display
' This saves all of the changes out to the file properly
' in combination with .Display
' Note: if I call myItem.SaveAs myFilename, olTemplate
' I get the 3KB broken OFT. Omitting ,olTemplate works
myItem.SaveAs myFilename
End If
Next
End Sub
The Save method doesn't propagate changes to the .oft file. It saves the Microsoft Outlook item to the current folder or, if this is a new item, to the Outlook default folder for the item type.
Try to open the existing .oft file without copying it anywhere. Then do the required changes and call the SaveAs method to save it as a template wherever you need.

Calling an Excel sheet in VBScript

I have the following code:
Option Explicit
Randomize
Dim a, song, album
a = Int((Rnd*195)+1)
song = "B" & a
album = "A" & a
Dim objApp, objWbs, objWorkbook, objSheet
Set objApp = CreateObject("Excel.Application")
Set objWbs = objApp.WorkBooks
objApp.Visible = False
Set objWorkbook = objWbs.Open("C:\Users\Name\Documents\Music.xlsx")
Set objSheet = objWorkbook.Sheets("Sheet1")
song = objSheet.Range(song).Value
album = objSheet.Range(album).Value
objWorkbook.Close False
objWbs.Close
objApp.Quit
Set objSheet = Nothing
Set objWorkbook = Nothing
Set objWbs = Nothing
Set objApp = Nothing
MsgBox("Album name: " & album & vbNewLine & "Song name: " & song)
It prints two random cells between row 1 and row 195 from the Excel sheet "Music". One of them - the one in column A - represents the album, and the other represents the song. The problem is that it takes quite a long time to return the results, about 20 seconds.
I was wondering whether there was a more efficient method I could use to get the results more quickly.
I think Ansgar Wiechers' answer is probably correct that starting Excel is the slowest part of the script. You could try using ADO to connect to the Excel file as if it were a database. This would avoid starting Excel:
Option Explicit
Randomize
Dim conn, rst, song, album
Set conn = CreateObject("ADODB.Connection")
conn.Open "Provider=Microsoft.ACE.OLEDB.12.0;" & _
"Data Source=C:\Users\Name\Documents\Music.xlsx;" & _
"Extended Properties='Excel 12.0 Xml;HDR=NO';"
' Select a random record; reference https://stackoverflow.com/a/9937263/249624
' Asc(album) is just a way to get some numeric value from the existing data
Set rst = conn.Execute("SELECT TOP 1 F1 AS album, F2 as song FROM [Sheet1$] ORDER BY Rnd(-(100000*Asc(F1))*Time())")
If rst.EOF Then
song = "[NO RECORDS]"
album = "[NO RECORDS]"
Else
song = rst("song").Value
album = rst("album").Value
End If
MsgBox("Album name: " & album & vbNewLine & "Song name: " & song)
The one possible snag here is that VBScript is run by default using the 64-bit version of wscript.exe, and the 64-bit ACE.OLEDB is only available if you installed the 64-bit version of Office 2010 or higher. This can be worked around, though, by running the script with the 32-bit version of wscript.exe (e.g., see How do I run a VBScript in 32-bit mode on a 64-bit machine?).
If you decide to go this route and can control the input Excel file, I would recommend adding a header row to the spreadsheet and changing HDR=NO to HDR=YES in the connection string. That way, you can refer to the columns by name in the query (e.g., SELECT TOP 1 album, song ...) instead of relying on the "F1" syntax.
The most time-consuming steps in your script are most likely
starting Excel and
opening the workbook.
One thing you could do is using an already running Excel instance instead of creating a new one all the time:
quitExcel = False
On Error Resume Next
Set objApp = GetObject(, "Excel.Application")
If Err Then
Set objApp = CreateObject(, "Excel.Application")
quitExcel = True
End If
On Error Goto 0
The variable quitExcel indicates whether you need to close Excel at the end of your script (when you created a new instance) or not (when you used an already running instance).
You could also check if the workbook is already open:
wbOpen = False
For Each wb In objWbs
If wb.Name = "Music.xlsx" Then
Set objWorkbook = wb
wbOpen = True
Exit For
End If
Next
If Not wbOpen Then
Set objWorkbook = objWbs.Open("C:\Users\Name\Documents\Music.xlsx")
End If
Other than that your only options are changing the way the data is stored or buying faster hardware, AFAICS.
Cheran, I disagree with the answers here.
I just ran your script on my 5 year old laptop, and got the answer in about 2 seconds. Whether an instance of Excel was already open made no difference in run time.
(I created a test Music.xlsx spreadsheet by entering "A1" in cell A1, and "B1" in cell B1, and dragged those cells down to row 195 to get a nice set of unique sample data).
Why don't you make Excel visible when it runs, so that you can see for yourself what is going on?
You might see, for example, that Excel takes one second to open, and the Excel Add-ins you have are taking the other fifteen seconds to initialize. It's also possible that your machine and/or hard drive is slow and does indeed take 20 seconds to run this. Who knows...
To get some insight, please make objApp.Visible = True and rerun.
You might also comment out the final eight lines, except for the MsgBox line so that your Excel file stays open after script is done, so that you might see other clues.
Other observations:
1) Your method of opening Excel with CreateObject from a .vbs script seems to be the most reliable/accepted method of automating Excel.
2) It's not stated here HOW you are running the .vbs script (command line vs. double-click from Explorer). Your script is running, but be aware that using cscript.exe to run the .vbs is also common when people try to automate this.
3) I'm not used to seeing an external vbs interact with the data inside Excel...I'm used to having vbs open Excel.xlsm, then letting a Macro do the number crunching. But, Macros bring an entirely different set of headaches. I'm not saying your method is good or bad...just not used to that approach.
Good luck!

Resources