Excel Crashes When Opening File A Second Time - vba

UPDATE 2 (3/21/17)
I have discovered that trying to open an Excel book after hitting the Submit button once happens only when one (or all) of the imported sheets from the module are deleted. (The process removes the old sheets before re-submitting to clear out the workbook to start over). For a manual test, I hit the submit button, delete any of the imported worksheets then try to open any excel file and it crashes. I have also ensured all defunct named ranges are removed when the sheet is deleted. I also tested this on a file that just imports a blank sheet. Then I delete it and am able to open workbook just fine. I'd like to avoid having to create my module (since it's a kind of a drag).
Original Question
I have an Excel Workbook Tool that opens other Excel Workbooks and imports worksheets from those workbook after processing some information.
In total there are 5 module workbooks. At a high level these workbooks are the same - sheet structure, general code structure, etc. There are differing formulas and some named ranges are different, etc.
In the main tool, there is the ability to re-run the code that pulls the information from the different workbooks. It essentially resets the original workbook and then runs the code again. This is done without closing the original workbook. (A user can refresh web-service data and re-run the tool).
The problem I am facing is that when I re-run the process for two of the modules workbooks Excel crashes during the re-run at that point where the code attempts to open the module workbook. The other 3 module workbooks work great. I can run and re-run and re-run ... The other 2 crash every time.
I have done a ton a research on the files to see why this could happen, but have not found out why. There are no links left in the main workbook after the process runs, no data connections, no bad links etc.
Also, the interesting thing is that the files I store in the UAT environment folder work fine all the time. The files in the production folder fail. I even copied the files directly from the UAT environment folder to the production environment folder and it still fails. I have also ruled out permission and security at the folder level.
I can also open the file manually after submitting the code the first time.
I realize this may be slightly out of scope for SO and a little vague but was hoping someone may have had a similar experience and could shed some light.
Update
The relevant code is below. Based on the comments by #Ralph I forced a memory wipe by adding the line Set wbLOB = Nothing, but unfortunately, issue still happens.
Function LoadLOB(sLOB As String, sXMLFile As String) As Boolean
Dim sLOBFile As String
sLOBFile = wsReference.Range("ModuleFolder").Value2 & sLOB & "\" & sLOB & ".xlsb"
Dim wbLOB As Workbook
Set wbLOB = Workbooks.Open(sLOBFile) '--> 2nd run crashes on this line.
If TieXMLToExcel(wbLOB, sXMLFile, sLOB) Then
MapXMLFieldsToExcelCells wbLOB, sLOB
Select Case sLOB
Case Is = "Property"
SortTableByAscendingColumn wbLOB, "xml" & sLOB, "tCommonLocationProperty", "Location_ID"
SortTableByAscendingColumn wbLOB, "xml" & sLOB, "tLocationByCoverageTypeProperty", "Location_ID"
Case Is = "GeneralLiability": SortTableByAscendingColumn wbLOB, "xml" & sLOB, "tClassCodesByLocationGeneralLiability", "Location_ID"
Case Is = "CommercialAuto": SortTableByAscendingColumn wbLOB, "xml" & sLOB, "tVehicleSummaryCommercialAuto", "AuVehicleNo"
Case Is = "Crime": SortTableByAscendingColumn wbLOB, "xml" & sLOB, "tCommonLocationCrime", "Location_ID"
End Select
Application.Run wbLOB.Name & "!PrepareSheetForMasterFile", ThisWorkbook
wbLOB.Close False
LoadLOB = True
End If
Set wbLOB = Nothing
End Function

I doubt this is the answer, but I figured this is a better forum for exchanging the ideas I had on this issue. What I did is I grabbed some windows APIs to check to see if that file is open before trying to open it again. I also added a method to close the file, and I made the SaveChanges parameter more explicit. I also added a few DoEvents in there, in case something is waiting to finish.
Hopefully this is a launching pad for other ideas. I hope some of this helps.
'Determine whether a file is already open or not
#If VBA7 And Win64 Then
Private Declare PtrSafe Function lOpen Lib "kernel32" Alias "_lopen" (ByVal lpPathName As String, ByVal iReadWrite As Long) As Long
Private Declare PtrSafe Function lClose Lib "kernel32" Alias "_lclose" (ByVal hFile As Long) As Long
#Else
Private Declare Function lOpen Lib "kernel32" Alias "_lopen" (ByVal lpPathName As String, ByVal iReadWrite As Long) As Long
Private Declare Function lClose Lib "kernel32" Alias "_lclose" (ByVal hFile As Long) As Long
#End If
Function LoadLOB(ByVal sLOB As String, _
ByVal sXMLFile As String) As Boolean
Dim sLOBFile As String
Dim wbLOB As Workbook
sLOBFile = wsReference.Range("ModuleFolder").Value2 & sLOB & "\" & sLOB & ".xlsb"
'Make sure the file is closed before processing
If Not isFileOpen(sLOBFile) Then
Set wbLOB = Workbooks.Open(sLOBFile, 0, False)
Else
'Close it if it is open
closeWB sLOBFile
Set wbLOB = Workbooks.Open(sLOBFile, 0, False)
End If
If TieXMLToExcel(wbLOB, sXMLFile, sLOB) Then
MapXMLFieldsToExcelCells wbLOB, sLOB
Select Case sLOB
Case Is = "Property"
SortTableByAscendingColumn wbLOB, "xml" & sLOB, "tCommonLocationProperty", "Location_ID"
SortTableByAscendingColumn wbLOB, "xml" & sLOB, "tLocationByCoverageTypeProperty", "Location_ID"
Case Is = "GeneralLiability": SortTableByAscendingColumn wbLOB, "xml" & sLOB, "tClassCodesByLocationGeneralLiability", "Location_ID"
Case Is = "CommercialAuto": SortTableByAscendingColumn wbLOB, "xml" & sLOB, "tVehicleSummaryCommercialAuto", "AuVehicleNo"
Case Is = "Crime": SortTableByAscendingColumn wbLOB, "xml" & sLOB, "tCommonLocationCrime", "Location_ID"
End Select
Application.Run wbLOB.Name & "!PrepareSheetForMasterFile", ThisWorkbook
DoEvents
wbLOB.Close SaveChanges:=False
LoadLOB = True
End If
Set wbLOB = Nothing
End Function
Sub closeWB(ByVal FilePath As String)
Dim wb As Workbook
For Each wb In Application.Workbooks
If wb.FullName = FilePath Then
wb.Close SaveChanges:=False
Set wb = Nothing
DoEvents
Exit For
End If
Next
End Sub
Function isFileOpen(ByVal FileName As String) As Boolean
Dim FileNumb As Long: FileNumb = -1
Dim lastErr As Long
FileNumb = lOpen(FileName, &H10)
'Determine if we can open the file
If FileNumb = -1 Then
lastErr = Err.LastDllError
Else
lClose (FileNumb)
End If
' Check if there is a sharing violation and report back status
isFileOpen = (FileNumb = -1) And (lastErr = 32)
End Function

Not sure how helpful this answer will be, but I re-created the module files from scratch and they worked in all environments. Most likely a small corruption in the file that I could not find.

Related

Open Folder from Cell in Excel, Create Folder if it Does Not Exist

I am trying to create a cell in my spreadsheet that will do a few things:
Open a folder based on values from two cells in a row
Create the folder from above two cells if it does not exist
Only work when I click on it for that row
Currently, I am using the Hyperlink formula to link to the folder that I manually create. I had the bright idea of linking to a batch file that opens/creates the folder from parsed data. I tried that for a good while, but have not been able to get the data from excel to the batch file.
Anyway to do this? Either in the way I described or with VBA?
My actual spread sheet has many more columns and rows, but hopefully the image below illustrates how I would like the link to the folder laid out.
Basically, I want to click "Open" in that row and it take the data from B2 and C2 and open/create a folder in C:\New Folder\B2\C2 (ex. C:\New Folder\2015\Folder 0001).
Below is the hyperlink formula I am using in Excel currently to try and accomplish this:
=HYPERLINK("C:\New folder\new.bat "&B2&" "&C2,"Open")
I get an "Cannot open the specified file" error. If I remove the cell data, it will open the program, but without the data, I have no way to create the necessary folders.
Below is the batch file I wrote to open/create the folder:
#echo off
set dir="C:\New folder\%1\%2"
if not exist %dir% mkdir %dir%
start "" %dir%
This works fine on it's own when run from the command line with the following:
new.bat 2015 Folder 0001
Any direction or help will be greatly appreciated. Thank in advance.
Best way to approach this is probably with VBA and without shell script.
Assuming your three cells are A1, A2 and A3. You can add a button to you sheet and assign a macro like this:
Sub btn1_click()
Dim dir as String
Dim fso As Object
dir = Range("A1").Value & "\" & Range("A2").Value & Range("A3").Value
Set fso = CreateObject("scripting.filesystemobject")
If Not fso.folderexists(dir) Then
fso.createfolder (dir)
End If
Call Shell("explorer.exe" & " " & dir, vbNormalFocus)
End Sub
I'm not sure what is in your cells. But you can build a path from the values like this.
Dim ws As Excel.Worksheet
Set ws = ActiveWorkbook.Sheets("Sheet1")
Dim szPath as string
szPath = ws.Range("B1").Value & "\" & ws.Range("C1").Value
Open a folder
To open a folder you can use shellexecute. Declare this at the top of all you code. Above all subs and functions.
Private Declare Function ShellExecute Lib "Shell32.dll" Alias "ShellExecuteA" _
(ByVal hwnd As Long, _
ByVal lpOperation As String, _
ByVal lpFile As String, _
ByVal lpParameters As String, _
ByVal lpDirectory As String, _
ByVal nShowCmd As Long) As Long
Then you can send it a path
ShellExecute 0, "open", "C:\Temp", 0, 0, 1
'or send it the value build from the cells
ShellExecute 0, "open", szPath , 0, 0, 1
You can also open files this way
Create files
If you want to create files it can be done like this.
Dim fs As FileSystemObject
Set fs = New FileSystemObject
'Create a text file
Set ts = fs.CreateTextFile("C:\Temp\test.txt", True, False)
'or using the path from the cells
Set ts = fs.CreateTextFile(szPath & "\text.txt", True, False)
Create folders
And lastly you can create folders.
fs.CreateFolder ("C:\Temp\SomeNewDir")
'or using the path from the cells
fs.CreateFolder (szPath & "\SomeNewDir")

How to prevent free distribution of a commercial Excel spreadsheet

I'm not sure if this is more appropriate under a different SE site (Super User?).
I want to build and sell a complex macro driven driven spreadsheet to a certain vertical. I am mainly concerned about free/unauthorised distribution between customers within that vertical.
I can see that there is a few obscure products on the market that might be able to do what I want, but the few reviews that i've been able to find haven't been favourable.
One vendor however lists that free distribution can circumvented by either:
Using a key generator to create license codes
Using the online activation feature
Or by simply using an encrypted password
Is anyone aware of any guidelines/frameworks (any language) for me to build my own solution to achieve this, namely requiring licence codes or online activation?
If this is generally a difficult endeavour, is there a commercial product that anyone recommend?
I'm also thinking the complexities involved in achieving this might push me to building a small SaaS application instead. Am I better off just going that route?
I have created an Excel sheet that I could remotely remove access to if a monthly subscription payment failed. Here is how to accomplish this:
Create and HTML table and upload it to your website
Within your Excel doc go to the data tab and select get from web - import your table into a sheet called "Verify" - make sure your table has 3 columns. Serial Number is in the first column, description of user in 2nd, and your error message in the top of col 3. The error message stored here is what every user that isn't registered will see. The first serial number should appear in cell A2 of the sheet Verify.
Within your Visual Basic editor paste this code into a Module - This code will return an 8 digit serial number based on a PC's Hard Drive serial number:
Function HDSerialNumber() As String
Dim fsObj As Object
Dim drv As Object
Set fsObj = CreateObject("Scripting.FileSystemObject")
Set drv = fsObj.Drives("C")
HDSerialNumber = Left(Hex(drv.SerialNumber), 4) _
& "-" & Right(Hex(drv.SerialNumber), 4)
End Function
Also in another module I make sure the Internet is connected. If no Internet then the sheet closes. If you don't do this then if someone disconnects from the Internet your serials won't be loaded.
Option Explicit
#If VBA7 And Win64 Then
Private Declare PtrSafe Function InternetGetConnectedStateEx Lib "wininet.dll" (ByRef lpdwFlags As Long, ByVal lpszConnectionName As String, ByVal dwNameLen As Integer, ByVal dwReserved As Long) As Long
#Else
Private Declare Function InternetGetConnectedStateEx Lib "wininet.dll" (ByRef lpdwFlags As Long, ByVal lpszConnectionName As String, ByVal dwNameLen As Integer, ByVal dwReserved As Long) As Long
#End If
Function IsInternetConnected() As Boolean
Dim strConnType As String, lngReturnStatus As Long, MyScript As String
If Application.OperatingSystem Like "*Macintosh*" Then
MyScript = "repeat with i from 1 to 2" & vbNewLine
MyScript = MyScript & "try" & vbNewLine
MyScript = MyScript & "do shell script ""ping -o -t 2 www.apple.com""" & vbNewLine
MyScript = MyScript & "set mystatus to 1" & vbNewLine
MyScript = MyScript & "exit repeat" & vbNewLine
MyScript = MyScript & "on error" & vbNewLine
MyScript = MyScript & "If i = 2 Then set mystatus to 0" & vbNewLine
MyScript = MyScript & "end try" & vbNewLine
MyScript = MyScript & "end repeat" & vbNewLine
MyScript = MyScript & "return mystatus"
If MacScript(MyScript) Then IsInternetConnected = True
Else
lngReturnStatus = InternetGetConnectedStateEx(lngReturnStatus, strConnType, 254, 0)
If lngReturnStatus = 1 Then IsInternetConnected = True
End If
End Function
Then inside the Workbook_Open area paste this:
Private Sub Workbook_Open()
If IsInternetConnected Then
Dim objFSO As Object
Dim MyFolder As String
Dim sFileName As String
Dim iFileNum As Integer
Dim sBuf As String
Dim trialstartdate As String
Dim z As String
Dim fsoFSO
Set fsoFSO = CreateObject("Scripting.FileSystemObject")
'UNCOMMENT below to SHOW the serials sheet when the workbook is opened
ActiveWorkbook.Sheets("Verify").Visible = xlSheetVisible
'UNCOMMENT below to hide the serials sheet when the workbook is opened
'ActiveWorkbook.Sheets("Verify").Visible = xlSheetVeryHidden
Refresh_Serials
z = 2
'loop here for valid hard drive serial number
Do Until IsEmpty(Worksheets("Verify").Cells(z, 1).Value)
If Worksheets("Verify").Cells(z, 1).Value = HDSerialNumber Then
'verified and let pass
GoTo SerialVerified
End If
z = z + 1
Loop
Dim custommessage As String
custommessage = Worksheets("Verify").Cells(2, 3)
MsgBox custommessage + " Your serial number is: " + HDSerialNumber
Dim wsh1, MyKey1
Set wsh1 = CreateObject("Wscript.Shell")
MyKey1 = "%{TAB}"
wsh1.SendKeys MyKey1
MsgBox "The Commission Tracker will not open without a valid serial number. It will now close. uncomment this in workbook->open to close the workbook if the serial isn't found"
Application.DisplayAlerts = False
'uncomment this to close the workbook if the serial isn't found
'ActiveWorkbook.Close
Application.DisplayAlerts = True
SerialVerified:
' does the end user agree to not use this tool for mailicous purposes?
MsgAgree = MsgBox("Your PC's serial number is " & HDSerialNumber & ". By clicking 'Yes' you agree to use our software as described in our end user agreement. - the URL to your terms here", vbYesNo, "Final Agreement")
If MsgAgree = vbNo Then
'close program
MsgBox "This program will now close since you do not agree to our end user agreement"
Application.DisplayAlerts = False
ActiveWorkbook.Close
Application.DisplayAlerts = True
Else
'continue to open the program
End If
Else
MsgBox "No Network Connection Detected - You must have an internet connection to run the commission tracker."
Application.DisplayAlerts = False
ActiveWorkbook.Close
Application.DisplayAlerts = True
End If
End Sub
That should do it....
Create your own special unique license keys in a macro that will unlikely be generated in a key generator. Add your own prefix, for example. You could store if a user is using it in an online database. Downfall to this solution is that the users would have to be connected to the outside internet.
Then lock down that module with the keys by the following:
To protect your code, open the Excel Workbook and go to Tools>Macro>Visual Basic Editor (Alt+F11). Now, from within the VBE go to Tools>VBAProject Properties and then click the Protection page tab and then check "Lock project from viewing" and then enter your password and again to confirm it. After doing this you must save, close & reopen the Workbook for the protection to take effect.

Rename and save ActiveDocument with VBA

Is it possible to rename the activedocument (the word document that I'm running the macro from) with VBA?
Right now I'm saving my activedocument under a new name and then attempt to delete the original. The latter part won't go through, so the original never gets deleted.
Anyone know if this is even possible?
I spent a lot of time doing this recently, because I disliked having to delete previous files when I did "Save As" - I wanted a "Save as and delete old file" answer. My answer is copied from here.
I added it to the quicklaunch bar which works wonderfully.
Insert following code into normal.dotm template (found in C:\Documents and Settings\user name\Application Data\Microsoft\Templates for Windows 7 for Word)
Save normal.dotm
Add this to the quicklaunch toolbar in Word.
Optional - remap a keyboard shortcut to this
Optional - digitally sign your template (recommended)
Note this actually moves the old file to the Recycle Bin rather than trashing completely and also sets the new file name in a very convenient fashion.
Option Explicit
'To send a file to the recycle bin, we'll need to use the Win32 API
'We'll be using the SHFileOperation function which uses a 'struct'
'as an argument. That struct is defined here:
Private Type SHFILEOPSTRUCT
hwnd As Long
wFunc As Long
pFrom As String
pTo As String
fFlags As Integer
fAnyOperationsAborted As Long
hNameMappings As Long
lpszProgressTitle As Long
End Type
' function declaration:
Private Declare Function SHFileOperation Lib "shell32.dll" Alias "SHFileOperationA" (lpFileOp As SHFILEOPSTRUCT) As Long
'there are some constants to declare too
Private Const FO_DELETE = &H3
Private Const FOF_ALLOWUNDO = &H40
Private Const FOF_NOCONFIRMATION = &H10
Private Const FOF_SILENT = &H4
Function RecycleFile(FileName As String, Optional UserConfirm As Boolean = True, Optional HideErrors As Boolean = False) As Long
'This function takes one mandatory argument (the file to be recycled) and two
'optional arguments: UserConfirm is used to determine if the "Are you sure..." dialog
'should be displayed before deleting the file and HideErrors is used to determine
'if any errors should be shown to the user
Dim ptFileOp As SHFILEOPSTRUCT
'We have declared FileOp as a SHFILEOPSTRUCT above, now to fill it:
With ptFileOp
.wFunc = FO_DELETE
.pFrom = FileName
.fFlags = FOF_ALLOWUNDO
If Not UserConfirm Then .fFlags = .fFlags + FOF_NOCONFIRMATION
If HideErrors Then .fFlags = .fFlags + FOF_SILENT
End With
'Note that the entire struct wasn't populated, so it would be legitimate to change it's
'declaration above and remove the unused elements. The reason we don't do that is that the
'struct is used in many operations, some of which may utilise those elements
'Now invoke the function and return the long from the call as the result of this function
RecycleFile = SHFileOperation(ptFileOp)
End Function
Sub renameAndDelete()
' Store original name
Dim sOriginalName As String
sOriginalName = ActiveDocument.FullName
' Save As
Dim sFilename As String, fDialog As FileDialog, ret As Long
Set fDialog = Application.FileDialog(msoFileDialogSaveAs)
'set initial name so you don't have to navigate to
fDialog.InitialFileName = sOriginalName
ret = fDialog.Show
If ret <> 0 Then
sFilename = fDialog.SelectedItems(1)
Else
Exit Sub
End If
Set fDialog = Nothing
'only do this if the file names are different...
If (sFilename <> sOriginalName) Then
'I love vba's pretty code
ActiveDocument.SaveAs2 FileName:=sFilename, FileFormat:= _
wdFormatXMLDocument, LockComments:=False, Password:="", AddToRecentFiles _
:=True, WritePassword:="", ReadOnlyRecommended:=False, EmbedTrueTypeFonts _
:=False, SaveNativePictureFormat:=False, SaveFormsData:=False, _
SaveAsAOCELetter:=False, CompatibilityMode:=14
' Delete original (don't care about errors, I guess)
Dim hatersGonnaHate As Integer
hatersGonnaHate = RecycleFile(sOriginalName, False, True)
End If
End Sub

FileSystemObject code has started to throw an error

Not sure why but the following code has begun to throw an unknown error. When the macro is run Excel stops responding.
Why is this error occuring?
What is an alternative route with the same functionality?
This code is located within an Excel 2010 xlsm file on a Windows 7 machine.
Sub CopyFolderToCasinoDirectory()
'reference Microsoft Scripting Runtime
On Error Resume Next
Dim fso As Scripting.FileSystemObject
Set fso = New Scripting.FileSystemObject
fso.CopyFolder _
"\\xxxfileserve\department$\DBA\Opers\All Operators\yyy", _
"\\xxxfileserve\department$\DBA\Cas\yyy", _
True
On Error GoTo 0
Set fso = Nothing
End Sub
ok - I've changed the pathways so that it is attempting to move less files - and it hesitates but does eventually run through. I suspect that the above is failing because there are too many files in the directory specified? Currently there are 753 files - maybe too much?
RonDeBruin has given me lots of ideas of how to test or alter the logic. One possibility might be to use DeleteFolder first on the destination folder, and then CopyFolder the target folder over?
Sorry for replying so late. I was not able to get hold of network directories and I wanted to test the code before posting it :)
Try this. Run the Sub Sample() Does it still hang? You will also see the Files getting transferred in a Windows Dialog Box.
Private Declare Function SHFileOperation _
Lib "shell32.dll" Alias "SHFileOperationA" _
(lpFileOp As SHFILEOPSTRUCT) As Long
Private Type SHFILEOPSTRUCT
hWnd As Long
wFunc As Long
pFrom As String
pTo As String
fFlags As Integer
fAborted As Boolean
hNameMaps As Long
sProgress As String
End Type
Private Const FO_COPY = &H2
Sub Sample()
Dim path1 As String, path2 As String
path1 = "\\xxxfileserve\department$\DBA\Opers\All Operators\yyy"
path2 = "\\xxxfileserve\department$\DBA\Opers\All Operators\yyy"
If CopyFolder(path1, path2) Then
MsgBox "Copied"
Else
MsgBox "Not copied"
End If
End Sub
Private Function CopyFolder(ByVal sFrom As String, _
ByVal sTo As String) As Boolean
Dim SHFileOp As SHFILEOPSTRUCT
On Error GoTo Whoa
CopyFolder = False
With SHFileOp
.wFunc = FO_COPY
.pFrom = sFrom
.pTo = sTo
End With
SHFileOperation SHFileOp
CopyFolder = True
Exit Function
Whoa:
MsgBox "Following error occurred while copying folder " & sFrom & vbCrLf & _
Err.Description, vbExclamation, "Error message"
End Function
There are some points regarding the fso.CopyFolder method:
If destination does not exist, the source folder and all its contents gets copied. This is the usual case.
If destination is an existing file, an error occurs.
If destination is a directory, an attempt is made to copy the folder and all its contents.
If a file contained in source already exists in destination, an error occurs if overwrite is False. Otherwise, it will attempt to copy the file over the existing file.
If destination is a read-only directory, an error occurs if an attempt is made to copy an existing read-only file into that directory and overwrite is False.
Make sure not any of these are becoming hindrance for your sub.
But test it another way like this
fso.CopyFolder _
"\\xxxfileserve\department$\DBA\Opers\All Operators\yyy\*", _
"\\xxxfileserve\department$\DBA\Cas\yyy", _
True
Hope this helps.

Source control of Excel VBA code modules

I'd like to be able to source control my Excel spreadsheet's VBA modules (currently using Excel 2003 SP3) so that I can share and manage the code used by a bunch of different spreadsheets - and therefore I'd like to re-load them from files when the spreadsheet is opened.
I've got a module called Loader.bas, that I use to do most of the donkey work (loading and unloading any other modules that are required) - and I'd like to be able to load it up from a file as soon as the spreadsheet is opened.
I've attached the following code to the Workbook_Open event (in the ThisWorkbook class).
Private Sub Workbook_Open()
Call RemoveLoader
Call LoadLoader
End Sub
Where RemoveLoader (also within the ThisWorkbook class) contains the following code:
Private Sub RemoveLoader()
Dim y As Integer
Dim OldModules, NumModules As Integer
Dim CompName As String
With ThisWorkbook.VBProject
NumModules = ThisWorkbook.VBProject.VBComponents.Count
y = 1
While y <= NumModules
If .VBComponents.Item(y).Type = 1 Then
CompName = .VBComponents.Item(y).Name
If VBA.Strings.InStr(CompName, "Loader") > 0 Then
OldModules = ThisWorkbook.VBProject.VBComponents.Count
.VBComponents.Remove .VBComponents(CompName)
NumModules = ThisWorkbook.VBProject.VBComponents.Count
If OldModules - NumModules = 1 Then
y = 1
Else
MsgBox ("Failed to remove " & CompName & " module from VBA project")
End If
End If
End If
y = y + 1
Wend
End With
End Sub
Which is probably a bit overcomplicated and slightly crude - but I'm trying everything I can find to get it to load the external module!
Often, when I open the spreadsheet, the RemoveLoader function finds that there's a "Loader1" module already included in the VBA project that it is unable to remove, and it also fails to load the new Loader module from the file.
Any ideas if what I'm trying to do is possible? Excel seems very fond of appending a 1 to these module names - either when loading or removing (I'm not sure which).
There is an excellent solution to the vba version control problem here: https://github.com/hilkoc/vbaDeveloper
The nice part about this is that it exports your code automatically, as soon as you save your workbook. Also, when you open a workbook, it imports the code.
You don't need to run any build scripts or maven commands and you don't need to make any changes to your workbooks. It works for all.
It has also solved the import problem where modules such as ModName are being imported as ModName1 into a duplicate module. The importing works as it should, even when doing it multiple times.
As a bonus, it comes with a simple code formatter, that allows you to format your vba code as you write it within the VBA Editor.
Look at the VBAMaven page. I have a homegrown solution that uses the same concepts. I have a common library with a bunch of source code, an ant build and an 'import' VB script. Ant controls the build, which takes a blank excel file and pushes the needed code into it. #Mike is absolutely correct - any duplicate module definitions will automatically have a number appended to the module name. Also, class modules (as in Sheet and ThisWorkbook) classes require special treatment. You can't create those modules, you have to read the input file and write the buffer into the appropriate module. This is the VB script I currently use to do this. The section containing # delimited text (i.e. #build file#) are placeholders - the ant build replaces these tags with meaningful content. It's not perfect, but works for me.
''
' Imports VB Basic module and class files from the src folder
' into the excel file stored in the bin folder.
'
Option Explicit
Dim pFileSystem, pFolder, pPath
Dim pShell
Dim pApp, book
Dim pFileName
pFileName = "#build file#"
Set pFileSystem = CreateObject("Scripting.FileSystemObject")
Set pShell = CreateObject("WScript.Shell")
pPath = pShell.CurrentDirectory
If IsExcelFile (pFileName) Then
Set pApp = WScript.CreateObject ("Excel.Application")
pApp.Visible = False
Set book = pApp.Workbooks.Open(pPath & "\build\" & pFileName)
Else
Set pApp = WScript.CreateObject ("Word.Application")
pApp.Visible = False
Set book = pApp.Documents.Open(pPath & "\build\" & pFileName)
End If
'Include root source folder code if no args set
If Wscript.Arguments.Count = 0 Then
Set pFolder = pFileSystem.GetFolder(pPath & "\src")
ImportFiles pFolder, book
'
' Get selected modules from the Common Library, if any
#common path##common file#
Else
'Add code from subdirectories of src . . .
If Wscript.Arguments(0) <> "" Then
Set pFolder = pFileSystem.GetFolder(pPath & "\src\" & Wscript.Arguments(0))
ImportFiles pFolder, book
End If
End If
Set pFolder = Nothing
Set pFileSystem = Nothing
Set pShell = Nothing
If IsExcelFile (pFileName) Then
pApp.ActiveWorkbook.Save
Else
pApp.ActiveDocument.Save
End If
pApp.Quit
Set book = Nothing
Set pApp = Nothing
'' Loops through all the .bas or .cls files in srcFolder
' and calls InsertVBComponent to insert it into the workbook wb.
'
Sub ImportFiles(ByVal srcFolder, ByVal obj)
Dim fileCollection, pFile
Set fileCollection = srcFolder.Files
For Each pFile in fileCollection
If Right(pFile, 3) = "bas _
Or Right(pFile, 3) = "cls _
Or Right(pFile, 3) = "frm Then
InsertVBComponent obj, pFile
End If
Next
Set fileCollection = Nothing
End Sub
'' Inserts the contents of CompFileName as a new component in
' a Workbook or Document object.
'
' If a class file begins with "Sheet", then the code is
' copied into the appropriate code module 1 painful line at a time.
'
' CompFileName must be a valid VBA component (class or module)
Sub InsertVBComponent(ByVal obj, ByVal CompFileName)
Dim t, mName
t = Split(CompFileName, "\")
mName = Split(t(UBound(t)), ".")
If IsSheetCodeModule(mName(0), CompFileName) = True Then
ImportCodeModule obj.VBProject.VBComponents(mName(0)).CodeModule, _
CompFileName
Else
If Not obj Is Nothing Then
obj.VBProject.VBComponents.Import CompFileName
Else
WScript.Echo "Failed to import " & CompFileName
End If
End If
End Sub
''
' Imports the code in the file fName into the workbook object
' referenced by mName.
' #param target destination CodeModule object in the excel file
' #param fName file system file containing code to be imported
Sub ImportCodeModule (ByVal target, ByVal fName)
Dim shtModule, code, buf
Dim fso
Set fso = CreateObject("Scripting.FileSystemObject")
Const ForReading = 1, ForWriting = 2, ForAppending = 3
Const TristateUseDefault = -2, TristateTrue = -1, TristateFalse = 0
Set buf = fso.OpenTextFile(fName, ForReading, False, TristateUseDefault)
buf.SkipLine
code = buf.ReadAll
target.InsertLines 1, code
Set fso = Nothing
End Sub
''
' Returns true if the code module in the file fName
' appears to be a code module for a worksheet.
Function IsSheetCodeModule (ByVal mName, ByVal fName)
IsSheetCodeModule = False
If mName = "ThisWorkbook" Then
IsSheetCodeModule = False
ElseIf Left(mName, 5) = "Sheet" And _
IsNumeric(Mid (mName, 6, 1)) And _
Right(fName, 3) = "cls Then
IsSheetCodeModule = True
End If
End Function
''
' Returns true if fName has a xls file extension
Function IsExcelFile (ByVal fName)
If Right(fName, 3) = "xls" Then
IsExcelFile = True
Else
IsExcelFile = False
End If
End Function
I've been working on exactly this for months. I think I figured it out.
If the VB Project is trying to remove a module containing something in the call stack, it delays the removal until the call stack pops the module being replaced.
To avoid a module being in the call stack, launch your code with Application.OnTime
Private Sub Workbook_Open()
'WAS: module_library (1)
Application.OnTime (Now + TimeValue("00:00:01")), "load_library_kicker_firstiter"
End Sub
If you are self-healing your code like I am, you'll also have to launch your code that overwrites the 'calling' code with that same strategy.
I did not perform extensive testing yet, I am in total celebration mode, but this gets me extremely close to straightforward 99.9% self-healing code within a standalone .xls file without any other tricks
Usually the "Loader1" thing happens when Excel is asked to import a module and a module already exists with the same name. So if you import "Loader", then load it again and you'll get "Loader1". This would be because Excel doesn't know (or maybe just doesn't care) if it's really the same thing or a new chunk of functionality that just happens have the same module name, so it imports it anyway.
I can't think of a perfect solution, but I think I'd be inclined to try putting the load/unload logic in an add-in - that Workbook_Open thing looks a little vulnerable and having it in all workbooks is going to be a huge pain if the code ever needs to change (never say never). The XLA logic might be more complex (trickier to trap the necessary events, for one thing) but at least it'll only exist in one place.
Can't leave comment to comment
There is an excellent solution to the vba version control problem
here: https://github.com/hilkoc/vbaDeveloper
About saving custom VBAProjects using this XLAM.
Try this in Build.bas:
'===============
Public Sub testImport()
Dim proj_name As String
Dim vbaProject As Object
'proj_name = "VBAProject"
'Set vbaProject = Application.VBE.VBProjects(proj_name)
Set vbaProject = Application.VBE.ActiveVBProject
proj_name = vbaProject.name
Build.importVbaCode vbaProject
End Sub
'===============
Public Sub testExport()
Dim proj_name As String
Dim vbaProject As Object
'proj_name = "VBAProject"
'Set vbaProject = Application.VBE.VBProjects(proj_name)
Set vbaProject = Application.VBE.ActiveVBProject
proj_name = vbaProject.name
Build.exportVbaCode vbaProject
End Sub
'===============
This will export/import Active VBA Project.
The following is an easy-to-implement answer if you don't need to export your VBA code automatically. Just Call the following sub and it will export (as text) the VBA code of the current active workbook in a subfolder named "VC_nameOfTheWorkBook". If your project is a .xlam, you need to temporarily set the IsAddin property to false. Then you can easily add the new subfolder to Git. It is a slight modification of the code found here made by Steve Jansen. For a more complete solution see Ron de Bruin post.
You need to set a reference to "Microsoft Visual Basic For Applications Extensibility 5.3" and to "Microsoft Scripting Runtime" in the VBE Editor.
Public Sub ExportVisualBasicCode()
Const Module = 1
Const ClassModule = 2
Const Form = 3
Const Document = 100
Const Padding = 24
Dim VBComponent As Object
Dim path As String
Dim directory As String
Dim extension As String
Dim fso As New FileSystemObject
directory = ActiveWorkbook.path & "\VC_" & fso.GetBaseName(ActiveWorkbook.Name)
If Not fso.FolderExists(directory) Then
Call fso.CreateFolder(directory)
End If
Set fso = Nothing
For Each VBComponent In ActiveWorkbook.VBProject.VBComponents
Select Case VBComponent.Type
Case ClassModule, Document
extension = ".cls"
Case Form
extension = ".frm"
Case Module
extension = ".bas"
Case Else
extension = ".txt"
End Select
On Error Resume Next
Err.Clear
path = directory & "\" & VBComponent.Name & extension
Call VBComponent.Export(path)
If Err.Number <> 0 Then
Call MsgBox("Failed to export " & VBComponent.Name & " to " & path, vbCritical)
Else
Debug.Print "Exported " & Left$(VBComponent.Name & ":" & Space(Padding), Padding) & path
End If
On Error GoTo 0
Next
End Sub